diff --git a/CMakeLists.txt b/CMakeLists.txt index ab42393215..94a1f1a326 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -535,6 +535,10 @@ set(SCENARIO_FILES ${PROJECT_SOURCE_DIR}/src/scenario/editor.c ${PROJECT_SOURCE_DIR}/src/scenario/editor_events.c ${PROJECT_SOURCE_DIR}/src/scenario/editor_map.c + ${PROJECT_SOURCE_DIR}/src/scenario/terrain_generator/terrain_generator.c + ${PROJECT_SOURCE_DIR}/src/scenario/terrain_generator/random_terrain_generator.c + ${PROJECT_SOURCE_DIR}/src/scenario/terrain_generator/river_generator.c + ${PROJECT_SOURCE_DIR}/src/scenario/terrain_generator/river_tools.c ${PROJECT_SOURCE_DIR}/src/scenario/emperor_change.c ${PROJECT_SOURCE_DIR}/src/scenario/empire.c ${PROJECT_SOURCE_DIR}/src/scenario/gladiator_revolt.c @@ -721,6 +725,7 @@ set(WINDOW_FILES ${PROJECT_SOURCE_DIR}/src/window/select_list.c ${PROJECT_SOURCE_DIR}/src/window/set_salary.c ${PROJECT_SOURCE_DIR}/src/window/text_input.c + ${PROJECT_SOURCE_DIR}/src/window/terrain_generator.c ${PROJECT_SOURCE_DIR}/src/window/trade_opened.c ${PROJECT_SOURCE_DIR}/src/window/trade_prices.c ${PROJECT_SOURCE_DIR}/src/window/user_path_setup.c diff --git a/src/core/random.c b/src/core/random.c index 9c80482b4d..c3816a508c 100644 --- a/src/core/random.c +++ b/src/core/random.c @@ -19,6 +19,8 @@ static struct { time_t last_seed; } data; +static int stdlib_use_fixed_seed = 0; + void random_init(void) { memset(&data, 0, sizeof(data)); @@ -97,6 +99,9 @@ void random_save_state(buffer *buf) } int random_from_stdlib(void) { + if (stdlib_use_fixed_seed) { + return rand(); + } time_t t; t = time(&t); if (data.last_seed != t) { @@ -106,6 +111,19 @@ int random_from_stdlib(void) { return rand(); } +void random_set_stdlib_seed(unsigned int seed) +{ + stdlib_use_fixed_seed = 1; + srand(seed); + data.last_seed = 0; +} + +void random_clear_stdlib_seed(void) +{ + stdlib_use_fixed_seed = 0; + data.last_seed = 0; +} + int random_between_from_stdlib(int min, int max) { int diff = max - min; @@ -116,6 +134,17 @@ int random_between_from_stdlib(int min, int max) return min + rnd; } +unsigned int generate_seed_value(void) +{ + unsigned int high = (unsigned int) random_from_stdlib(); + unsigned int low = (unsigned int) random_from_stdlib(); + unsigned int seed = (high << 16) ^ low ^ (unsigned int) time(NULL); + + // Keep bit 31 clear so signed interpretation can never appear negative. + seed &= 0x7fffffffu; + return seed ? seed : 1; +} + double random_fractional_from_stdlib(void) { return (double) random_from_stdlib() / (double) RAND_MAX; diff --git a/src/core/random.h b/src/core/random.h index 3dee1a22cf..047673cd80 100644 --- a/src/core/random.h +++ b/src/core/random.h @@ -70,8 +70,14 @@ void random_load_state(buffer *buf); int random_from_stdlib(void); +void random_set_stdlib_seed(unsigned int seed); + +void random_clear_stdlib_seed(void); + int random_between_from_stdlib(int min, int max); +unsigned int generate_seed_value(void); + double random_fractional_from_stdlib(void); #endif // CORE_RANDOM_H diff --git a/src/game/file_editor.c b/src/game/file_editor.c index b19a614770..1b0990e911 100644 --- a/src/game/file_editor.c +++ b/src/game/file_editor.c @@ -50,6 +50,7 @@ #include "scenario/invasion.h" #include "scenario/map.h" #include "scenario/property.h" +#include "scenario/terrain_generator/terrain_generator.h" #include "sound/city.h" #include "sound/music.h" #include "widget/map_editor.h" @@ -131,6 +132,7 @@ static void prepare_map_for_editing(void) map_tiles_update_all_water(); map_tiles_update_all_earthquake(); map_tiles_update_all_rocks(); + map_tiles_update_all_trees_shrubs(); map_tiles_update_all_empty_land(); map_tiles_update_all_meadow(); map_tiles_update_all_rubble(); @@ -157,6 +159,15 @@ void game_file_editor_create_scenario(int size) scenario_editor_set_custom_victory_message(0); } +void game_file_editor_create_scenario_generated(int size, int generator_algorithm) +{ + create_blank_map(size); + terrain_generator_generate((terrain_generator_algorithm) generator_algorithm); + prepare_map_for_editing(); + scenario_editor_set_custom_message_introduction(0); + scenario_editor_set_custom_victory_message(0); +} + int game_file_editor_load_scenario(const char *scenario_file) { clear_map_data(); diff --git a/src/game/file_editor.h b/src/game/file_editor.h index 7a78b150b3..542aba9b8b 100644 --- a/src/game/file_editor.h +++ b/src/game/file_editor.h @@ -11,6 +11,7 @@ void game_file_editor_clear_data(void); * @param size Size of the map to create */ void game_file_editor_create_scenario(int size); +void game_file_editor_create_scenario_generated(int size, int generator_algorithm); /** * Load scenario from disk and init it for using in the editor diff --git a/src/game/game.c b/src/game/game.c index 666c3e38fc..ba73962f3d 100644 --- a/src/game/game.c +++ b/src/game/game.c @@ -165,14 +165,18 @@ static int reload_language(int is_editor, int reload_images) return 1; } -int game_init_editor(void) +static int game_init_editor_internal(int generated, int size, int generator_algorithm) { if (!reload_language(1, 0)) { return 0; } game_file_editor_clear_data(); - game_file_editor_create_scenario(2); + if (generated) { + game_file_editor_create_scenario_generated(size, generator_algorithm); + } else { + game_file_editor_create_scenario(2); + } if (city_view_is_sidebar_collapsed()) { city_view_toggle_sidebar(); @@ -183,6 +187,16 @@ int game_init_editor(void) return 1; } +int game_init_editor(void) +{ + return game_init_editor_internal(0, 0, 0); +} + +int game_init_editor_generated(int size, int generator_algorithm) +{ + return game_init_editor_internal(1, size, generator_algorithm); +} + void game_exit_editor(void) { if (!reload_language(0, 0)) { diff --git a/src/game/game.h b/src/game/game.h index 49feed9734..38eb9af777 100644 --- a/src/game/game.h +++ b/src/game/game.h @@ -6,6 +6,7 @@ int game_pre_init(void); int game_init(void); int game_init_editor(void); +int game_init_editor_generated(int size, int generator_algorithm); int game_reload_language(void); diff --git a/src/graphics/window.h b/src/graphics/window.h index 6137b05c2d..2c1300d832 100644 --- a/src/graphics/window.h +++ b/src/graphics/window.h @@ -14,6 +14,7 @@ typedef enum { WINDOW_SELECT_CAMPAIGN, WINDOW_MISSION_LIST, WINDOW_CCK_SELECTION, + WINDOW_TERRAIN_GENERATOR, WINDOW_FILE_DIALOG, WINDOW_POPUP_DIALOG, WINDOW_PLAIN_MESSAGE_DIALOG, diff --git a/src/map/random.c b/src/map/random.c index fb31d4a911..18cb44162e 100644 --- a/src/map/random.c +++ b/src/map/random.c @@ -1,13 +1,14 @@ #include "random.h" #include "core/random.h" + #include "map/grid.h" -static grid_u8 random; +static grid_u8 random_value; void map_random_clear(void) { - map_grid_clear_u8(random.items); + map_grid_clear_u8(random_value.items); } void map_random_init(void) @@ -16,14 +17,14 @@ void map_random_init(void) for (int y = 0; y < GRID_SIZE; y++) { for (int x = 0; x < GRID_SIZE; x++, grid_offset++) { random_generate_next(); - random.items[grid_offset] = (uint8_t) random_short(); + random_value.items[grid_offset] = (uint8_t) random_short(); } } } int map_random_get(int grid_offset) { - return random.items[grid_offset]; + return random_value.items[grid_offset]; } int map_random_get_from_buffer(buffer *buf, int grid_offset) @@ -34,10 +35,11 @@ int map_random_get_from_buffer(buffer *buf, int grid_offset) void map_random_save_state(buffer *buf) { - map_grid_save_state_u8(random.items, buf); + map_grid_save_state_u8(random_value.items, buf); } void map_random_load_state(buffer *buf) { - map_grid_load_state_u8(random.items, buf); + map_grid_load_state_u8(random_value.items, buf); } + diff --git a/src/map/random.h b/src/map/random.h index a6ac3a3b70..ee68443128 100644 --- a/src/map/random.h +++ b/src/map/random.h @@ -15,4 +15,5 @@ void map_random_save_state(buffer *buf); void map_random_load_state(buffer *buf); + #endif // MAP_RANDOM_H diff --git a/src/map/terrain.c b/src/map/terrain.c index 212260e1ac..ce6ad209c9 100644 --- a/src/map/terrain.c +++ b/src/map/terrain.c @@ -10,6 +10,7 @@ #include "map/ring.h" #include "map/routing.h" #include "map/sprite.h" +#include "map/tiles.h" #include @@ -94,6 +95,22 @@ void map_terrain_set(int grid_offset, int terrain) terrain_grid.items[grid_offset] = terrain; } +void map_terrain_set_with_tile_update(int grid_offset, int terrain) +{ + map_terrain_set(grid_offset, terrain); + + if (terrain & TERRAIN_TREE) { + int x = map_grid_offset_to_x(grid_offset); + int y = map_grid_offset_to_y(grid_offset); + map_tiles_update_region_trees(x, y, x, y); + } + if (terrain & TERRAIN_SHRUB) { + int x = map_grid_offset_to_x(grid_offset); + int y = map_grid_offset_to_y(grid_offset); + map_tiles_update_region_shrub(x, y, x, y); + } +} + void map_terrain_add(int grid_offset, int terrain) { terrain_grid.items[grid_offset] |= terrain; diff --git a/src/map/terrain.h b/src/map/terrain.h index 48ed98bc7c..23a1076eb3 100644 --- a/src/map/terrain.h +++ b/src/map/terrain.h @@ -89,6 +89,7 @@ int map_terrain_get_from_buffer_16(buffer *buf, int grid_offset); int map_terrain_get_from_buffer_32(buffer *buf, int grid_offset); void map_terrain_set(int grid_offset, int terrain); +void map_terrain_set_with_tile_update(int grid_offset, int terrain); void map_terrain_add(int grid_offset, int terrain); diff --git a/src/map/tiles.c b/src/map/tiles.c index d25f3fdc2d..e1f7075b9f 100644 --- a/src/map/tiles.c +++ b/src/map/tiles.c @@ -216,6 +216,12 @@ void map_tiles_update_region_shrub(int x_min, int y_min, int x_max, int y_max) foreach_region_tile(x_min, y_min, x_max, y_max, set_shrub_image); } +void map_tiles_update_all_trees_shrubs(void) +{ + foreach_map_tile(set_tree_image); + foreach_map_tile(set_shrub_image); +} + static void clear_garden_image(int x, int y, int grid_offset) { if (map_terrain_is(grid_offset, TERRAIN_GARDEN) && diff --git a/src/map/tiles.h b/src/map/tiles.h index 1d91d7f96f..7e196107ba 100644 --- a/src/map/tiles.h +++ b/src/map/tiles.h @@ -5,6 +5,7 @@ void map_tiles_update_all_rocks(void); void map_tiles_update_region_trees(int x_min, int y_min, int x_max, int y_max); void map_tiles_update_region_shrub(int x_min, int y_min, int x_max, int y_max); +void map_tiles_update_all_trees_shrubs(void); void map_tiles_update_all_gardens(void); diff --git a/src/scenario/terrain_generator/random_terrain_generator.c b/src/scenario/terrain_generator/random_terrain_generator.c new file mode 100644 index 0000000000..cb7056a377 --- /dev/null +++ b/src/scenario/terrain_generator/random_terrain_generator.c @@ -0,0 +1,28 @@ +#include "terrain_generator_algorithms.h" + +#include "map/grid.h" +#include "map/terrain.h" + +void terrain_generator_random_terrain(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int grid_offset = map_grid_offset(x, y); + int roll = terrain_generator_random_between(0, 100); + if (roll < 6) { + map_terrain_set_with_tile_update(grid_offset, TERRAIN_TREE); + } else if (roll < 12) { + map_terrain_set_with_tile_update(grid_offset, TERRAIN_SHRUB); + } else if (roll < 18) { + map_terrain_set_with_tile_update(grid_offset, TERRAIN_MEADOW); + } else if (roll < 19) { + map_terrain_set_with_tile_update(grid_offset, TERRAIN_ROCK); + } else if (roll < 20) { + map_terrain_set_with_tile_update(grid_offset, TERRAIN_WATER); + } + } + } +} diff --git a/src/scenario/terrain_generator/river_generator.c b/src/scenario/terrain_generator/river_generator.c new file mode 100644 index 0000000000..2d3bd5f7e9 --- /dev/null +++ b/src/scenario/terrain_generator/river_generator.c @@ -0,0 +1,257 @@ +#include "terrain_generator_algorithms.h" + +#include "map/elevation.h" +#include "map/grid.h" +#include "map/terrain.h" + +#include +#include + +static unsigned int perlin_seed = 1; +static unsigned int mountain_seed = 1; +static unsigned int meadow_seed = 1; + +// Mixes integer coordinates and seed into a stable pseudo-random 32-bit value. +static uint32_t hash_2d(int x, int y, unsigned int seed) +{ + uint32_t h = (uint32_t) x; + h ^= (uint32_t) y * 374761393u; + h ^= seed * 668265263u; + h = (h ^ (h >> 13)) * 1274126177u; + return h ^ (h >> 16); +} + +// Maps the 2D hash to a normalized [0, 1] float. +static float hash_value(int x, int y, unsigned int seed) +{ + return (hash_2d(x, y, seed) & 0xffffu) / 65535.0f; +} + +// Quintic smoothing curve used for interpolation weights. +static float fade(float t) +{ + return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); +} + +// Linear interpolation between two values. +static float lerp(float a, float b, float t) +{ + return a + (b - a) * t; +} + +// Bilinearly interpolated value noise sampled from four lattice corners. +static float value_noise(float x, float y, unsigned int seed) +{ + int x0 = (int) floorf(x); + int y0 = (int) floorf(y); + int x1 = x0 + 1; + int y1 = y0 + 1; + + float sx = fade(x - (float) x0); + float sy = fade(y - (float) y0); + + float n00 = hash_value(x0, y0, seed); + float n10 = hash_value(x1, y0, seed); + float n01 = hash_value(x0, y1, seed); + float n11 = hash_value(x1, y1, seed); + + float ix0 = lerp(n00, n10, sx); + float ix1 = lerp(n01, n11, sx); + return lerp(ix0, ix1, sy); +} + +// Fractal Brownian motion: sums multiple noise octaves for richer structure. +static float fbm(float x, float y, unsigned int seed) +{ + float sum = 0.0f; + float amp = 0.5f; + float freq = 0.02f; + float norm = 0.0f; + + for (int i = 0; i < 4; i++) { + sum += amp * value_noise(x * freq, y * freq, seed + (unsigned int) i * 101u); + norm += amp; + amp *= 0.5f; + freq *= 2.0f; + } + + if (norm > 0.0f) { + sum /= norm; + } + return sum; +} + +// Resets the map to flat grassland as the base layer for procedural passes. +static void generate_grassland(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int grid_offset = map_grid_offset(x, y); + map_terrain_set_with_tile_update(grid_offset, TERRAIN_CLEAR); + map_elevation_set(grid_offset, 0); + } + } +} + +// Places forests using an fBM threshold, but only on untouched clear tiles. +static void add_forests(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int grid_offset = map_grid_offset(x, y); + if (map_terrain_get(grid_offset) != TERRAIN_CLEAR) { + continue; + } + + float n = fbm((float) x, (float) y, perlin_seed); + if (n > 0.58f) { + map_terrain_set_with_tile_update(grid_offset, TERRAIN_TREE); + } + } + } +} + +// Places mountain rock patches in high-noise regions on remaining clear tiles. +static void add_mountains(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int grid_offset = map_grid_offset(x, y); + if (map_terrain_get(grid_offset) != TERRAIN_CLEAR) { + continue; + } + float n = fbm((float) x + 1000.0f, (float) y + 1000.0f, mountain_seed); + if (n > 0.68f) { + // int elevation = 2 + (int) ((n - 0.68f) * 10.0f); + // elevation = terrain_generator_clamp_int(elevation, 2, 4); + // map_elevation_set(map_grid_offset(x, y), elevation); + map_terrain_set_with_tile_update(map_grid_offset(x, y), TERRAIN_ROCK); + } + } + } +} + +// Adds meadow patches from a mid-range noise band on low, clear terrain. +static void add_meadows(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int grid_offset = map_grid_offset(x, y); + if (map_terrain_get(grid_offset) != TERRAIN_CLEAR) { + continue; + } + if (map_elevation_at(grid_offset) > 0) { + continue; + } + if (map_terrain_is(grid_offset, TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_WATER)) { + continue; + } + float n = fbm((float) x + 2500.0f, (float) y + 2500.0f, meadow_seed); + if (n > 0.52f && n < 0.65f) { + map_terrain_set_with_tile_update(grid_offset, TERRAIN_MEADOW); + } + } + } +} + + +// Carves irregular circular lakes with noise-distorted shorelines. +// Not used at the moment. +static void add_lakes(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + int area = width * height; + + int lake_count = 1 + area / 8000; + if (lake_count < 1) { + lake_count = 1; + } else if (lake_count > 6) { + lake_count = 6; + } + + for (int i = 0; i < lake_count; i++) { + int radius = terrain_generator_random_between(2, 6); + int center_x = terrain_generator_random_between(radius + 2, width - radius - 3); + int center_y = terrain_generator_random_between(radius + 2, height - radius - 3); + unsigned int shape_seed = perlin_seed + 3001u + (unsigned int) i * 97u; + + for (int dy = -radius - 2; dy <= radius + 2; dy++) { + for (int dx = -radius - 2; dx <= radius + 2; dx++) { + int x = center_x + dx; + int y = center_y + dy; + if (!map_grid_is_inside(x, y, 1)) { + continue; + } + + float dist = sqrtf((float) (dx * dx + dy * dy)); + float n = fbm((float) x * 0.35f, (float) y * 0.35f, shape_seed); + float threshold = (float) radius * (0.75f + 0.55f * n); + if (dist <= threshold) { + int grid_offset = map_grid_offset(x, y); + map_terrain_set_with_tile_update(grid_offset, TERRAIN_WATER); + map_elevation_set(grid_offset, 0); + } + } + } + } +} + +// Floods one random border to form a simple sea edge. +// Not used at the moment +static void add_sea_edge(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + int side = terrain_generator_random_between(0, 4); + int thickness = terrain_generator_random_between(3, 6); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int is_sea = 0; + if (side == 0 && y < thickness) { + is_sea = 1; + } else if (side == 1 && y >= height - thickness) { + is_sea = 1; + } else if (side == 2 && x < thickness) { + is_sea = 1; + } else if (side == 3 && x >= width - thickness) { + is_sea = 1; + } + if (is_sea) { + int grid_offset = map_grid_offset(x, y); + map_terrain_set_with_tile_update(grid_offset, TERRAIN_WATER); + map_elevation_set(grid_offset, 0); + } + } + } +} + +// Builds a complete terrain pass from base layer through river and biome overlays. +void terrain_generator_river_map(unsigned int seed) +{ + perlin_seed = seed; + mountain_seed = seed; + meadow_seed = seed; + + generate_grassland(); + terrain_generator_generate_river(); + // terrain_generator_straight_river(); + add_forests(); + add_mountains(); + add_meadows(); + // add_lakes(); + // add_sea_edge(); +} diff --git a/src/scenario/terrain_generator/river_tools.c b/src/scenario/terrain_generator/river_tools.c new file mode 100644 index 0000000000..719d241015 --- /dev/null +++ b/src/scenario/terrain_generator/river_tools.c @@ -0,0 +1,410 @@ +#include "terrain_generator_algorithms.h" + +#include "map/elevation.h" +#include "map/grid.h" +#include "map/terrain.h" + +enum { + SIDE_NORTH = 0, + SIDE_SOUTH = 1, + SIDE_WEST = 2, + SIDE_EAST = 3, + DIR_COUNT = 8, +}; + +static const int DIR_X[DIR_COUNT] = { 0, 1, 1, 1, 0, -1, -1, -1 }; +static const int DIR_Y[DIR_COUNT] = { -1, -1, 0, 1, 1, 1, 0, -1 }; + +static int abs_int(int value) +{ + return (value < 0) ? -value : value; +} + +static int sign_int(int value) +{ + return (value > 0) - (value < 0); +} + +static int tile_is_on_side(int x, int y, int width, int height, int side) +{ + switch (side) { + case SIDE_NORTH: + return y == 0; + case SIDE_SOUTH: + return y == height - 1; + case SIDE_WEST: + return x == 0; + default: + return x == width - 1; + } +} + +static void choose_edge_point(int side, int width, int height, int *x, int *y) +{ + switch (side) { + case SIDE_NORTH: + *x = terrain_generator_random_between(0, width); + *y = 0; + break; + case SIDE_SOUTH: + *x = terrain_generator_random_between(0, width); + *y = height - 1; + break; + case SIDE_WEST: + *x = 0; + *y = terrain_generator_random_between(0, height); + break; + default: + *x = width - 1; + *y = terrain_generator_random_between(0, height); + break; + } +} + +static int direction_moves_away_from_side(int dir, int side) +{ + switch (side) { + case SIDE_NORTH: + return DIR_Y[dir] > 0; + case SIDE_SOUTH: + return DIR_Y[dir] < 0; + case SIDE_WEST: + return DIR_X[dir] > 0; + default: + return DIR_X[dir] < 0; + } +} + +static int direction_moves_toward_side(int dir, int side) +{ + switch (side) { + case SIDE_NORTH: + return DIR_Y[dir] < 0; + case SIDE_SOUTH: + return DIR_Y[dir] > 0; + case SIDE_WEST: + return DIR_X[dir] < 0; + default: + return DIR_X[dir] > 0; + } +} + +static void carve_river_tile(int x, int y) +{ + int grid_offset = map_grid_offset(x, y); + map_terrain_set_with_tile_update(grid_offset, TERRAIN_WATER); + map_elevation_set(grid_offset, 0); +} + +static void carve_river_tile_brush(int x, int y, int r) +{ + if (r <= 0) { + carve_river_tile(x, y); + return; + } + + for (int dy = -r; dy <= r; dy++) { + for (int dx = -r; dx <= r; dx++) { + if ((dx * dx) + (dy * dy) > (r * r)) { + continue; + } + + int nx = x + dx; + int ny = y + dy; + if (!map_grid_is_inside(nx, ny, 1)) { + continue; + } + + carve_river_tile(nx, ny); + } + } +} + +typedef struct { + int x; + int y; +} river_tile; + +static river_tile generated_river_tiles[GRID_SIZE * GRID_SIZE]; +static int generated_river_tile_count = 0; + +static void record_river_tile(int x, int y) +{ + if (generated_river_tile_count <= 0) { + generated_river_tiles[generated_river_tile_count].x = x; + generated_river_tiles[generated_river_tile_count].y = y; + generated_river_tile_count++; + return; + } + + river_tile *last = &generated_river_tiles[generated_river_tile_count - 1]; + if (last->x == x && last->y == y) { + return; + } + + if (generated_river_tile_count >= GRID_SIZE * GRID_SIZE) { + return; + } + + generated_river_tiles[generated_river_tile_count].x = x; + generated_river_tiles[generated_river_tile_count].y = y; + generated_river_tile_count++; +} + + + +static void replay_river_tiles(void) +{ + const int river_min_radius = 1; + const int river_max_radius = 10; + int current_river_radius = river_min_radius; + + for (int i = 0; i < generated_river_tile_count; i++) { + // Adjust the river_radius by randomly increasing or decreasing it by 1 step + current_river_radius += terrain_generator_random_between(-1, 2); + if (current_river_radius < river_min_radius) { + current_river_radius = river_min_radius; + } else if (current_river_radius > river_max_radius) { + current_river_radius = river_max_radius; + } + + carve_river_tile_brush(generated_river_tiles[i].x, generated_river_tiles[i].y, current_river_radius); + } +} + +static int choose_next_direction( + int x, + int y, + int target_x, + int target_y, + int previous_dir, + int phase, + int start_side, + int end_side, + int width, + int height, + int step, + int min_exit_steps) +{ + int total_weight = 0; + int weights[DIR_COUNT] = { 0 }; + + int target_step_x = sign_int(target_x - x); + int target_step_y = sign_int(target_y - y); + + for (int dir = 0; dir < DIR_COUNT; dir++) { + int nx = x + DIR_X[dir]; + int ny = y + DIR_Y[dir]; + if (!map_grid_is_inside(nx, ny, 1)) { + continue; + } + + int weight = 1; + + if (previous_dir >= 0) { + int delta = dir - previous_dir; + if (delta < 0) { + delta = -delta; + } + if (delta > DIR_COUNT / 2) { + delta = DIR_COUNT - delta; + } + + if (delta == 4) { + continue; + } else if (delta == 0) { + weight += 8; + } else if (delta == 1) { + weight += 3; + } else if (delta == 2) { + weight += 1; + } + } + + int alignment = DIR_X[dir] * target_step_x + DIR_Y[dir] * target_step_y; + if (alignment >= 2) { + weight += (phase == 0) ? 5 : 4; + } else if (alignment == 1) { + weight += 2; + } + + if (phase == 0 && direction_moves_away_from_side(dir, start_side)) { + weight += 3; + } + if (phase == 1 && direction_moves_toward_side(dir, end_side)) { + weight += 2; + } + + if (step < min_exit_steps && tile_is_on_side(nx, ny, width, height, start_side)) { + weight -= 4; + } + + weight += terrain_generator_random_between(0, 3); + + if (weight <= 0) { + continue; + } + + weights[dir] = weight; + total_weight += weight; + } + + if (total_weight <= 0) { + return -1; + } + + int pick = terrain_generator_random_between(0, total_weight); + for (int dir = 0; dir < DIR_COUNT; dir++) { + if (weights[dir] <= 0) { + continue; + } + if (pick < weights[dir]) { + return dir; + } + pick -= weights[dir]; + } + + return -1; +} + + + +void terrain_generator_straight_river(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + if (width <= 1 || height <= 1) { + return; + } + + generated_river_tile_count = 0; + + int start_side = terrain_generator_random_between(0, 4); + int x = 0; + int y = 0; + choose_edge_point(start_side, width, height, &x, &y); + + int end_x = x; + int end_y = y; + switch (start_side) { + case SIDE_NORTH: + end_y = height - 1; + break; + case SIDE_SOUTH: + end_y = 0; + break; + case SIDE_WEST: + end_x = width - 1; + break; + default: + end_x = 0; + break; + } + + int step_x = sign_int(end_x - x); + int step_y = sign_int(end_y - y); + int guard = 0; + int max_steps = width + height + 4; + + while ((x != end_x || y != end_y) && guard++ < max_steps) { + record_river_tile(x, y); + x += step_x; + y += step_y; + } + + record_river_tile(end_x, end_y); + replay_river_tiles(); +} + +void terrain_generator_generate_river(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + if (width <= 1 || height <= 1) { + return; + } + + generated_river_tile_count = 0; + + int start_side = terrain_generator_random_between(0, 4); + int end_side = terrain_generator_random_between(0, 4); + while (end_side == start_side) { + end_side = terrain_generator_random_between(0, 4); + } + + int x = 0; + int y = 0; + int end_x = 0; + int end_y = 0; + choose_edge_point(start_side, width, height, &x, &y); + choose_edge_point(end_side, width, height, &end_x, &end_y); + + int center_x = width / 2; + int center_y = height / 2; + int center_radius = terrain_generator_clamp_int((width + height) / 16, 4, 12); + int min_exit_steps = terrain_generator_clamp_int((width + height) / 6, 8, 80); + + int previous_dir = -1; + int phase = 0; + int max_steps = width * height; + + for (int step = 0; step < max_steps; step++) { + record_river_tile(x, y); + + if (phase == 0) { + int distance_to_center = abs_int(x - center_x) + abs_int(y - center_y); + if (distance_to_center <= center_radius) { + phase = 1; + } + } + + if (phase == 1 && step >= min_exit_steps && tile_is_on_side(x, y, width, height, end_side)) { + break; + } + + int target_x = (phase == 0) ? center_x : end_x; + int target_y = (phase == 0) ? center_y : end_y; + int dir = choose_next_direction( + x, + y, + target_x, + target_y, + previous_dir, + phase, + start_side, + end_side, + width, + height, + step, + min_exit_steps); + if (dir < 0) { + break; + } + + x += DIR_X[dir]; + y += DIR_Y[dir]; + previous_dir = dir; + } + + for (int guard = 0; guard < width + height; guard++) { + if (tile_is_on_side(x, y, width, height, end_side)) { + break; + } + + record_river_tile(x, y); + + int step_x = sign_int(end_x - x); + int step_y = sign_int(end_y - y); + if (abs_int(end_x - x) >= abs_int(end_y - y)) { + x += step_x; + } else { + y += step_y; + } + x = terrain_generator_clamp_int(x, 0, width - 1); + y = terrain_generator_clamp_int(y, 0, height - 1); + } + + record_river_tile(x, y); + + replay_river_tiles(); +} diff --git a/src/scenario/terrain_generator/terrain_generator.c b/src/scenario/terrain_generator/terrain_generator.c new file mode 100644 index 0000000000..d475fcfeac --- /dev/null +++ b/src/scenario/terrain_generator/terrain_generator.c @@ -0,0 +1,212 @@ +#include "terrain_generator.h" + +#include "terrain_generator_algorithms.h" + +#include "core/random.h" +#include "map/elevation.h" +#include "map/grid.h" +#include "map/property.h" +#include "map/terrain.h" +#include "map/tiles.h" +#include "scenario/editor_map.h" + +#include + +static int use_fixed_seed = 0; +static unsigned int fixed_seed = 0; + +int terrain_generator_clamp_int(int value, int min_value, int max_value) +{ + if (value < min_value) { + return min_value; + } + if (value > max_value) { + return max_value; + } + return value; +} + +int terrain_generator_random_between(int min_value, int max_value) +{ + return random_between_from_stdlib(min_value, max_value); +} + +static void clear_base_terrain(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int grid_offset = map_grid_offset(x, y); + map_terrain_set(grid_offset, TERRAIN_CLEAR); + map_elevation_set(grid_offset, 0); + } + } +} + +static void choose_edge_point(int side, int width, int height, int *x, int *y) +{ + switch (side) { + case 0: // north + *y = 0; + *x = terrain_generator_random_between(1, width - 1); + break; + case 1: // south + *y = height - 1; + *x = terrain_generator_random_between(1, width - 1); + break; + case 2: // west + *x = 0; + *y = terrain_generator_random_between(1, height - 1); + break; + default: // east + *x = width - 1; + *y = terrain_generator_random_between(1, height - 1); + break; + } + + if (width > 2) { + *x = terrain_generator_clamp_int(*x, 0, width - 1); + } + if (height > 2) { + *y = terrain_generator_clamp_int(*y, 0, height - 1); + } +} + +static void adjust_point_to_land(int *x, int *y, int width, int height) +{ + int grid_offset = map_grid_offset(*x, *y); + if (!map_terrain_is(grid_offset, TERRAIN_WATER)) { + return; + } + + for (int radius = 1; radius <= 10; radius++) { + for (int dy = -radius; dy <= radius; dy++) { + for (int dx = -radius; dx <= radius; dx++) { + int nx = *x + dx; + int ny = *y + dy; + if (!map_grid_is_inside(nx, ny, 1)) { + continue; + } + int offset = map_grid_offset(nx, ny); + if (!map_terrain_is(offset, TERRAIN_WATER)) { + *x = nx; + *y = ny; + return; + } + } + } + } +} + +static void set_road_tile(int x, int y) +{ + int grid_offset = map_grid_offset(x, y); + map_terrain_remove(grid_offset, + TERRAIN_WATER | TERRAIN_TREE | TERRAIN_SHRUB | TERRAIN_ROCK | TERRAIN_MEADOW | TERRAIN_ELEVATION | TERRAIN_ACCESS_RAMP); + map_terrain_add(grid_offset, TERRAIN_ROAD); + map_property_clear_constructing(grid_offset); + map_property_set_multi_tile_size(grid_offset, 1); + map_property_mark_draw_tile(grid_offset); + map_tiles_update_area_roads(x, y, 3); + map_tiles_update_region_empty_land(x - 1, y - 1, x + 1, y + 1); +} + +static void add_road_between_points(int start_x, int start_y, int end_x, int end_y) +{ + int x = start_x; + int y = start_y; + int guard = 0; + int max_steps = map_grid_width() * map_grid_height() * 2; + + set_road_tile(x, y); + while ((x != end_x || y != end_y) && guard++ < max_steps) { + int dx = end_x - x; + int dy = end_y - y; + int step_x = (dx > 0) - (dx < 0); + int step_y = (dy > 0) - (dy < 0); + int abs_dx = abs(dx); + int abs_dy = abs(dy); + int roll = terrain_generator_random_between(0, 100); + + if (abs_dx >= abs_dy) { + if (step_x && roll < 70) { + x += step_x; + } else if (step_y) { + y += step_y; + } + } else { + if (step_y && roll < 70) { + y += step_y; + } else if (step_x) { + x += step_x; + } + } + + x = terrain_generator_clamp_int(x, 0, map_grid_width() - 1); + y = terrain_generator_clamp_int(y, 0, map_grid_height() - 1); + set_road_tile(x, y); + } +} + +static void set_entry_exit_points(void) +{ + int width = map_grid_width(); + int height = map_grid_height(); + int entry_side = terrain_generator_random_between(0, 4); + int exit_side = terrain_generator_random_between(0, 4); + while (exit_side == entry_side) { + exit_side = terrain_generator_random_between(0, 4); + } + + int entry_x = 0; + int entry_y = 0; + int exit_x = 0; + int exit_y = 0; + + choose_edge_point(entry_side, width, height, &entry_x, &entry_y); + choose_edge_point(exit_side, width, height, &exit_x, &exit_y); + + adjust_point_to_land(&entry_x, &entry_y, width, height); + adjust_point_to_land(&exit_x, &exit_y, width, height); + + scenario_editor_set_entry_point(entry_x, entry_y); + scenario_editor_set_exit_point(exit_x, exit_y); + + add_road_between_points(entry_x, entry_y, exit_x, exit_y); +} + +void terrain_generator_generate(terrain_generator_algorithm algorithm) +{ + if (use_fixed_seed) { + random_set_stdlib_seed(fixed_seed); + } else { + fixed_seed = terrain_generator_random_between(1, 0x7fffffff); + use_fixed_seed = 1; + random_set_stdlib_seed(fixed_seed); + } + + + + clear_base_terrain(); + + switch (algorithm) { + case TERRAIN_GENERATOR_RIVER: + terrain_generator_river_map(fixed_seed); + break; + case TERRAIN_GENERATOR_RANDOM: + default: + terrain_generator_random_terrain(); + break; + } + + set_entry_exit_points(); + + random_clear_stdlib_seed(); +} + +void terrain_generator_set_seed(int enabled, unsigned int seed) +{ + use_fixed_seed = enabled != 0; + fixed_seed = seed; +} diff --git a/src/scenario/terrain_generator/terrain_generator.h b/src/scenario/terrain_generator/terrain_generator.h new file mode 100644 index 0000000000..33fa3fb541 --- /dev/null +++ b/src/scenario/terrain_generator/terrain_generator.h @@ -0,0 +1,14 @@ +#ifndef SCENARIO_TERRAIN_GENERATOR_H +#define SCENARIO_TERRAIN_GENERATOR_H + +typedef enum { + TERRAIN_GENERATOR_RANDOM = 1, + TERRAIN_GENERATOR_RIVER = 0, + TERRAIN_GENERATOR_COUNT = 2 +} terrain_generator_algorithm; + +void terrain_generator_generate(terrain_generator_algorithm algorithm); + +void terrain_generator_set_seed(int enabled, unsigned int seed); + +#endif // SCENARIO_TERRAIN_GENERATOR_H diff --git a/src/scenario/terrain_generator/terrain_generator_algorithms.h b/src/scenario/terrain_generator/terrain_generator_algorithms.h new file mode 100644 index 0000000000..052888e8a8 --- /dev/null +++ b/src/scenario/terrain_generator/terrain_generator_algorithms.h @@ -0,0 +1,11 @@ +#ifndef SCENARIO_TERRAIN_GENERATOR_ALGORITHMS_H +#define SCENARIO_TERRAIN_GENERATOR_ALGORITHMS_H + +int terrain_generator_random_between(int min_value, int max_value); +int terrain_generator_clamp_int(int value, int min_value, int max_value); + +void terrain_generator_random_terrain(void); +void terrain_generator_straight_river(void); +void terrain_generator_generate_river(void); +void terrain_generator_river_map(unsigned int); +#endif // SCENARIO_TERRAIN_GENERATOR_ALGORITHMS_H diff --git a/src/translation/english.c b/src/translation/english.c index c1d03d4967..630216df09 100644 --- a/src/translation/english.c +++ b/src/translation/english.c @@ -1471,6 +1471,7 @@ static translation_string all_strings[] = { {TR_CHEAT_EDITOR_WARNING_TITLE, "Warning"}, {TR_CHEAT_EDITOR_WARNING_TEXT, "Running the map editor from city mode can corrupt the save or cause the game to crash.\n\nProceed at your own risk and make sure you have a backup of the current save."}, {TR_MAIN_MENU_SELECT_CAMPAIGN, "Start new campaign"}, + {TR_MAIN_MENU_TERRAIN_GENERATOR, "Terrain generator"}, {TR_WINDOW_SELECT_CAMPAIGN, "Select a campaign"}, {TR_WINDOW_CAMPAIGN_AUTHOR, "Author:"}, {TR_WINDOW_CAMPAIGN_NO_DESC, "No description"}, diff --git a/src/translation/french.c b/src/translation/french.c index 59b7ebf385..d0a22c70d9 100644 --- a/src/translation/french.c +++ b/src/translation/french.c @@ -1470,6 +1470,7 @@ static translation_string all_strings[] = { {TR_CHEAT_EDITOR_WARNING_TITLE, "Avertissement"}, {TR_CHEAT_EDITOR_WARNING_TEXT, "Lancer l'éditeur depuis une cité peut corrompre la sauvegarde ou provoquer un arrêt inopiné.\n\nEffectuez une copie de la sauvegarde avant de continuer."}, {TR_MAIN_MENU_SELECT_CAMPAIGN, "Nouvelle campagne"}, + {TR_MAIN_MENU_TERRAIN_GENERATOR, "Terrain generator"}, {TR_WINDOW_SELECT_CAMPAIGN, "Choisir une campagne"}, {TR_WINDOW_CAMPAIGN_AUTHOR, "Auteur :"}, {TR_WINDOW_CAMPAIGN_NO_DESC, "Pas de description"}, diff --git a/src/translation/german.c b/src/translation/german.c index c8cbd1ccf1..c6d7e83f8e 100644 --- a/src/translation/german.c +++ b/src/translation/german.c @@ -1241,6 +1241,7 @@ static translation_string all_strings[] = { {TR_WINDOW_RACE_GREEN_HORSE_DESCRIPTION, "Das grüne Team - Nachfahren der \"Celeres\"-Reiter aus der Zeit des römischen Königreichs. Sie behaupten immer noch, die Besten der Besten zu sein."}, /* System - Kampagnen-Menü */ {TR_MAIN_MENU_SELECT_CAMPAIGN, "Neue Kampagne starten"}, + {TR_MAIN_MENU_TERRAIN_GENERATOR, "Terrain generator"}, {TR_WINDOW_SELECT_CAMPAIGN, "Eine Kampagne auswählen"}, {TR_WINDOW_CAMPAIGN_AUTHOR, "Autor:"}, {TR_WINDOW_CAMPAIGN_NO_DESC, "Keine Beschreibung"}, diff --git a/src/translation/greek.c b/src/translation/greek.c index 481fb55c5a..e06a3110c2 100644 --- a/src/translation/greek.c +++ b/src/translation/greek.c @@ -1477,6 +1477,7 @@ static translation_string all_strings[] = { { TR_CHEAT_EDITOR_WARNING_TITLE, "Προειδοποίηση" }, { TR_CHEAT_EDITOR_WARNING_TEXT, "Η εκτέλεση του προγράμματος επεξεργασίας χαρτών από τη λειτουργία πόλης μπορεί να καταστρέψει την αποθήκευση ή να προκαλέσει σφάλμα στο παιχνίδι.\n\nΠροχωρήστε με δική σας ευθύνη και βεβαιωθείτε ότι έχετε δημιουργήσει αντίγραφο ασφαλείας της τρέχουσας αποθήκευσης." }, { TR_MAIN_MENU_SELECT_CAMPAIGN, "Έναρξη νέας εκστρατείας" }, + { TR_MAIN_MENU_TERRAIN_GENERATOR, "Terrain generator" }, { TR_WINDOW_SELECT_CAMPAIGN, "Επιλέξτε μια εκστρατεία" }, { TR_WINDOW_CAMPAIGN_AUTHOR, "Συγγραφέας:" }, { TR_WINDOW_CAMPAIGN_NO_DESC, "Καμία περιγραφή" }, diff --git a/src/translation/italian.c b/src/translation/italian.c index 02ba106f8b..8a52150a42 100755 --- a/src/translation/italian.c +++ b/src/translation/italian.c @@ -1470,6 +1470,7 @@ static translation_string all_strings[] = { {TR_CHEAT_EDITOR_WARNING_TITLE, "Attenzione"}, {TR_CHEAT_EDITOR_WARNING_TEXT, "Usare l'editor di mappe in modalità città può corrompere il salvataggio o causare un crash del gioco.\n\nProcedere a proprio rischio e pericolo e assicurarsi di avere una copia di sicurezza del salvataggio corrente."}, {TR_MAIN_MENU_SELECT_CAMPAIGN, "Inizia nuova campagna"}, + {TR_MAIN_MENU_TERRAIN_GENERATOR, "Terrain generator"}, {TR_WINDOW_SELECT_CAMPAIGN, "Seleziona una campagna"}, {TR_WINDOW_CAMPAIGN_AUTHOR, "Autore:"}, {TR_WINDOW_CAMPAIGN_NO_DESC, "Nessuna descrizione"}, diff --git a/src/translation/korean.c b/src/translation/korean.c index 22cb7eb8ff..df09f49354 100644 --- a/src/translation/korean.c +++ b/src/translation/korean.c @@ -1469,6 +1469,7 @@ static translation_string all_strings[] = { {TR_CHEAT_EDITOR_WARNING_TITLE, "경고"}, {TR_CHEAT_EDITOR_WARNING_TEXT, "도시 모드에서 지도 편집기를 실행하면 저장 파일이 손상되거나 게임이 깨질 수 있습니다.\n진행은 사용자의 책임이며, 현재 저장 파일을 반드시 백업하십시오."}, {TR_MAIN_MENU_SELECT_CAMPAIGN, "새 캠페인 시작"}, + {TR_MAIN_MENU_TERRAIN_GENERATOR, "Terrain generator"}, {TR_WINDOW_SELECT_CAMPAIGN, "캠페인을 선택하세요"}, {TR_WINDOW_CAMPAIGN_AUTHOR, "원작자:"}, {TR_WINDOW_CAMPAIGN_NO_DESC, "설명 없음"}, diff --git a/src/translation/translation.h b/src/translation/translation.h index a25e6f3896..b876812315 100644 --- a/src/translation/translation.h +++ b/src/translation/translation.h @@ -1472,6 +1472,7 @@ typedef enum { TR_BUILDING_ARENA_DESC_UPGRADED_NO_LIONS, TR_BUILDING_ARENA_DESC_UPGRADED_NO_SHOWS, TR_MAIN_MENU_SELECT_CAMPAIGN, + TR_MAIN_MENU_TERRAIN_GENERATOR, TR_WINDOW_SELECT_CAMPAIGN, TR_WINDOW_CAMPAIGN_AUTHOR, TR_WINDOW_CAMPAIGN_NO_DESC, diff --git a/src/window/main_menu.c b/src/window/main_menu.c index 1c21af036b..0ac5775089 100644 --- a/src/window/main_menu.c +++ b/src/window/main_menu.c @@ -26,10 +26,10 @@ #include "window/plain_message_dialog.h" #include "window/popup_dialog.h" #include "window/select_campaign.h" +#include "window/terrain_generator.h" #include "window/video.h" -#define MAX_BUTTONS 6 - +#define MAX_BUTTONS 7 static void button_click(const generic_button *button); static struct { @@ -39,11 +39,12 @@ static struct { static generic_button buttons[] = { {192, 130, 256, 25, button_click, 0, 1}, - {192, 170, 256, 25, button_click, 0, 2}, - {192, 210, 256, 25, button_click, 0, 3}, - {192, 250, 256, 25, button_click, 0, 4}, - {192, 290, 256, 25, button_click, 0, 5}, - {192, 330, 256, 25, button_click, 0, 6}, + {192, 164, 256, 25, button_click, 0, 2}, + {192, 198, 256, 25, button_click, 0, 3}, + {192, 232, 256, 25, button_click, 0, 4}, + {192, 266, 256, 25, button_click, 0, 5}, + {192, 300, 256, 25, button_click, 0, 6}, + {192, 334, 256, 25, button_click, 0, 7}, }; static void draw_version_string(void) @@ -89,11 +90,12 @@ static void draw_foreground(void) } lang_text_draw_centered(CUSTOM_TRANSLATION, TR_MAIN_MENU_SELECT_CAMPAIGN, 192, 137, 256, FONT_NORMAL_GREEN); - lang_text_draw_centered(30, 2, 192, 177, 256, FONT_NORMAL_GREEN); - lang_text_draw_centered(30, 3, 192, 217, 256, FONT_NORMAL_GREEN); - lang_text_draw_centered(9, 8, 192, 257, 256, FONT_NORMAL_GREEN); - lang_text_draw_centered(2, 0, 192, 297, 256, FONT_NORMAL_GREEN); - lang_text_draw_centered(30, 5, 192, 337, 256, FONT_NORMAL_GREEN); + lang_text_draw_centered(30, 2, 192, 171, 256, FONT_NORMAL_GREEN); + lang_text_draw_centered(30, 3, 192, 205, 256, FONT_NORMAL_GREEN); + lang_text_draw_centered(9, 8, 192, 239, 256, FONT_NORMAL_GREEN); + lang_text_draw_centered(CUSTOM_TRANSLATION, TR_MAIN_MENU_TERRAIN_GENERATOR, 192, 273, 256, FONT_NORMAL_GREEN); + lang_text_draw_centered(2, 0, 192, 307, 256, FONT_NORMAL_GREEN); + lang_text_draw_centered(30, 5, 192, 341, 256, FONT_NORMAL_GREEN); graphics_reset_dialog(); } @@ -138,8 +140,11 @@ static void button_click(const generic_button *button) sound_music_play_editor(); } } else if (type == 5) { - window_config_show(CONFIG_FIRST_PAGE, 0, 1); + (void) button; + window_terrain_generator_show(); } else if (type == 6) { + window_config_show(CONFIG_FIRST_PAGE, 0, 1); + } else if (type == 7) { window_popup_dialog_show(POPUP_DIALOG_QUIT, confirm_exit, 1); } } diff --git a/src/window/terrain_generator.c b/src/window/terrain_generator.c new file mode 100644 index 0000000000..d925bc1f8c --- /dev/null +++ b/src/window/terrain_generator.c @@ -0,0 +1,902 @@ +#include "terrain_generator.h" + +#include "assets/assets.h" +#include "city/view.h" +#include "core/config.h" +#include "core/dir.h" +#include "core/image_group.h" +#include "core/random.h" +#include "core/string.h" +#include "editor/editor.h" +#include "game/file.h" +#include "game/file_editor.h" +#include "game/game.h" +#include "graphics/button.h" +#include "graphics/generic_button.h" +#include "graphics/graphics.h" +#include "graphics/image.h" +#include "graphics/lang_text.h" +#include "graphics/panel.h" +#include "graphics/text.h" +#include "graphics/window.h" +#include "input/input.h" +#include "map/aqueduct.h" +#include "map/building.h" +#include "map/desirability.h" +#include "map/elevation.h" +#include "map/figure.h" +#include "map/grid.h" +#include "map/image.h" +#include "map/image_context.h" +#include "map/property.h" +#include "map/random.h" +#include "map/road_network.h" +#include "map/soldier_strength.h" +#include "map/sprite.h" +#include "map/terrain.h" +#include "map/tiles.h" +#include "scenario/editor.h" +#include "scenario/data.h" +#include "scenario/map.h" +#include "scenario/property.h" +#include "scenario/terrain_generator/terrain_generator.h" +#include "sound/music.h" +#include "translation/translation.h" +#include "widget/input_box.h" +#include "widget/minimap.h" +#include "window/plain_message_dialog.h" +#include "window/select_list.h" +#include "window/video.h" +#include "window/city.h" +#include "window/editor/map.h" + +#include +#include + +#define WINDOW_WIDTH 640 +#define WINDOW_HEIGHT 480 + +#define CONTROL_LABEL_X 32 +#define CONTROL_VALUE_X 160 +#define CONTROL_BUTTON_WIDTH 120 +#define CONTROL_BUTTON_HEIGHT 20 + +#define SIZE_BUTTON_Y 60 +#define ALGORITHM_BUTTON_Y 84 +#define SEED_INPUT_Y 108 +#define RANDOMIZE_BUTTON_Y 144 +#define CLIMATE_BUTTON_Y 168 + + +#define SETTINGS_LABEL_Y 135 +#define SETTINGS_ROW_Y 155 +#define SETTINGS_ROW_HEIGHT 22 + +#define VICTORY_LABEL_Y (SETTINGS_ROW_Y + SETTINGS_ROW_HEIGHT * 4 + 8) +#define VICTORY_ROW_Y (VICTORY_LABEL_Y + 20) + +#define SETTING_BUTTON_WIDTH 120 +#define TOGGLE_BUTTON_WIDTH 60 +#define VALUE_BUTTON_WIDTH 90 +#define CRITERIA_TOGGLE_X CONTROL_VALUE_X +#define CRITERIA_VALUE_X (CONTROL_VALUE_X + TOGGLE_BUTTON_WIDTH + 8) + +#define ACTION_BUTTON_WIDTH 288 +#define ACTION_BUTTON_HEIGHT 24 +#define ACTION_BUTTON_X 320 + +#define REGENERATE_BUTTON_Y 356 +#define OPEN_EDITOR_BUTTON_Y 384 +#define START_GAME_BUTTON_Y 412 +#define BACK_BUTTON_Y 440 + + +#define PREVIEW_X 320 +#define PREVIEW_Y 60 +#define PREVIEW_WIDTH 288 +// #define PREVIEW_HEIGHT 240 +#define PREVIEW_HEIGHT 288 + +#define SEED_TEXT_LENGTH 16 +#define TERRAIN_GENERATOR_SIZE_COUNT 6 + +#define CLIMATE_COUNT 3 + +#define SETTINGS_INPUT_WIDTH_BLOCKS 8 +#define SETTINGS_INPUT_HEIGHT_BLOCKS 2 +#define SETTINGS_INPUT_X CONTROL_VALUE_X +#define SETTINGS_START_Y 196 +#define SETTINGS_ROW_SPACING 24 +#define SETTINGS_TEXT_LENGTH 12 + +#define FUNDS_MIN 0 +#define FUNDS_MAX 99999 +#define CRITERIA_PERCENT_MIN 0 +#define CRITERIA_PERCENT_MAX 100 +#define POPULATION_GOAL_MIN 0 +#define POPULATION_GOAL_MAX 50000 + +static void button_select_size(const generic_button *button); +static void button_select_algorithm(const generic_button *button); +static void button_select_climate(const generic_button *button); +static void button_randomize(const generic_button *button); +static void button_regenerate(const generic_button *button); +static void button_open_editor(const generic_button *button); +static void button_start_game(const generic_button *button); +static void button_back(const generic_button *button); + +static void size_selected(int id); +static void algorithm_selected(int id); +static void climate_selected(int id); +static void sync_settings_from_scenario(void); +static void apply_scenario_settings(void); +static void randomize_scenario_settings(void); +static void sync_settings_to_inputs(void); +static void sync_inputs_to_settings(void); +static void set_input_box_value(input_box *box, int value); +static void initial_funds_on_change(int is_addition_at_end); +static void culture_goal_on_change(int is_addition_at_end); +static void prosperity_goal_on_change(int is_addition_at_end); +static void peace_goal_on_change(int is_addition_at_end); +static void favor_goal_on_change(int is_addition_at_end); +static void population_goal_on_change(int is_addition_at_end); + +static const uint8_t label_size[] = "Size"; +static const uint8_t label_algorithm[] = "Algorithm"; +static const uint8_t label_seed[] = "Seed"; +static const uint8_t label_randomize[] = "Randomize"; +static const uint8_t label_climate[] = "Climate"; +static const uint8_t label_regenerate[] = "Regenerate preview"; +static const uint8_t label_open_editor[] = "Open in editor"; +static const uint8_t label_start_game[] = "Start game"; +static const uint8_t label_back[] = "Back"; +static const uint8_t label_seed_placeholder[] = "Random"; +static const uint8_t label_initial_funds[] = "Initial funds"; +static const uint8_t label_culture_goal[] = "Culture goal"; +static const uint8_t label_prosperity_goal[] = "Prosperity goal"; +static const uint8_t label_peace_goal[] = "Peace goal"; +static const uint8_t label_favor_goal[] = "Favor goal"; +static const uint8_t label_population_goal[] = "Population goal"; + +static const uint8_t size_label_40[] = "40 x 40"; +static const uint8_t size_label_60[] = "60 x 60"; +static const uint8_t size_label_80[] = "80 x 80"; +static const uint8_t size_label_100[] = "100 x 100"; +static const uint8_t size_label_120[] = "120 x 120"; +static const uint8_t size_label_160[] = "160 x 160"; + +static const uint8_t label_climate_central[] = "Central"; +static const uint8_t label_climate_northern[] = "Northern"; +static const uint8_t label_climate_desert[] = "Desert"; + + +static const uint8_t *climate_labels[CLIMATE_COUNT] = { + label_climate_central, + label_climate_northern, + label_climate_desert +}; + +static const uint8_t *terrain_generator_size_labels[TERRAIN_GENERATOR_SIZE_COUNT] = { + size_label_40, + size_label_60, + size_label_80, + size_label_100, + size_label_120, + size_label_160 +}; + +static const uint8_t *terrain_generator_algorithm_labels[TERRAIN_GENERATOR_COUNT]; + + +static generic_button buttons[] = { + {CONTROL_VALUE_X, SIZE_BUTTON_Y, CONTROL_BUTTON_WIDTH, CONTROL_BUTTON_HEIGHT, button_select_size, 0, 0}, + {CONTROL_VALUE_X, ALGORITHM_BUTTON_Y, CONTROL_BUTTON_WIDTH, CONTROL_BUTTON_HEIGHT, button_select_algorithm, 0, 0}, + {CONTROL_VALUE_X, CLIMATE_BUTTON_Y, CONTROL_BUTTON_WIDTH, CONTROL_BUTTON_HEIGHT, button_select_climate, 0, 0}, + {CONTROL_VALUE_X, RANDOMIZE_BUTTON_Y, CONTROL_BUTTON_WIDTH, CONTROL_BUTTON_HEIGHT, button_randomize, 0, 0}, + {ACTION_BUTTON_X, REGENERATE_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT, button_regenerate, 0, 0}, + {ACTION_BUTTON_X, OPEN_EDITOR_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT, button_open_editor, 0, 0}, + {ACTION_BUTTON_X, START_GAME_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT, button_start_game, 0, 0}, + {ACTION_BUTTON_X, BACK_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT, button_back, 0, 0} +}; + +static input_box seed_input = { + CONTROL_VALUE_X, + SEED_INPUT_Y, + 11, + 2, + FONT_NORMAL_WHITE, + 0, + NULL, + SEED_TEXT_LENGTH, + 0, + label_seed_placeholder, + NULL, + NULL, + INPUT_BOX_CHARS_NUMERIC +}; + +static uint8_t initial_funds_text[SETTINGS_TEXT_LENGTH] = { 0 }; +static uint8_t culture_goal_text[SETTINGS_TEXT_LENGTH] = { 0 }; +static uint8_t prosperity_goal_text[SETTINGS_TEXT_LENGTH] = { 0 }; +static uint8_t peace_goal_text[SETTINGS_TEXT_LENGTH] = { 0 }; +static uint8_t favor_goal_text[SETTINGS_TEXT_LENGTH] = { 0 }; +static uint8_t population_goal_text[SETTINGS_TEXT_LENGTH] = { 0 }; + +static input_box initial_funds_input = { + SETTINGS_INPUT_X, + SETTINGS_START_Y, + SETTINGS_INPUT_WIDTH_BLOCKS, + SETTINGS_INPUT_HEIGHT_BLOCKS, + FONT_NORMAL_WHITE, + 0, + initial_funds_text, + SETTINGS_TEXT_LENGTH, + 0, + NULL, + initial_funds_on_change, + NULL, + INPUT_BOX_CHARS_NUMERIC +}; + +static input_box culture_goal_input = { + SETTINGS_INPUT_X, + SETTINGS_START_Y + SETTINGS_ROW_SPACING, + SETTINGS_INPUT_WIDTH_BLOCKS, + SETTINGS_INPUT_HEIGHT_BLOCKS, + FONT_NORMAL_WHITE, + 0, + culture_goal_text, + SETTINGS_TEXT_LENGTH, + 0, + NULL, + culture_goal_on_change, + NULL, + INPUT_BOX_CHARS_NUMERIC +}; + +static input_box prosperity_goal_input = { + SETTINGS_INPUT_X, + SETTINGS_START_Y + SETTINGS_ROW_SPACING * 2, + SETTINGS_INPUT_WIDTH_BLOCKS, + SETTINGS_INPUT_HEIGHT_BLOCKS, + FONT_NORMAL_WHITE, + 0, + prosperity_goal_text, + SETTINGS_TEXT_LENGTH, + 0, + NULL, + prosperity_goal_on_change, + NULL, + INPUT_BOX_CHARS_NUMERIC +}; + +static input_box peace_goal_input = { + SETTINGS_INPUT_X, + SETTINGS_START_Y + SETTINGS_ROW_SPACING * 3, + SETTINGS_INPUT_WIDTH_BLOCKS, + SETTINGS_INPUT_HEIGHT_BLOCKS, + FONT_NORMAL_WHITE, + 0, + peace_goal_text, + SETTINGS_TEXT_LENGTH, + 0, + NULL, + peace_goal_on_change, + NULL, + INPUT_BOX_CHARS_NUMERIC +}; + +static input_box favor_goal_input = { + SETTINGS_INPUT_X, + SETTINGS_START_Y + SETTINGS_ROW_SPACING * 4, + SETTINGS_INPUT_WIDTH_BLOCKS, + SETTINGS_INPUT_HEIGHT_BLOCKS, + FONT_NORMAL_WHITE, + 0, + favor_goal_text, + SETTINGS_TEXT_LENGTH, + 0, + NULL, + favor_goal_on_change, + NULL, + INPUT_BOX_CHARS_NUMERIC +}; + +static input_box population_goal_input = { + SETTINGS_INPUT_X, + SETTINGS_START_Y + SETTINGS_ROW_SPACING * 5, + SETTINGS_INPUT_WIDTH_BLOCKS, + SETTINGS_INPUT_HEIGHT_BLOCKS, + FONT_NORMAL_WHITE, + 0, + population_goal_text, + SETTINGS_TEXT_LENGTH, + 0, + NULL, + population_goal_on_change, + NULL, + INPUT_BOX_CHARS_NUMERIC +}; + +static input_box *settings_inputs[] = { + &initial_funds_input, + &culture_goal_input, + &prosperity_goal_input, + &peace_goal_input, + &favor_goal_input, + &population_goal_input +}; + +static input_box *active_input = &seed_input; + +static struct { + unsigned int focus_button_id; + int size_index; + int algorithm_index; + uint8_t seed_text[SEED_TEXT_LENGTH]; + int settings_initialized; + scenario_climate climate_index; + int initial_funds; + scenario_win_criteria win_criteria; +} data; + +static minimap_functions preview_minimap_functions; + +static uint8_t preview_road_flags[GRID_SIZE * GRID_SIZE]; + +static void preview_viewport(int *x, int *y, int *width, int *height) +{ + *x = 0; + *y = 0; + *width = map_grid_width(); + *height = map_grid_height(); +} + +static void clear_map_data(void) +{ + map_image_clear(); + map_building_clear(); + map_terrain_clear(); + map_aqueduct_clear(); + map_figure_clear(); + map_property_clear(); + map_sprite_clear(); + map_random_clear(); + map_desirability_clear(); + map_elevation_clear(); + map_soldier_strength_clear(); + map_road_network_clear(); + + map_image_context_init(); + map_terrain_init_outside_map(); + map_random_init(); + map_property_init_alternate_terrain(); +} + +static int get_seed_value(unsigned int *seed_out) +{ + if (!string_length(data.seed_text)) { + return 0; + } + int seed = string_to_int(data.seed_text); + if (seed < 0) { + seed = -seed; + } + *seed_out = (unsigned int) seed; + return 1; +} + +static int clamp_int(int value, int min_value, int max_value) +{ + if (value < min_value) { + return min_value; + } + if (value > max_value) { + return max_value; + } + return value; +} + +static int parse_input_box_value(const input_box *box) +{ + if (!string_length(box->text)) { + return 0; + } + return string_to_int(box->text); +} + +static int sanitize_input_box_value(input_box *box, int min_value, int max_value) +{ + int value = clamp_int(parse_input_box_value(box), min_value, max_value); + set_input_box_value(box, value); + return value; +} + +static void set_input_box_value(input_box *box, int value) +{ + snprintf((char *) box->text, box->text_length, "%d", value); +} + +static void initial_funds_on_change(int is_addition_at_end) +{ + (void) is_addition_at_end; + sanitize_input_box_value(&initial_funds_input, FUNDS_MIN, FUNDS_MAX); +} + +static void culture_goal_on_change(int is_addition_at_end) +{ + (void) is_addition_at_end; + sanitize_input_box_value(&culture_goal_input, CRITERIA_PERCENT_MIN, CRITERIA_PERCENT_MAX); +} + +static void prosperity_goal_on_change(int is_addition_at_end) +{ + (void) is_addition_at_end; + sanitize_input_box_value(&prosperity_goal_input, CRITERIA_PERCENT_MIN, CRITERIA_PERCENT_MAX); +} + +static void peace_goal_on_change(int is_addition_at_end) +{ + (void) is_addition_at_end; + sanitize_input_box_value(&peace_goal_input, CRITERIA_PERCENT_MIN, CRITERIA_PERCENT_MAX); +} + +static void favor_goal_on_change(int is_addition_at_end) +{ + (void) is_addition_at_end; + sanitize_input_box_value(&favor_goal_input, CRITERIA_PERCENT_MIN, CRITERIA_PERCENT_MAX); +} + +static void population_goal_on_change(int is_addition_at_end) +{ + (void) is_addition_at_end; + sanitize_input_box_value(&population_goal_input, POPULATION_GOAL_MIN, POPULATION_GOAL_MAX); +} + +static void draw_static_input_box(const input_box *box) +{ + inner_panel_draw(box->x, box->y, box->width_blocks, box->height_blocks); + text_draw(box->text, box->x + 16, box->y + 10, box->font, 0); +} + +static int is_mouse_inside_box(const mouse *m, const input_box *box) +{ + return m->x >= box->x && m->x < box->x + box->width_blocks * BLOCK_SIZE + && m->y >= box->y && m->y < box->y + box->height_blocks * BLOCK_SIZE; +} + +static void focus_input_box(input_box *box) +{ + if (active_input == box) { + return; + } + + if (active_input) { + input_box_stop(active_input); + } + active_input = box; + input_box_start(active_input); +} + +static void sync_settings_to_inputs(void) +{ + set_input_box_value(&initial_funds_input, data.initial_funds); + set_input_box_value(&culture_goal_input, data.win_criteria.culture.goal); + set_input_box_value(&prosperity_goal_input, data.win_criteria.prosperity.goal); + set_input_box_value(&peace_goal_input, data.win_criteria.peace.goal); + set_input_box_value(&favor_goal_input, data.win_criteria.favor.goal); + set_input_box_value(&population_goal_input, data.win_criteria.population.goal); + + if (active_input) { + input_box_refresh_text(active_input); + } +} + +static void sync_inputs_to_settings(void) +{ + data.initial_funds = sanitize_input_box_value(&initial_funds_input, FUNDS_MIN, FUNDS_MAX); + + data.win_criteria.culture.goal = sanitize_input_box_value(&culture_goal_input, CRITERIA_PERCENT_MIN, CRITERIA_PERCENT_MAX); + data.win_criteria.prosperity.goal = sanitize_input_box_value(&prosperity_goal_input, CRITERIA_PERCENT_MIN, CRITERIA_PERCENT_MAX); + data.win_criteria.peace.goal = sanitize_input_box_value(&peace_goal_input, CRITERIA_PERCENT_MIN, CRITERIA_PERCENT_MAX); + data.win_criteria.favor.goal = sanitize_input_box_value(&favor_goal_input, CRITERIA_PERCENT_MIN, CRITERIA_PERCENT_MAX); + data.win_criteria.population.goal = sanitize_input_box_value(&population_goal_input, POPULATION_GOAL_MIN, POPULATION_GOAL_MAX); + + data.win_criteria.culture.enabled = data.win_criteria.culture.goal > 0; + data.win_criteria.prosperity.enabled = data.win_criteria.prosperity.goal > 0; + data.win_criteria.peace.enabled = data.win_criteria.peace.goal > 0; + data.win_criteria.favor.enabled = data.win_criteria.favor.goal > 0; + data.win_criteria.population.enabled = data.win_criteria.population.goal > 0; + + sync_settings_to_inputs(); +} + +static void generate_preview_map(void) +{ + scenario_editor_create(data.size_index); + scenario_map_init(); + if (data.settings_initialized) { + apply_scenario_settings(); + } else { + sync_settings_from_scenario(); + data.settings_initialized = 1; + apply_scenario_settings(); + } + clear_map_data(); + map_image_init_edges(); + + unsigned int seed = 0; + int use_seed = get_seed_value(&seed); + + terrain_generator_set_seed(use_seed, seed); + terrain_generator_generate((terrain_generator_algorithm) data.algorithm_index); + int width = map_grid_width(); + int height = map_grid_height(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int grid_offset = map_grid_offset(x, y); + preview_road_flags[grid_offset] = map_terrain_is(grid_offset, TERRAIN_ROAD) ? 1 : 0; + if (preview_road_flags[grid_offset]) { + map_terrain_remove(grid_offset, TERRAIN_ROAD); + } + } + } + + map_tiles_update_all(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int grid_offset = map_grid_offset(x, y); + if (preview_road_flags[grid_offset]) { + map_terrain_add(grid_offset, TERRAIN_ROAD); + } + } + } + map_tiles_update_all_roads(); + + preview_minimap_functions.climate = scenario_property_climate; + preview_minimap_functions.map.width = map_grid_width; + preview_minimap_functions.map.height = map_grid_height; + preview_minimap_functions.offset.figure = 0; + preview_minimap_functions.offset.terrain = map_terrain_get; + preview_minimap_functions.offset.building_id = 0; + preview_minimap_functions.offset.is_draw_tile = map_property_is_draw_tile; + preview_minimap_functions.offset.tile_size = map_property_multi_tile_size; + preview_minimap_functions.offset.random = map_random_get; + preview_minimap_functions.building = 0; + preview_minimap_functions.viewport = preview_viewport; + + city_view_set_custom_lookup(scenario.map.grid_start, scenario.map.width, + scenario.map.height, scenario.map.grid_border_size); + widget_minimap_update(&preview_minimap_functions); + city_view_restore_lookup(); +} + +static void draw_background(void) +{ + image_draw_fullscreen_background(image_group(GROUP_INTERMEZZO_BACKGROUND) + 25); + + graphics_set_clip_rectangle(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT); + graphics_in_dialog(); + outer_panel_draw(0, 0, 40, 30); + text_draw_centered(translation_for(TR_MAIN_MENU_TERRAIN_GENERATOR), 32, 14, 554, FONT_LARGE_BLACK, 0); + + text_draw(label_size, CONTROL_LABEL_X, SIZE_BUTTON_Y + 6, FONT_NORMAL_BLACK, 0); + text_draw(label_algorithm, CONTROL_LABEL_X, ALGORITHM_BUTTON_Y + 6, FONT_NORMAL_BLACK, 0); + text_draw(label_seed, CONTROL_LABEL_X, SEED_INPUT_Y + 6, FONT_NORMAL_BLACK, 0); + // text_draw(label_randomize, CONTROL_LABEL_X, RANDOMIZE_BUTTON_Y + 6, FONT_NORMAL_BLACK, 0); + text_draw(label_climate, CONTROL_LABEL_X, CLIMATE_BUTTON_Y + 6, FONT_NORMAL_BLACK, 0); + + text_draw(label_initial_funds, CONTROL_LABEL_X, SETTINGS_START_Y + 6, FONT_NORMAL_BLACK, 0); + text_draw(label_culture_goal, CONTROL_LABEL_X, SETTINGS_START_Y + SETTINGS_ROW_SPACING + 6, FONT_NORMAL_BLACK, 0); + text_draw(label_prosperity_goal, CONTROL_LABEL_X, SETTINGS_START_Y + SETTINGS_ROW_SPACING * 2 + 6, FONT_NORMAL_BLACK, 0); + text_draw(label_peace_goal, CONTROL_LABEL_X, SETTINGS_START_Y + SETTINGS_ROW_SPACING * 3 + 6, FONT_NORMAL_BLACK, 0); + text_draw(label_favor_goal, CONTROL_LABEL_X, SETTINGS_START_Y + SETTINGS_ROW_SPACING * 4 + 6, FONT_NORMAL_BLACK, 0); + text_draw(label_population_goal, CONTROL_LABEL_X, SETTINGS_START_Y + SETTINGS_ROW_SPACING * 5 + 6, FONT_NORMAL_BLACK, 0); + + inner_panel_draw(PREVIEW_X - 8, PREVIEW_Y - 8, (PREVIEW_WIDTH + 16) / BLOCK_SIZE, + (PREVIEW_HEIGHT + 16) / BLOCK_SIZE); + widget_minimap_draw(PREVIEW_X, PREVIEW_Y, PREVIEW_WIDTH, PREVIEW_HEIGHT); + + graphics_reset_clip_rectangle(); + graphics_reset_dialog(); +} + +static void draw_foreground(void) +{ + graphics_in_dialog(); + + for (unsigned int i = 0; i < sizeof(buttons) / sizeof(buttons[0]); i++) { + button_border_draw(buttons[i].x, buttons[i].y, buttons[i].width, buttons[i].height, + data.focus_button_id == i + 1); + } + + text_draw_centered(terrain_generator_size_labels[data.size_index], + CONTROL_VALUE_X, SIZE_BUTTON_Y + 6, CONTROL_BUTTON_WIDTH, FONT_NORMAL_BLACK, 0); + text_draw_centered(terrain_generator_algorithm_labels[data.algorithm_index], + CONTROL_VALUE_X, ALGORITHM_BUTTON_Y + 6, CONTROL_BUTTON_WIDTH, FONT_NORMAL_BLACK, 0); + text_draw_centered(label_randomize, + CONTROL_VALUE_X, RANDOMIZE_BUTTON_Y + 6, CONTROL_BUTTON_WIDTH, FONT_NORMAL_BLACK, 0); + text_draw_centered(climate_labels[data.climate_index], + CONTROL_VALUE_X, CLIMATE_BUTTON_Y + 6, CONTROL_BUTTON_WIDTH, FONT_NORMAL_BLACK, 0); + + + text_draw_centered(label_regenerate, ACTION_BUTTON_X, REGENERATE_BUTTON_Y + 8, + ACTION_BUTTON_WIDTH, FONT_NORMAL_BLACK, 0); + text_draw_centered(label_open_editor, ACTION_BUTTON_X, OPEN_EDITOR_BUTTON_Y + 8, + ACTION_BUTTON_WIDTH, FONT_NORMAL_BLACK, 0); + text_draw_centered(label_start_game, ACTION_BUTTON_X, START_GAME_BUTTON_Y + 8, + ACTION_BUTTON_WIDTH, FONT_NORMAL_BLACK, 0); + text_draw_centered(label_back, ACTION_BUTTON_X, BACK_BUTTON_Y + 8, + ACTION_BUTTON_WIDTH, FONT_NORMAL_BLACK, 0); + + if (active_input == &seed_input) { + input_box_draw(&seed_input); + } else { + draw_static_input_box(&seed_input); + } + for (unsigned int i = 0; i < sizeof(settings_inputs) / sizeof(settings_inputs[0]); i++) { + if (settings_inputs[i] == active_input) { + input_box_draw(settings_inputs[i]); + } else { + draw_static_input_box(settings_inputs[i]); + } + } + + graphics_reset_dialog(); +} + +static void handle_input(const mouse *m, const hotkeys *h) +{ + const mouse *m_dialog = mouse_in_dialog(m); + data.focus_button_id = 0; + + if (input_box_is_accepted()) { + sync_inputs_to_settings(); + generate_preview_map(); + window_invalidate(); + return; + } + + if (active_input && input_box_handle_mouse(m_dialog, active_input)) { + return; + } + + if (m_dialog->left.went_up) { + if (is_mouse_inside_box(m_dialog, &seed_input)) { + focus_input_box(&seed_input); + return; + } + for (unsigned int i = 0; i < sizeof(settings_inputs) / sizeof(settings_inputs[0]); i++) { + if (is_mouse_inside_box(m_dialog, settings_inputs[i])) { + focus_input_box(settings_inputs[i]); + return; + } + } + } + + if (generic_buttons_handle_mouse(m_dialog, 0, 0, buttons, sizeof(buttons) / sizeof(buttons[0]), + &data.focus_button_id)) { + return; + } + + if (input_go_back_requested(m, h)) { + button_back(NULL); + } +} + +static void button_select_size(const generic_button *button) +{ + window_select_list_show_text(0, 0, button, terrain_generator_size_labels, + TERRAIN_GENERATOR_SIZE_COUNT, size_selected); +} + +static void button_select_algorithm(const generic_button *button) +{ + window_select_list_show_text(0, 0, button, terrain_generator_algorithm_labels, + TERRAIN_GENERATOR_COUNT, algorithm_selected); +} + +static void button_select_climate(const generic_button *button) +{ + window_select_list_show_text(0, 0, button, climate_labels, CLIMATE_COUNT, climate_selected); +} + +static void button_randomize(const generic_button *button) +{ + (void) button; + unsigned int random_seed = generate_seed_value(); + sprintf(data.seed_text, "%d", random_seed); + seed_input.placeholder = data.seed_text; + randomize_scenario_settings(); + // input_box_set_text(&seed_input, data.seed_text); + generate_preview_map(); + window_invalidate(); +} + +static void button_regenerate(const generic_button *button) +{ + (void) button; + sync_inputs_to_settings(); + generate_preview_map(); + window_invalidate(); +} + +static void button_open_editor(const generic_button *button) +{ + (void) button; + unsigned int seed = 0; + int use_seed = get_seed_value(&seed); + terrain_generator_set_seed(use_seed, seed); + + apply_scenario_settings(); + + if (!editor_is_present() || + !game_init_editor_generated(data.size_index, data.algorithm_index)) { + window_plain_message_dialog_show(TR_NO_EDITOR_TITLE, TR_NO_EDITOR_MESSAGE, 1); + return; + } + + + if (active_input) { + input_box_stop(active_input); + } + if (config_get(CONFIG_UI_SHOW_INTRO_VIDEO)) { + window_video_show("map_intro.smk", window_editor_map_show); + } + sound_music_play_editor(); +} + +static void button_start_game(const generic_button *button) +{ + (void) button; + const uint8_t scenario_name_text[] = "generated_terrain"; + const char scenario_filename[] = "generated_terrain.map"; + + const char *scenario_path = dir_append_location(scenario_filename, PATH_LOCATION_SCENARIO); + + if (active_input) { + input_box_stop(active_input); + } + + if (!scenario_path) { + window_plain_message_dialog_show(TR_SAVEGAME_NOT_ABLE_TO_SAVE_TITLE, + TR_SAVEGAME_NOT_ABLE_TO_SAVE_MESSAGE, 1); + return; + } + + unsigned int seed = 0; + int use_seed = get_seed_value(&seed); + terrain_generator_set_seed(use_seed, seed); + + scenario_editor_create(data.size_index); + scenario_map_init(); + clear_map_data(); + map_image_init_edges(); + apply_scenario_settings(); + terrain_generator_generate((terrain_generator_algorithm) data.algorithm_index); + scenario_set_name(scenario_name_text); + scenario_set_custom(2); + + if (!game_file_editor_write_scenario(scenario_path)) { + window_plain_message_dialog_show(TR_SAVEGAME_NOT_ABLE_TO_SAVE_TITLE, + TR_SAVEGAME_NOT_ABLE_TO_SAVE_MESSAGE, 1); + return; + } + + window_city_show(); + if (!game_file_start_scenario_by_name(scenario_name())) { + window_plain_message_dialog_show_with_extra(TR_REPLAY_MAP_NOT_FOUND_TITLE, + TR_REPLAY_MAP_NOT_FOUND_MESSAGE, 0, scenario_name()); + } +} + +static void button_back(const generic_button *button) +{ + (void) button; + if (active_input) { + input_box_stop(active_input); + } + window_go_back(); +} + +static void size_selected(int id) +{ + data.size_index = id; + generate_preview_map(); + window_invalidate(); +} + +static void algorithm_selected(int id) +{ + data.algorithm_index = id; + generate_preview_map(); + window_invalidate(); +} + +static void climate_selected(int id) +{ + data.climate_index = id; + generate_preview_map(); + window_invalidate(); +} + +static void sync_settings_from_scenario(void) +{ + data.climate_index = scenario_property_climate(); + data.initial_funds = scenario.initial_funds; + data.win_criteria = scenario.win_criteria; + sync_settings_to_inputs(); +} + +static void apply_scenario_settings(void) +{ + sync_inputs_to_settings(); + scenario_change_climate(data.climate_index); + scenario.initial_funds = data.initial_funds; + scenario.win_criteria = data.win_criteria; +} + +static void randomize_scenario_settings(void) +{ + data.climate_index = (scenario_climate) (generate_seed_value() % CLIMATE_COUNT); + + data.initial_funds = 500 + (int) (generate_seed_value() % 60) * 250; + + data.win_criteria.culture.enabled = (int) (generate_seed_value() % 2); + data.win_criteria.prosperity.enabled = (int) (generate_seed_value() % 2); + data.win_criteria.peace.enabled = (int) (generate_seed_value() % 2); + data.win_criteria.favor.enabled = (int) (generate_seed_value() % 2); + data.win_criteria.population.enabled = (int) (generate_seed_value() % 3 == 0); + + data.win_criteria.culture.goal = 10 + (int) (generate_seed_value() % 91); + data.win_criteria.prosperity.goal = 10 + (int) (generate_seed_value() % 91); + data.win_criteria.peace.goal = 10 + (int) (generate_seed_value() % 91); + data.win_criteria.favor.goal = 10 + (int) (generate_seed_value() % 91); + data.win_criteria.population.goal = 500 + (int) (generate_seed_value() % 56) * 250; + + if (!data.win_criteria.culture.enabled && !data.win_criteria.prosperity.enabled + && !data.win_criteria.peace.enabled && !data.win_criteria.favor.enabled + && !data.win_criteria.population.enabled) { + data.win_criteria.culture.enabled = 1; + } + sync_settings_to_inputs(); +} + +static void init(void) +{ + data.focus_button_id = 0; + data.size_index = 2; + data.algorithm_index = 0; + data.seed_text[0] = 0; + data.settings_initialized = 0; + data.climate_index = CLIMATE_CENTRAL; + data.initial_funds = 1000; + memset(&data.win_criteria, 0, sizeof(data.win_criteria)); + + seed_input.text = data.seed_text; + seed_input.allowed_chars = INPUT_BOX_CHARS_NUMERIC; + + unsigned int random_seed = generate_seed_value(); + sprintf(data.seed_text, "%d", random_seed); + seed_input.placeholder = data.seed_text; + + terrain_generator_algorithm_labels[1] = "Random"; + terrain_generator_algorithm_labels[0] = "River"; + + active_input = &seed_input; + input_box_start(active_input); + generate_preview_map(); +} + +void window_terrain_generator_show(void) +{ + window_type window = { + WINDOW_TERRAIN_GENERATOR, + draw_background, + draw_foreground, + handle_input + }; + init(); + window_show(&window); +} diff --git a/src/window/terrain_generator.h b/src/window/terrain_generator.h new file mode 100644 index 0000000000..cbc91c5297 --- /dev/null +++ b/src/window/terrain_generator.h @@ -0,0 +1,6 @@ +#ifndef WINDOW_TERRAIN_GENERATOR_H +#define WINDOW_TERRAIN_GENERATOR_H + +void window_terrain_generator_show(void); + +#endif // WINDOW_TERRAIN_GENERATOR_H