diff --git a/res/assets/Graphics/terrain_maps.xml b/res/assets/Graphics/terrain_maps.xml index 58a881f430..daaf6e228d 100644 --- a/res/assets/Graphics/terrain_maps.xml +++ b/res/assets/Graphics/terrain_maps.xml @@ -117,7 +117,10 @@ - + @@ -126,3970 +129,76 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/building/construction_clear.c b/src/building/construction_clear.c index be01836c7a..77c4e942e5 100644 --- a/src/building/construction_clear.c +++ b/src/building/construction_clear.c @@ -85,6 +85,8 @@ unsigned int building_construction_clear_select(int x_start, int y_start, int x_ continue; } else if (map_terrain_is(grid_offset, TERRAIN_WATER)) { // keep the "bridge is free" bug from C3 continue; + } else if (map_terrain_is(grid_offset, TERRAIN_MARSHLAND)) { + continue; } else if (map_terrain_is(grid_offset, TERRAIN_AQUEDUCT)) { items_placed++; } else if (map_terrain_is(grid_offset, TERRAIN_HIGHWAY)) { @@ -125,6 +127,9 @@ static unsigned int clear_land_confirmed(int x_start, int y_start, int x_end, in if (map_terrain_is(grid_offset, TERRAIN_ROCK | TERRAIN_ELEVATION | TERRAIN_ACCESS_RAMP)) { continue; } + if (map_terrain_is(grid_offset, TERRAIN_MARSHLAND)) { + continue; + } if (map_terrain_is(grid_offset, TERRAIN_BUILDING) && is_shared_building_at(grid_offset)) { diff --git a/src/city/health.c b/src/city/health.c index a4940e9c79..5189cddef9 100644 --- a/src/city/health.c +++ b/src/city/health.c @@ -14,9 +14,13 @@ #include "core/calc.h" #include "core/random.h" #include "game/tutorial.h" +#include "map/grid.h" +#include "map/terrain.h" #include "scenario/property.h" #define SICKNESS_SPREAD_DIVISION_FACTOR 4 +#define MARSHLAND_HEALTH_RANGE 3 +#define MARSHLAND_SICKNESS_MALUS 5 #define NUM_PLAGUE_BUILDINGS (sizeof(PLAGUE_BUILDINGS) / sizeof(building_type)) static const building_type PLAGUE_BUILDINGS[] = { BUILDING_DOCK, BUILDING_WAREHOUSE, BUILDING_GRANARY }; @@ -363,6 +367,21 @@ int city_health_get_house_health_level(const building *b, int update_city_data) return house_health; } +static void apply_marshland_sickness_malus(building *b) +{ + int x_min, y_min, x_max, y_max; + map_grid_get_area(b->x, b->y, b->size, MARSHLAND_HEALTH_RANGE, &x_min, &y_min, &x_max, &y_max); + for (int yy = y_min; yy <= y_max; yy++) { + for (int xx = x_min; xx <= x_max; xx++) { + if (map_terrain_is(map_grid_offset(xx, yy), TERRAIN_MARSHLAND)) { + b->sickness_level = calc_bound( + b->sickness_level + MARSHLAND_SICKNESS_MALUS, 0, MAX_SICKNESS_LEVEL); + return; // one malus per cycle regardless of how many marsh tiles are near + } + } + } +} + void city_health_update(void) { int only_gather_stats = 0; @@ -397,6 +416,7 @@ void city_health_update(void) if (!only_gather_stats) { healthy_population += calc_adjust_with_percentage(b->house_population, house_health); adjust_sickness_level_in_house(b, house_health, population_health_offset, hospital_coverage_bonus); + apply_marshland_sickness_malus(b); } } } diff --git a/src/core/lang.c b/src/core/lang.c index dc85a3c70d..c3bdb854ad 100644 --- a/src/core/lang.c +++ b/src/core/lang.c @@ -647,6 +647,10 @@ const uint8_t *lang_get_string(int group, int index) return translation_for(TR_EDITOR_TOOL_EARTHQUAKE_REMOVE); case TR_EDITOR_RUBBLE: return translation_for(TR_EDITOR_RUBBLE); + case TR_EDITOR_TOOL_WATER: + return translation_for(TR_EDITOR_TOOL_WATER); + case TR_EDITOR_TOOL_MARSHLAND: + return translation_for(TR_EDITOR_TOOL_MARSHLAND); default: break; } diff --git a/src/editor/tool.c b/src/editor/tool.c index c87c397924..d5f3978bab 100644 --- a/src/editor/tool.c +++ b/src/editor/tool.c @@ -29,7 +29,7 @@ #include #define TERRAIN_PAINT_MASK ~(TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_WATER | TERRAIN_BUILDING |\ - TERRAIN_SHRUB | TERRAIN_GARDEN | TERRAIN_ROAD | TERRAIN_MEADOW) + TERRAIN_SHRUB | TERRAIN_GARDEN | TERRAIN_ROAD | TERRAIN_MEADOW | TERRAIN_MARSHLAND) static struct { int active; @@ -151,6 +151,7 @@ int editor_tool_is_brush(void) case TOOL_SHRUB: case TOOL_ROCKS: case TOOL_MEADOW: + case TOOL_MARSHLAND: case TOOL_NATIVE_RUINS: case TOOL_RAISE_LAND: case TOOL_LOWER_LAND: @@ -169,7 +170,7 @@ static int raise_land_tile(int x, int y, int grid_offset, int terrain) if (!(terrain & (TERRAIN_ACCESS_RAMP | TERRAIN_ELEVATION))) { map_property_set_multi_tile_size(grid_offset, 1); map_elevation_set(grid_offset, elevation + 1); - terrain &= ~(TERRAIN_WATER | TERRAIN_BUILDING | TERRAIN_GARDEN | TERRAIN_ROAD); + terrain &= ~(TERRAIN_WATER | TERRAIN_BUILDING | TERRAIN_GARDEN | TERRAIN_ROAD | TERRAIN_MARSHLAND); } } return terrain; @@ -239,7 +240,8 @@ static void add_terrain(const void *tile_data, int dx, int dy) } break; case TOOL_WATER: - if (!(terrain & TERRAIN_WATER) && !(terrain & TERRAIN_ELEVATION_ROCK)) { + if ((!(terrain & TERRAIN_WATER) || (terrain & TERRAIN_MARSHLAND)) && + !(terrain & TERRAIN_ELEVATION_ROCK)) { terrain &= TERRAIN_PAINT_MASK; terrain |= TERRAIN_WATER; map_property_clear_future_earthquake(grid_offset); @@ -257,6 +259,15 @@ static void add_terrain(const void *tile_data, int dx, int dy) terrain |= TERRAIN_MEADOW; } break; + case TOOL_MARSHLAND: + // Marsh is an OVERLAY: it adds the TERRAIN_MARSHLAND flag and is drawn on top of the + // native terrain tile (land / water / coast), which keeps its own image and autotiling. + if (!(terrain & TERRAIN_MARSHLAND) && !(terrain & TERRAIN_ELEVATION_ROCK)) { + terrain &= ~(TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_SHRUB | TERRAIN_GARDEN); + terrain |= TERRAIN_MARSHLAND; + map_property_clear_future_earthquake(grid_offset); + } + break; case TOOL_NATIVE_RUINS: if (!(terrain & TERRAIN_RUBBLE)) { terrain &= TERRAIN_PAINT_MASK; @@ -310,30 +321,42 @@ void editor_tool_update_use(const map_tile *tile) map_tiles_update_all_rocks(); map_tiles_update_region_empty_land(x_min, y_min, x_max, y_max); map_tiles_update_region_meadow(x_min, y_min, x_max, y_max); + map_tiles_update_region_marshland(x_min - 1, y_min - 1, x_max + 1, y_max + 1); break; case TOOL_TREES: map_image_context_reset_water(); map_tiles_update_region_water(x_min, y_min, x_max, y_max); map_tiles_update_all_rocks(); map_tiles_update_region_trees(x_min, y_min, x_max, y_max); + map_tiles_update_region_marshland(x_min - 1, y_min - 1, x_max + 1, y_max + 1); break; case TOOL_WATER: case TOOL_ROCKS: map_image_context_reset_water(); map_tiles_update_all_rocks(); map_tiles_update_region_water(x_min, y_min, x_max, y_max); + map_tiles_update_region_marshland(x_min - 1, y_min - 1, x_max + 1, y_max + 1); break; case TOOL_SHRUB: map_image_context_reset_water(); map_tiles_update_region_water(x_min, y_min, x_max, y_max); map_tiles_update_all_rocks(); map_tiles_update_region_shrub(x_min, y_min, x_max, y_max); + map_tiles_update_region_marshland(x_min - 1, y_min - 1, x_max + 1, y_max + 1); break; case TOOL_MEADOW: map_image_context_reset_water(); map_tiles_update_region_water(x_min, y_min, x_max, y_max); map_tiles_update_all_rocks(); map_tiles_update_region_meadow(x_min, y_min, x_max, y_max); + map_tiles_update_region_marshland(x_min - 1, y_min - 1, x_max + 1, y_max + 1); + break; + case TOOL_MARSHLAND: + map_image_context_reset_water(); + map_tiles_update_region_water(x_min - 1, y_min - 1, x_max + 1, y_max + 1); + map_tiles_update_all_rocks(); + map_tiles_update_region_empty_land(x_min, y_min, x_max, y_max); + map_tiles_update_region_marshland(x_min - 1, y_min - 1, x_max + 1, y_max + 1); break; case TOOL_NATIVE_RUINS: // Rubble doesn't need terrain updates, just refresh the rubble graphics @@ -350,6 +373,7 @@ void editor_tool_update_use(const map_tile *tile) map_tiles_update_all_rocks(); map_tiles_update_region_empty_land(x_min, y_min, x_max, y_max); map_tiles_update_region_meadow(x_min, y_min, x_max, y_max); + map_tiles_update_region_marshland(x_min - 1, y_min - 1, x_max + 1, y_max + 1); break; case TOOL_EARTHQUAKE_CUSTOM: { @@ -479,6 +503,7 @@ static void update_terrain_after_elevation_changes(void) map_tiles_update_all_empty_land(); map_tiles_update_all_meadow(); map_tiles_update_all_water(); + map_tiles_update_all_marshland(); scenario_editor_set_as_unsaved(); } diff --git a/src/editor/tool.h b/src/editor/tool.h index 11dea63863..2e3c9cf878 100644 --- a/src/editor/tool.h +++ b/src/editor/tool.h @@ -33,7 +33,8 @@ typedef enum { TOOL_EARTHQUAKE_CUSTOM = 30, TOOL_EARTHQUAKE_CUSTOM_REMOVE = 31, TOOL_NATIVE_RUINS = 32, - TOOL_SELECT_LAND = 33 + TOOL_SELECT_LAND = 33, + TOOL_MARSHLAND = 34 } tool_type; tool_type editor_tool_type(void); diff --git a/src/figuretype/animal.c b/src/figuretype/animal.c index 904c628bab..6912e11bbb 100644 --- a/src/figuretype/animal.c +++ b/src/figuretype/animal.c @@ -271,7 +271,7 @@ void figure_wolf_action(figure *f) static int terrain_blocked_for_animals(int grid_offset) { return map_terrain_is(grid_offset, TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_WATER | - TERRAIN_BUILDING | TERRAIN_SHRUB ); + TERRAIN_BUILDING | TERRAIN_SHRUB | TERRAIN_MARSHLAND); } void figure_animal_try_nudge_at(int building_center_tile_grid_offset, int animal_tile_offset, int building_size) diff --git a/src/game/file.c b/src/game/file.c index 69d1ab7159..a6de76e8b0 100644 --- a/src/game/file.c +++ b/src/game/file.c @@ -125,6 +125,7 @@ static void clear_scenario_data(void) // clear grids map_image_clear(); + map_marsh_image_clear(); map_building_clear(); map_terrain_clear(); map_aqueduct_clear(); @@ -149,6 +150,7 @@ static void initialize_scenario_data(const uint8_t *scenario_name) // initialize grids map_tiles_update_all_elevation(); map_tiles_update_all_water(); + map_tiles_update_all_marshland(); map_tiles_update_all_earthquake(); map_tiles_update_all_rocks(); map_tiles_add_entry_exit_flags(); @@ -273,6 +275,7 @@ static void initialize_saved_game(void) map_image_context_init(); map_image_clear(); + map_marsh_image_clear(); map_image_update_all(); scenario_map_init(); diff --git a/src/game/file_editor.c b/src/game/file_editor.c index b19a614770..18f5393b46 100644 --- a/src/game/file_editor.c +++ b/src/game/file_editor.c @@ -84,6 +84,7 @@ void game_file_editor_clear_data(void) static void clear_map_data(void) { map_image_clear(); + map_marsh_image_clear(); map_building_clear(); map_terrain_clear(); map_aqueduct_clear(); @@ -129,6 +130,7 @@ static void prepare_map_for_editing(void) map_tiles_update_all_elevation_editor(); map_tiles_update_all_water(); + map_tiles_update_all_marshland(); map_tiles_update_all_earthquake(); map_tiles_update_all_rocks(); map_tiles_update_all_empty_land(); diff --git a/src/game/file_io.c b/src/game/file_io.c index 80a34cb6cb..1c3b665d10 100644 --- a/src/game/file_io.c +++ b/src/game/file_io.c @@ -367,7 +367,11 @@ static void init_scenario_data(scenario_version_t version) } state->graphic_ids = create_scenario_piece(GRID_SIZE_BUF_U16, 0); state->edge = create_scenario_piece(GRID_SIZE_BUF_U8, 0); - state->terrain = create_scenario_piece(GRID_SIZE_BUF_U16, 0); + if (version > SCENARIO_LAST_U16_TERRAIN) { + state->terrain = create_scenario_piece(GRID_SIZE_BUF_U32, 0); + } else { + state->terrain = create_scenario_piece(GRID_SIZE_BUF_U16, 0); + } if (version > SCENARIO_LAST_NO_FORMULAS_AND_MODEL_DATA) { state->bitfields = create_scenario_piece(GRID_SIZE_BUF_U16, 0); } else { @@ -724,7 +728,7 @@ static void scenario_load_from_state(scenario_state *file, scenario_version_t ve resource_set_mapping(resource_version); map_image_load_state_legacy(file->graphic_ids); - map_terrain_load_state(file->terrain, 0, file->graphic_ids, 1); + map_terrain_load_state(file->terrain, version > SCENARIO_LAST_U16_TERRAIN, file->graphic_ids, 1); if (version > SCENARIO_LAST_NO_FORMULAS_AND_MODEL_DATA) { map_property_load_state(file->bitfields, file->edge); } else { @@ -801,7 +805,7 @@ static void scenario_save_to_state(scenario_state *file) buffer_write_u32(file->resource_version, RESOURCE_CURRENT_VERSION); map_image_save_state_legacy(file->graphic_ids); - map_terrain_save_state_legacy(file->terrain); + map_terrain_save_state(file->terrain); map_property_save_state(file->bitfields, file->edge); map_random_save_state(file->random); map_elevation_save_state(file->elevation); @@ -1341,7 +1345,11 @@ int game_file_io_read_scenario(const char *filename) static int scenario_terrain_at(int grid_offset) { - return map_terrain_get_from_buffer_16(scenario_data.state.terrain, grid_offset); + if (scenario_data.version <= SCENARIO_LAST_U16_TERRAIN) { + return map_terrain_get_from_buffer_16(scenario_data.state.terrain, grid_offset); + } else { + return map_terrain_get_from_buffer_32(scenario_data.state.terrain, grid_offset); + } } static int scenario_tile_size_at(int grid_offset) diff --git a/src/game/save_version.h b/src/game/save_version.h index 90eafdaac6..41442a0de6 100644 --- a/src/game/save_version.h +++ b/src/game/save_version.h @@ -109,7 +109,7 @@ typedef enum { } savegame_version_t; typedef enum { - SCENARIO_CURRENT_VERSION = 22, + SCENARIO_CURRENT_VERSION = 23, SCENARIO_VERSION_NONE = 0, SCENARIO_LAST_UNVERSIONED = 1, @@ -132,7 +132,8 @@ typedef enum { SCENARIO_LAST_NO_VISIBLE_CUSTOM_VARIABLES = 18, SCENARIO_LAST_NO_FORMULAS_AND_MODEL_DATA = 19, SCENARIO_LAST_NO_EMPIRE_EDITOR = 20, - SCENARIO_LAST_LIMITED_ROUTE_COST = 21 + SCENARIO_LAST_LIMITED_ROUTE_COST = 21, + SCENARIO_LAST_U16_TERRAIN = 22 } scenario_version_t; typedef enum { diff --git a/src/game/tick.c b/src/game/tick.c index 740cc17c51..756982a81e 100644 --- a/src/game/tick.c +++ b/src/game/tick.c @@ -100,6 +100,7 @@ static void advance_month(void) map_tiles_update_all_roads(); map_tiles_update_all_highways(); map_tiles_update_all_water(); + map_tiles_update_all_marshland(); map_routing_update_land_citizen(); city_message_sort_and_compact(); diff --git a/src/map/bridge.c b/src/map/bridge.c index 29a3671d6f..f67a18cc4c 100644 --- a/src/map/bridge.c +++ b/src/map/bridge.c @@ -84,13 +84,13 @@ int map_bridge_calculate_length_direction(int x, int y, int *length, int *direct if (i == 0) { //check for an inaccessible tile before the bridge starts int previous_offset = grid_offset - 2 * bridge.direction_grid_delta; - if (map_terrain_is(previous_offset, TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_SHRUB | TERRAIN_BUILDING)) { + if (map_terrain_is(previous_offset, TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_SHRUB | TERRAIN_BUILDING | TERRAIN_MARSHLAND)) { blocking_tiles->grid_offsets[blocking_tiles->size++] = previous_offset; bridge.end_grid_offset = 0; } } int next_offset = grid_offset + bridge.direction_grid_delta; - if (map_terrain_is(next_offset, TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_SHRUB | TERRAIN_BUILDING)) { + if (map_terrain_is(next_offset, TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_SHRUB | TERRAIN_BUILDING | TERRAIN_MARSHLAND)) { blocking_tiles->grid_offsets[blocking_tiles->size++] = next_offset; bridge.end_grid_offset = 0; break; diff --git a/src/map/desirability.c b/src/map/desirability.c index 0996503648..399dd45fcb 100644 --- a/src/map/desirability.c +++ b/src/map/desirability.c @@ -187,6 +187,8 @@ static void update_terrain(void) model->desirability_step, model->desirability_step_size, model->desirability_range); + } else if (terrain & TERRAIN_MARSHLAND) { + add_to_terrain(x, y, 1, -3, 1, 1, 3); } else if (terrain & TERRAIN_AQUEDUCT) { add_to_terrain(x, y, 1, -2, 1, 1, 2); } diff --git a/src/map/image.c b/src/map/image.c index 68cd66ddb1..1d5db6e888 100644 --- a/src/map/image.c +++ b/src/map/image.c @@ -13,6 +13,38 @@ static grid_u32 images; static grid_u32 images_backup; +// Marshland is drawn as an OVERLAY on top of the native terrain tile (which keeps its own image), +// so its image id is stored in a separate grid rather than overwriting the terrain image. 0 = no +// marsh overlay on the tile. This grid is derived from the TERRAIN_MARSHLAND flag and recomputed +// on load/rotation/climate change, so it is not part of the save format. +static grid_u32 marsh_images; +static grid_u32 marsh_images_backup; + +unsigned int map_marsh_image_at(int grid_offset) +{ + return marsh_images.items[grid_offset]; +} + +void map_marsh_image_set(int grid_offset, int image_id) +{ + marsh_images.items[grid_offset] = image_id; +} + +void map_marsh_image_backup(void) +{ + map_grid_copy_u32(marsh_images.items, marsh_images_backup.items); +} + +void map_marsh_image_restore(void) +{ + map_grid_copy_u32(marsh_images_backup.items, marsh_images.items); +} + +void map_marsh_image_clear(void) +{ + map_grid_clear_u32(marsh_images.items); +} + unsigned int map_image_at(int grid_offset) { return images.items[grid_offset]; diff --git a/src/map/image.h b/src/map/image.h index df212e0ab4..56ca10d3fc 100644 --- a/src/map/image.h +++ b/src/map/image.h @@ -23,6 +23,12 @@ void map_image_clear(void); void map_image_init_edges(void); void map_image_update_all(void); +unsigned int map_marsh_image_at(int grid_offset); +void map_marsh_image_set(int grid_offset, int image_id); +void map_marsh_image_backup(void); +void map_marsh_image_restore(void); +void map_marsh_image_clear(void); + void map_image_save_state_legacy(buffer *buf); void map_image_load_state_legacy(buffer *buf); diff --git a/src/map/image_context.c b/src/map/image_context.c index 41bd5f4f89..3f203bf6e3 100644 --- a/src/map/image_context.c +++ b/src/map/image_context.c @@ -342,6 +342,7 @@ static const terrain_image *get_image(int group, int tiles[MAX_TILES]) result.group_offset = context[i].offset_for_orientation[city_view_orientation() / 2]; result.item_offset = context[i].current_item_offset; result.aqueduct_offset = context[i].aqueduct_offset; + result.max_item_offset = context[i].max_item_offset; break; } } diff --git a/src/map/image_context.h b/src/map/image_context.h index 413607f3e3..b2bc37cb05 100644 --- a/src/map/image_context.h +++ b/src/map/image_context.h @@ -6,6 +6,7 @@ typedef struct { int group_offset; int item_offset; int aqueduct_offset; + int max_item_offset; } terrain_image; void map_image_context_init(void); diff --git a/src/map/orientation.c b/src/map/orientation.c index ceb754c986..8ac8861396 100644 --- a/src/map/orientation.c +++ b/src/map/orientation.c @@ -56,6 +56,7 @@ void map_orientation_change(int counter_clockwise) map_tiles_update_all_elevation(); map_tiles_update_all_water(); + map_tiles_update_all_marshland(); map_tiles_update_all_earthquake(); map_tiles_update_all_rocks(); map_tiles_update_all_gardens(); diff --git a/src/map/routing_terrain.c b/src/map/routing_terrain.c index 0b98d9ed36..f50b85ca2e 100644 --- a/src/map/routing_terrain.c +++ b/src/map/routing_terrain.c @@ -249,7 +249,8 @@ void map_routing_update_water(void) int grid_offset = map_data.start_offset; for (int y = 0; y < map_data.height; y++, grid_offset += map_data.border_size) { for (int x = 0; x < map_data.width; x++, grid_offset++) { - if (map_terrain_is(grid_offset, TERRAIN_WATER) && is_surrounded_by_water(grid_offset)) { + if (map_terrain_is(grid_offset, TERRAIN_WATER) && !map_terrain_is(grid_offset, TERRAIN_MARSHLAND) && + is_surrounded_by_water(grid_offset)) { if (x > 0 && x < map_data.width - 1 && y > 0 && y < map_data.height - 1) { switch (map_sprite_bridge_at(grid_offset)) { diff --git a/src/map/terrain.c b/src/map/terrain.c index 4d6ed8075d..1807c8a153 100644 --- a/src/map/terrain.c +++ b/src/map/terrain.c @@ -22,7 +22,7 @@ const terrain_flags_array *map_terrain_to_array(int grid_offset) static const char *names[TERRAIN_NUM_FLAGS] = { "TREE", "ROCK", "WATER", "BUILDING", "SHRUB", "GARDEN", "ROAD", "RESERVOIR_R", "AQUEDUCT", "ELEVATION", "ACCESS_RAMP", "MEADOW", "RUBBLE", "FOUNTAIN_R", "WALL", "GATEHOUSE", "ORG_TREE", "HIGHWAY1", "HIGHWAY2", - "HIGHWAY3", "HIGHWAY4" + "HIGHWAY3", "HIGHWAY4", "MARSHLAND" }; static terrain_flags_array result; unsigned int terrain_value = terrain_grid.items[grid_offset]; diff --git a/src/map/terrain.h b/src/map/terrain.h index cdac9b3ace..58e011001b 100644 --- a/src/map/terrain.h +++ b/src/map/terrain.h @@ -4,8 +4,8 @@ #include #include "core/buffer.h" -#define TERRAIN_LAST_FLAG TERRAIN_HIGHWAY_BOTTOM_RIGHT -#define TERRAIN_NUM_FLAGS (21) // bits +#define TERRAIN_LAST_FLAG TERRAIN_MARSHLAND +#define TERRAIN_NUM_FLAGS (22) // bits #define KEY_MAX_LEN 32 // max length of a single key - only debugging purposes enum { @@ -31,6 +31,7 @@ enum { TERRAIN_HIGHWAY_BOTTOM_LEFT = 1 << 18, TERRAIN_HIGHWAY_TOP_RIGHT = 1 << 19, TERRAIN_HIGHWAY_BOTTOM_RIGHT = 1 << 20, + TERRAIN_MARSHLAND = 1 << 21, // Combined TERRAIN_HIGHWAY = TERRAIN_HIGHWAY_TOP_LEFT | TERRAIN_HIGHWAY_BOTTOM_LEFT | @@ -39,7 +40,7 @@ enum { TERRAIN_NOT_CLEAR_EXCEPT_ROAD = TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_WATER | TERRAIN_BUILDING | TERRAIN_SHRUB | TERRAIN_GARDEN | TERRAIN_AQUEDUCT | TERRAIN_ELEVATION | TERRAIN_ACCESS_RAMP | - TERRAIN_RUBBLE | TERRAIN_WALL_OR_GATEHOUSE | TERRAIN_HIGHWAY, + TERRAIN_RUBBLE | TERRAIN_WALL_OR_GATEHOUSE | TERRAIN_HIGHWAY | TERRAIN_MARSHLAND, TERRAIN_NOT_CLEAR = TERRAIN_NOT_CLEAR_EXCEPT_ROAD | TERRAIN_ROAD, @@ -48,14 +49,14 @@ enum { TERRAIN_WALL_OR_GATEHOUSE | TERRAIN_HIGHWAY, TERRAIN_IMPASSABLE = TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_WATER | TERRAIN_BUILDING | TERRAIN_SHRUB | - TERRAIN_AQUEDUCT | TERRAIN_ELEVATION | TERRAIN_WALL_OR_GATEHOUSE, + TERRAIN_AQUEDUCT | TERRAIN_ELEVATION | TERRAIN_WALL_OR_GATEHOUSE | TERRAIN_MARSHLAND, TERRAIN_IMPASSABLE_ENEMY = TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_WATER | TERRAIN_SHRUB | TERRAIN_GARDEN | - TERRAIN_ELEVATION | TERRAIN_RUBBLE, + TERRAIN_ELEVATION | TERRAIN_RUBBLE | TERRAIN_MARSHLAND, TERRAIN_IMPASSABLE_WOLF = TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_WATER | TERRAIN_BUILDING | TERRAIN_SHRUB | TERRAIN_GARDEN | TERRAIN_AQUEDUCT | TERRAIN_ELEVATION | TERRAIN_ACCESS_RAMP | - TERRAIN_RUBBLE | TERRAIN_WALL_OR_GATEHOUSE, + TERRAIN_RUBBLE | TERRAIN_WALL_OR_GATEHOUSE | TERRAIN_MARSHLAND, TERRAIN_IMPASSABLE_EARTHQUAKE = TERRAIN_ROCK | TERRAIN_WATER | TERRAIN_ELEVATION | TERRAIN_ACCESS_RAMP, @@ -64,7 +65,7 @@ enum { TERRAIN_ALL = TERRAIN_TREE | TERRAIN_ROCK | TERRAIN_WATER | TERRAIN_BUILDING | TERRAIN_SHRUB | TERRAIN_GARDEN | TERRAIN_ROAD | TERRAIN_AQUEDUCT | TERRAIN_ELEVATION | TERRAIN_ACCESS_RAMP | TERRAIN_MEADOW | TERRAIN_RUBBLE | TERRAIN_FOUNTAIN_RANGE | - TERRAIN_WALL_OR_GATEHOUSE | TERRAIN_HIGHWAY, + TERRAIN_WALL_OR_GATEHOUSE | TERRAIN_HIGHWAY | TERRAIN_MARSHLAND, TERRAIN_MAP_EDGE = TERRAIN_TREE | TERRAIN_WATER, }; diff --git a/src/map/tiles.c b/src/map/tiles.c index f699a2d06d..27380d448b 100644 --- a/src/map/tiles.c +++ b/src/map/tiles.c @@ -26,6 +26,7 @@ #include "map/routing_terrain.h" #include "map/terrain.h" #include "scenario/map.h" +#include "scenario/property.h" @@ -1086,8 +1087,8 @@ static void set_water_image(int x, int y, int grid_offset) const terrain_image *img = map_image_context_get_shore(grid_offset); int image_id = image_group(GROUP_TERRAIN_WATER) + img->group_offset + img->item_offset; if (map_terrain_exists_tile_in_radius_with_type(x, y, 1, 2, TERRAIN_BUILDING)) { - // fortified shore - if (!map_is_bridge(grid_offset)) { //no fortification right under the bridge + // fortified shore -- but not under a marsh overlay (its wall sprite would draw over it) + if (!map_is_bridge(grid_offset) && !map_terrain_is(grid_offset, TERRAIN_MARSHLAND)) { int base = image_group(GROUP_TERRAIN_WATER_SHORE); switch (img->group_offset) { case 8: image_id = base + 10; break; @@ -1135,6 +1136,132 @@ void map_tiles_set_water(int x, int y) foreach_region_tile(x - 1, y - 1, x + 1, y + 1, set_water_image); } +// --- Overlay-terrain autotiling --- +// +// The tileset sprite is chosen from the 8 same-type neighbours +// the iso diamond's 4 EDGES are NE/SE/SW/NW, its 4 VERTICES are N/E/S/W. Map-orthogonal neighbours +// drive the edges, map-diagonal neighbours drive the vertices, with zero rotational offset: +// edge NE<-mapN SE<-mapE SW<-mapS NW<-mapW +// vtx N <-mapNW E <-mapNE S <-mapSE W <-mapSW +// Rules: an EDGE is SOLID when its orthogonal neighbour is same-type (the side merges), OPEN when +// not. A VERTEX carries a notch unless the sand/marsh reaches it (both flanking edges solid AND the +// corner diagonal present). Fully surrounded -> full tile (variants 1-8). Isolated -> 80 +// +// Signature bits: edges open NE=1,SE=2,SW=4,NW=8 ; vertex notch N=16,E=32,S=64,W=128. +typedef struct { unsigned char sig; unsigned char sprite; } overlay_sig; +static const overlay_sig OVERLAY_SIG_TABLE[] = { + {0xC4, 9}, {0x98, 13}, {0x31, 17}, {0x62, 21}, + {0xDC, 25}, {0xB9, 29}, {0x73, 33}, {0xE6, 37}, + {0xF5, 41}, {0xFA, 43}, + {0xFE, 45}, {0xFD, 46}, {0xFB, 47}, {0xF7, 48}, + {0xFF, 80}, + {0xA0, 49}, {0x50, 50}, + {0x80, 51}, {0x10, 52}, {0x20, 53}, {0x40, 54}, + {0x90, 55}, {0x30, 56}, {0x60, 57}, {0xC0, 58}, + {0xB0, 59}, {0x70, 60}, {0xE0, 61}, {0xD0, 62}, + {0xF0, 63}, + {0xD4, 64}, {0xE4, 65}, {0xF4, 66}, + {0xB8, 67}, {0xD8, 68}, {0xF8, 69}, + {0x71, 70}, {0xB1, 71}, {0xF1, 72}, + {0xE2, 73}, {0x72, 74}, {0xF2, 75}, + {0xFC, 76}, {0xF9, 77}, {0xF3, 78}, {0xF6, 79}, +}; + +// Compute the overlay sprite offset (0-based) for a tile, from its 8 neighbours of `terrain`. +// Map edge tiles count as same-type so masses flush to the map border have no artificial edge. +static int overlay_tile_offset(int grid_offset, int terrain) +{ + // same[] indexed by map direction 0=N 1=NE 2=E 3=SE 4=S 5=SW 6=W 7=NW. + // A neighbour OUTSIDE the playable map counts as same-type, so an overlay mass flush against + // the map border joins it (no artificial edge drawn towards the void). + int same[8]; + for (int i = 0; i < 8; i++) { + int n = grid_offset + map_grid_direction_delta(i); + int nx = map_grid_offset_to_x(n); + int ny = map_grid_offset_to_y(n); + same[i] = (!map_grid_is_inside(nx, ny, 1)) || map_terrain_is(n, terrain) ? 1 : 0; + } + // Rotate map directions into screen space according to the camera. Each 90 deg step = +2 dirs. + int rot = city_view_orientation(); // 0,2,4,6 + int o[8]; + for (int i = 0; i < 8; i++) { + o[i] = same[(i + rot) & 7]; + } + // screen-fixed orthogonals: o0=N o2=E o4=S o6=W ; diagonals o1=NE o3=SE o5=SW o7=NW + int n_ = o[0], e_ = o[2], s_ = o[4], w_ = o[6]; + int ne = o[1], se = o[3], sw = o[5], nw = o[7]; + if (n_ && e_ && s_ && w_ && ne && se && sw && nw) { + return -1; // fully surrounded -> caller picks a full-tile variant (1-8) + } + int edge_ne = !n_, edge_se = !e_, edge_sw = !s_, edge_nw = !w_; + int sig = 0; + if (edge_ne) sig |= 0x01; + if (edge_se) sig |= 0x02; + if (edge_sw) sig |= 0x04; + if (edge_nw) sig |= 0x08; + // A vertex is notched unless the terrain reaches it: both flanking edges solid and the corner + // diagonal present. + if (edge_nw || edge_ne || !nw) sig |= 0x10; // N + if (edge_ne || edge_se || !ne) sig |= 0x20; // E + if (edge_se || edge_sw || !se) sig |= 0x40; // S + if (edge_sw || edge_nw || !sw) sig |= 0x80; // W + for (unsigned int i = 0; i < sizeof(OVERLAY_SIG_TABLE) / sizeof(OVERLAY_SIG_TABLE[0]); i++) { + if (OVERLAY_SIG_TABLE[i].sig == sig) { + return OVERLAY_SIG_TABLE[i].sprite - 1; // 0-based offset + } + } + return -1; // no exact match (treat as full tile) +} + +static int marshland_base_image(void) +{ + return assets_get_image_id("Terrain_Maps", "Marshland_C_01"); +} + +static void set_overlay_native_ground(int grid_offset) +{ + if (map_terrain_is(grid_offset, TERRAIN_WATER | TERRAIN_ELEVATION | TERRAIN_ACCESS_RAMP)) { + return; + } + int image_id = map_property_is_alternate_terrain(grid_offset) + ? image_group(GROUP_TERRAIN_GRASS_2) : image_group(GROUP_TERRAIN_GRASS_1); + map_image_set(grid_offset, image_id + (map_random_get(grid_offset) & 7)); +} + +static void set_marshland_image(int x, int y, int grid_offset) +{ + if (!map_terrain_is(grid_offset, TERRAIN_MARSHLAND)) { + map_marsh_image_set(grid_offset, 0); + return; + } + set_overlay_native_ground(grid_offset); + int base = marshland_base_image(); + if (base <= 0) { + base = assets_get_image_id("Terrain_Maps", "Marshland_C_01"); + } + int offset = overlay_tile_offset(grid_offset, TERRAIN_MARSHLAND); + if (offset < 0) { + offset = map_random_get(grid_offset) % 8; // fully-surrounded / no-edge: full-tile variant 1-8 + } + map_marsh_image_set(grid_offset, base + offset); + map_property_mark_draw_tile(grid_offset); +} + +static void update_marshland_tile(int x, int y, int grid_offset) +{ + set_marshland_image(x, y, grid_offset); +} + +void map_tiles_update_all_marshland(void) +{ + foreach_map_tile(update_marshland_tile); +} + +void map_tiles_update_region_marshland(int x_min, int y_min, int x_max, int y_max) +{ + foreach_region_tile(x_min, y_min, x_max, y_max, update_marshland_tile); +} + static void set_aqueduct(int grid_offset) { const terrain_image *img = map_image_context_get_aqueduct(grid_offset, aqueduct_include_construction); @@ -1460,6 +1587,7 @@ void map_tiles_update_all(void) map_tiles_update_all_elevation(); map_tiles_update_all_water(); + map_tiles_update_all_marshland(); map_tiles_update_all_earthquake(); map_tiles_update_all_rocks(); foreach_map_tile(set_tree_image); diff --git a/src/map/tiles.h b/src/map/tiles.h index bbb51714c6..0a91382af4 100644 --- a/src/map/tiles.h +++ b/src/map/tiles.h @@ -35,6 +35,9 @@ void map_tiles_update_all_water(void); void map_tiles_update_region_water(int x_min, int y_min, int x_max, int y_max); void map_tiles_set_water(int x, int y); +void map_tiles_update_all_marshland(void); +void map_tiles_update_region_marshland(int x_min, int y_min, int x_max, int y_max); + void map_tiles_update_all_aqueducts(int include_construction); void map_tiles_update_region_aqueducts(int x_min, int y_min, int x_max, int y_max); diff --git a/src/translation/english.c b/src/translation/english.c index bfdad0f928..c7a86e1e1b 100644 --- a/src/translation/english.c +++ b/src/translation/english.c @@ -2116,7 +2116,11 @@ static translation_string all_strings[] = { {TR_BUILDING_TRIUMPHAL_ARCH_CONSTRUCTION_DESC, "The triumphal arch does not need resources from your city to be build. All necessary resources and laborers will be supplied by Rome."}, {TR_CITY_MESSAGE_TITLE_TRIUMPHAL_ARCH_COMPLETE, "Triumphal arch completed"}, {TR_CITY_MESSAGE_TEXT_TRIUMPHAL_ARCH_COMPLETE, "The triumphal arch now stands complete, its towering stonework and finely carved facade honoring the courage of our soldiers and the victories they have secured for the city. May it stand for generations as a symbol of strength, sacrifice and civic pride."}, - {TR_BUILDING_TRIUMPHAL_ARCH_SUPPLIED_BY_ROME, "(Supplied by Rome)"} + {TR_BUILDING_TRIUMPHAL_ARCH_SUPPLIED_BY_ROME, "(Supplied by Rome)"}, + {TR_EDITOR_TOOL_MARSHLAND, "Marshland"}, + {TR_EDITOR_TOOL_WATER, "Water"}, + {TR_TERRAIN_MARSHLAND, "Marshland"}, + {TR_TERRAIN_MARSHLAND_DESC, "A fetid swamp. Marshland is impassable and breeds disease: it lowers the desirability of the surrounding area and spreads sickness to nearby houses. No respectable citizen wants to live near it."}, }; void translation_english(const translation_string **strings, int *num_strings) diff --git a/src/translation/french.c b/src/translation/french.c index 0639c73534..57333db1a3 100644 --- a/src/translation/french.c +++ b/src/translation/french.c @@ -2116,7 +2116,11 @@ static translation_string all_strings[] = { {TR_BUILDING_TRIUMPHAL_ARCH_CONSTRUCTION_DESC, "La construction de l'arc de triomphe ne nécessite pas de ressources issues de votre cité. Ces dernières ainsi que la main-d'œuvre seront fournies par Rome."}, {TR_CITY_MESSAGE_TITLE_TRIUMPHAL_ARCH_COMPLETE, "Arc de triomphe achevé"}, {TR_CITY_MESSAGE_TEXT_TRIUMPHAL_ARCH_COMPLETE, "L'arc de triomphe est achevé. Sa stature imposante et sa façade finement taillée rendent honneur à la bravoure de nos soldats et à leurs victoires pour la grandeur de la cité. Qu'il trône avec éclat pour des générations comme symbole de la force, du sacrifice, et de la fierté citoyenne."}, - {TR_BUILDING_TRIUMPHAL_ARCH_SUPPLIED_BY_ROME, "(fourni par Rome)"} + {TR_BUILDING_TRIUMPHAL_ARCH_SUPPLIED_BY_ROME, "(fourni par Rome)"}, + {TR_EDITOR_TOOL_MARSHLAND, "Marais"}, + {TR_EDITOR_TOOL_WATER, "Eau"}, + {TR_TERRAIN_MARSHLAND, "Marais"}, + {TR_TERRAIN_MARSHLAND_DESC, "Un marécage fétide. Le marais est infranchissable et c'est un nid à maladies : il réduit l'attrait des environs et propage la maladie aux habitations proches. Aucun citoyen respectable ne veut vivre à proximité."}, }; void translation_french(const translation_string **strings, int *num_strings) diff --git a/src/translation/translation.h b/src/translation/translation.h index 28161daf15..d2bd7aea18 100644 --- a/src/translation/translation.h +++ b/src/translation/translation.h @@ -2112,6 +2112,10 @@ typedef enum { TR_CITY_MESSAGE_TITLE_TRIUMPHAL_ARCH_COMPLETE, TR_CITY_MESSAGE_TEXT_TRIUMPHAL_ARCH_COMPLETE, TR_BUILDING_TRIUMPHAL_ARCH_SUPPLIED_BY_ROME, + TR_EDITOR_TOOL_MARSHLAND, + TR_EDITOR_TOOL_WATER, + TR_TERRAIN_MARSHLAND, + TR_TERRAIN_MARSHLAND_DESC, TRANSLATION_MAX_KEY } translation_key; diff --git a/src/widget/city/building_ghost.c b/src/widget/city/building_ghost.c index d8014ac36c..2b54a0bdaa 100644 --- a/src/widget/city/building_ghost.c +++ b/src/widget/city/building_ghost.c @@ -981,6 +981,13 @@ static void draw_bridge(const map_tile *tile, int x, int y, building_type type) grid_slice blocked_tiles = { .size = 0 }; int end_grid_offset = map_bridge_calculate_length_direction(tile->x, tile->y, &length, &direction, &blocked_tiles); + for (int i = 0; i < blocked_tiles.size; i++) { + if (map_terrain_is(blocked_tiles.grid_offsets[i], TERRAIN_MARSHLAND)) { + building_construction_set_cost(0); + return; + } + } + int dir = direction - city_view_orientation(); if (dir < 0) { dir += 8; diff --git a/src/widget/city/draw.c b/src/widget/city/draw.c index b7eb7cc442..e7082c5178 100644 --- a/src/widget/city/draw.c +++ b/src/widget/city/draw.c @@ -404,6 +404,11 @@ static void draw_footprint(int x, int y, int grid_offset) image_draw_isometric_footprint_from_draw_tile(image_id, x, y, color_mask, draw_context.scale); } + int marsh_image = map_marsh_image_at(grid_offset); + if (marsh_image > 0) { + image_draw_isometric_footprint_from_draw_tile(marsh_image, x, y, color_mask, draw_context.scale); + } + // Grid is drawn by the renderer directly at zoom > 200% if (!building_id && config_get(CONFIG_UI_SHOW_GRID) && draw_context.scale <= 2.0f) { image_draw(assets_lookup_image_id(ASSET_UI_GRID), x, y, full_grid_color(), draw_context.scale); @@ -741,6 +746,10 @@ static void draw_top(int x, int y, int grid_offset) } else { image_draw_isometric_top_from_draw_tile(map_image_at(grid_offset), x, y, color_mask, draw_context.scale); } + int marsh_image = map_marsh_image_at(grid_offset); + if (marsh_image > 0) { + image_draw_isometric_top_from_draw_tile(marsh_image, x, y, color_mask, draw_context.scale); + } } static void draw_figures(int x, int y, int grid_offset) diff --git a/src/widget/map_editor.c b/src/widget/map_editor.c index 390b5e0274..4edbc6ea26 100644 --- a/src/widget/map_editor.c +++ b/src/widget/map_editor.c @@ -137,6 +137,11 @@ static void draw_footprint(int x, int y, int grid_offset) } image_draw_isometric_footprint_from_draw_tile(image_id, x, y, color_mask, draw_context.scale); + int marsh_image = map_marsh_image_at(grid_offset); + if (marsh_image > 0) { + image_draw_isometric_footprint_from_draw_tile(marsh_image, x, y, color_mask, draw_context.scale); + } + if (config_get(CONFIG_UI_SHOW_GRID) && draw_context.scale <= 2.0f) { //grid is drawn by the renderer directly at zoom > 200% static int grid_id = 0; @@ -175,6 +180,10 @@ static void draw_top(int x, int y, int grid_offset) color_mask = complex_button_basic_colors((event_tiles[grid_offset][0] % 10) + 1); } image_draw_isometric_top_from_draw_tile(image_id, x, y, color_mask, draw_context.scale); + int marsh_image = map_marsh_image_at(grid_offset); + if (marsh_image > 0) { + image_draw_isometric_top_from_draw_tile(marsh_image, x, y, color_mask, draw_context.scale); + } } static void draw_flags(int x, int y, int grid_offset) diff --git a/src/widget/map_editor_tool.c b/src/widget/map_editor_tool.c index 747398d573..1ab780a4de 100644 --- a/src/widget/map_editor_tool.c +++ b/src/widget/map_editor_tool.c @@ -281,6 +281,7 @@ void map_editor_tool_draw(const map_tile *tile) case TOOL_GRASS: case TOOL_MEADOW: + case TOOL_MARSHLAND: case TOOL_ROCKS: case TOOL_SHRUB: case TOOL_TREES: diff --git a/src/widget/minimap.c b/src/widget/minimap.c index 9aced64c9f..cc0fd71a8a 100644 --- a/src/widget/minimap.c +++ b/src/widget/minimap.c @@ -41,6 +41,7 @@ typedef struct { tile_color tree[4]; tile_color rock[4]; tile_color meadow[4]; + tile_color marshland[4]; tile_color grass[8]; tile_color road; tile_color highway; @@ -74,6 +75,7 @@ static const tile_color_climate_variants CLIMATE_VARIANTS[3] = { .tree = {{0xff6b8431, 0xff102108}, {0xff103908, 0xff737b29}, {0xff103108, 0xff526b21}, {0xff737b31, 0xff084a10}}, .rock = {{0xff948484, 0xff635a4a}, {0xffa59c94, 0xffb5ada5}, {0xffb5ada5, 0xff8c8484}, {0xff635a4a, 0xffa59c94}}, .meadow = {{0xffd6bd63, 0xff9c8c39}, {0xff948c39, 0xffd6bd63}, {0xffd6bd63, 0xff9c9439}, {0xff848431, 0xffada54a}}, + .marshland = {{0xff5a6b39, 0xff52633a}, {0xff63734a, 0xff4a5a31}, {0xff525a33, 0xff63733a}, {0xff4a5a31, 0xff5a6b42}}, .grass = { {0xff6b8c31, 0xff6b7b29}, {0xff738431, 0xff6b7b29}, {0xff6b7329, 0xff7b8c39}, {0xff527b29, 0xff6b7321}, {0xff6b8431, 0xff737b31}, {0xff6b7b31, 0xff737b29}, {0xff636b18, 0xff526b21}, {0xff737b31, 0xff737b29} @@ -88,6 +90,7 @@ static const tile_color_climate_variants CLIMATE_VARIANTS[3] = { .tree = {{0xff527b31, 0xff082108}, {0xff083908, 0xff5a7329}, {0xff082908, 0xff316b21}, {0xff527b29, 0xff084a21}}, .rock = {{0xff8c8484, 0xff5a5252}, {0xff9c9c94, 0xffa5a5a5}, {0xffa5a5a5, 0xff848484}, {0xff5a5252, 0xff9c9c94}}, .meadow = {{0xff427318, 0xff8c9442}, {0xffb5ad4a, 0xff738c39}, {0xff8c8c39, 0xff6b7b29}, {0xff527331, 0xff5a8442}}, + .marshland = {{0xff42583a, 0xff3a5233}, {0xff4a634a, 0xff344a2a}, {0xff3a4a31, 0xff4a5a3a}, {0xff344a2a, 0xff42583a}}, .grass = { {0xff4a8431, 0xff4a7329}, {0xff527b29, 0xff4a7329}, {0xff526b29, 0xff5a8439}, {0xff397321, 0xff4a6b21}, {0xff527b31, 0xff5a7331}, {0xff4a7329, 0xff5a7329}, {0xff4a6b18, 0xff316b21}, {0xff527b29, 0xff527329} @@ -102,6 +105,7 @@ static const tile_color_climate_variants CLIMATE_VARIANTS[3] = { .tree = {{0xffa59c7b, 0xff6b7b18}, {0xff214210, 0xffada573}, {0xff526b21, 0xffcec6a5}, {0xffa59c7b, 0xff316321}}, .rock = {{0xffa59494, 0xff736352}, {0xffa59c94, 0xffb5ada5}, {0xffb5ada5, 0xff8c847b}, {0xff736352, 0xffbdada5}}, .meadow = {{0xff739c31, 0xff9cbd52}, {0xff7bb529, 0xff63ad21}, {0xff9cbd52, 0xff8c944a}, {0xff7ba539, 0xff739c31}}, + .marshland = {{0xff7b846b, 0xff737b63}, {0xff848c73, 0xff6b735a}, {0xff737b5a, 0xff848c6b}, {0xff6b735a, 0xff7b846b}}, .grass = { {0xffbdbd9c, 0xffb5b594}, {0xffc6bda5, 0xffbdbda5}, {0xffbdbd9c, 0xffc6c6ad}, {0xffd6cead, 0xffc6bd9c}, {0xffa59c7b, 0xffbdb594}, {0xffcecead, 0xffb5ad94}, {0xffc6c6a5, 0xffdedebd}, {0xffcecead, 0xffd6d6b5} @@ -465,6 +469,8 @@ static void draw_minimap_tile(int x_view, int y_view, int grid_offset) colors = &minimap_colors.climate->road; } else if (terrain & TERRAIN_HIGHWAY) { colors = &minimap_colors.climate->highway; + } else if (terrain & TERRAIN_MARSHLAND) { + colors = &minimap_colors.climate->marshland[rand & 3]; } else if (terrain & TERRAIN_WATER) { colors = &minimap_colors.climate->water[rand & 3]; } else if (terrain & (TERRAIN_SHRUB | TERRAIN_TREE)) { diff --git a/src/widget/sidebar/editor.c b/src/widget/sidebar/editor.c index b0335c2ff9..4ffb040d68 100644 --- a/src/widget/sidebar/editor.c +++ b/src/widget/sidebar/editor.c @@ -34,7 +34,7 @@ static image_button buttons_build[] = { {84, 123, 71, 23, IB_NORMAL, GROUP_EDITOR_SIDEBAR_BUTTONS, 48, button_attributes, button_none, 1, 0, 1}, {13, 267, 39, 26, IB_NORMAL, GROUP_EDITOR_SIDEBAR_BUTTONS, 0, button_build_tool, button_none, TOOL_GRASS, 0, 1}, {63, 267, 39, 26, IB_NORMAL, GROUP_EDITOR_SIDEBAR_BUTTONS, 3, button_build_tool, button_none, TOOL_TREES, 0, 1}, - {113, 267, 39, 26, IB_NORMAL, GROUP_EDITOR_SIDEBAR_BUTTONS, 6, button_build_tool, button_none, TOOL_WATER, 0, 1}, + {113, 267, 39, 26, IB_NORMAL, GROUP_EDITOR_SIDEBAR_BUTTONS, 6, button_build_menu, button_none, MENU_WATER, 0, 1}, {13, 303, 39, 26, IB_BUILD, GROUP_EDITOR_SIDEBAR_BUTTONS, 21, button_build_menu, button_none, MENU_ELEVATION, 0, 1}, {63, 303, 39, 26, IB_NORMAL, GROUP_EDITOR_SIDEBAR_BUTTONS, 12, button_build_tool, button_none, TOOL_SHRUB, 0, 1}, {113, 303, 39, 26, IB_NORMAL, GROUP_EDITOR_SIDEBAR_BUTTONS, 15, button_build_tool, button_none, TOOL_ROCKS, 0, 1}, @@ -62,7 +62,11 @@ static void draw_status(void) int selected_tool = editor_tool_type(); int brush_size = editor_tool_brush_size() - 1; - lang_text_draw(49, selected_tool, text_offset, 178, FONT_NORMAL_WHITE); + if (selected_tool == TOOL_MARSHLAND) { + lang_text_draw(CUSTOM_TRANSLATION, TR_EDITOR_TOOL_MARSHLAND, text_offset, 178, FONT_NORMAL_WHITE); + } else { + lang_text_draw(49, selected_tool, text_offset, 178, FONT_NORMAL_WHITE); + } switch (selected_tool) { case TOOL_GRASS: case TOOL_TREES: @@ -70,6 +74,7 @@ static void draw_status(void) case TOOL_SHRUB: case TOOL_ROCKS: case TOOL_MEADOW: + case TOOL_MARSHLAND: case TOOL_RAISE_LAND: case TOOL_LOWER_LAND: lang_text_draw(48, brush_size, text_offset, 194, FONT_NORMAL_GREEN); diff --git a/src/window/building/common.h b/src/window/building/common.h index e6cc967489..0bae7acd60 100644 --- a/src/window/building/common.h +++ b/src/window/building/common.h @@ -36,6 +36,7 @@ typedef enum { TERRAIN_INFO_ENTRY_FLAG = 14, TERRAIN_INFO_EXIT_FLAG = 15, TERRAIN_INFO_HIGHWAY = 16, + TERRAIN_INFO_MARSHLAND = 17, } terrain_info_type; typedef struct { diff --git a/src/window/building/terrain.c b/src/window/building/terrain.c index 0e290bc696..8b7e543032 100644 --- a/src/window/building/terrain.c +++ b/src/window/building/terrain.c @@ -4,10 +4,13 @@ #include "military.h" #include "utility.h" +#include "core/dir.h" +#include "core/random.h" #include "figure/figure.h" #include "graphics/lang_text.h" #include "graphics/panel.h" #include "sound/speech.h" +#include "translation/translation.h" #include "window/building/figures.h" void window_building_draw_no_people(building_info_context *c) @@ -43,6 +46,10 @@ void window_building_draw_terrain(building_info_context *c) c->can_play_sound = 0; if (c->figure.count > 0) { window_building_play_figure_phrase(c); + } else if (c->terrain_type == TERRAIN_INFO_MARSHLAND) { + sound_speech_play_file((random_byte() & 1) + ? ASSETS_DIRECTORY "/Sounds/Marshland01.ogg" + : ASSETS_DIRECTORY "/Sounds/Marshland02.ogg"); } else { sound_speech_play_file("wavs/empty_land.wav"); } @@ -60,10 +67,21 @@ void window_building_draw_terrain(building_info_context *c) outer_panel_draw(c->x_offset, c->y_offset, c->width_blocks, c->height_blocks); if (!c->figure.count) { - lang_text_draw_centered(70, c->terrain_type + 10, - c->x_offset, c->y_offset + 10, BLOCK_SIZE * c->width_blocks, FONT_LARGE_BLACK); + // Marshland has no entry in the vanilla terrain text group 70, so use its + // custom title/description; otherwise fall back to the generic group-70 texts. + if (c->terrain_type == TERRAIN_INFO_MARSHLAND) { + lang_text_draw_centered(CUSTOM_TRANSLATION, TR_TERRAIN_MARSHLAND, + c->x_offset, c->y_offset + 10, BLOCK_SIZE * c->width_blocks, FONT_LARGE_BLACK); + } else { + lang_text_draw_centered(70, c->terrain_type + 10, + c->x_offset, c->y_offset + 10, BLOCK_SIZE * c->width_blocks, FONT_LARGE_BLACK); + } } - if (c->figure.count == 0 && + if (c->figure.count == 0 && c->terrain_type == TERRAIN_INFO_MARSHLAND) { + lang_text_draw_multiline(CUSTOM_TRANSLATION, TR_TERRAIN_MARSHLAND_DESC, + c->x_offset + 40, c->y_offset + BLOCK_SIZE * c->height_blocks - 125, + BLOCK_SIZE * (c->width_blocks - 4), FONT_NORMAL_BLACK); + } else if (c->figure.count == 0 && c->terrain_type != TERRAIN_INFO_ROAD && c->terrain_type != TERRAIN_INFO_PLAZA && c->terrain_type != TERRAIN_INFO_HIGHWAY) { diff --git a/src/window/building_info.c b/src/window/building_info.c index 5c1a844cda..077a697879 100644 --- a/src/window/building_info.c +++ b/src/window/building_info.c @@ -377,6 +377,8 @@ static void init(int grid_offset) } else { context.terrain_type = TERRAIN_INFO_ROCK; } + } else if (map_terrain_is(grid_offset, TERRAIN_MARSHLAND)) { + context.terrain_type = TERRAIN_INFO_MARSHLAND; } else if ((map_terrain_get(grid_offset) & (TERRAIN_WATER | TERRAIN_BUILDING)) == TERRAIN_WATER) { context.terrain_type = TERRAIN_INFO_WATER; } else if (map_terrain_is(grid_offset, TERRAIN_SHRUB)) { diff --git a/src/window/editor/attributes.c b/src/window/editor/attributes.c index e208d68296..a2ee295fd1 100644 --- a/src/window/editor/attributes.c +++ b/src/window/editor/attributes.c @@ -16,6 +16,7 @@ #include "graphics/text.h" #include "graphics/window.h" #include "input/input.h" +#include "map/tiles.h" #include "scenario/demand_change.h" #include "scenario/editor.h" #include "scenario/invasion.h" @@ -368,6 +369,7 @@ static void button_change_climate(const generic_button *button) { scenario_editor_cycle_climate(); image_load_climate(scenario_property_climate(), editor_is_active(), 0, 0); + map_tiles_update_all_marshland(); widget_minimap_invalidate(); window_request_refresh(); } diff --git a/src/window/editor/build_menu.c b/src/window/editor/build_menu.c index 2e41d7d4a8..2669aa4e5b 100644 --- a/src/window/editor/build_menu.c +++ b/src/window/editor/build_menu.c @@ -55,6 +55,7 @@ static const int MENU_TYPES[MENU_NUM_ITEMS][MAX_ITEMS_PER_MENU] = { TR_EDITOR_SCENARIO_BUILDING_NATIVE_WATCHTOWER, TR_EDITOR_RUBBLE,-1}, {23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, -1}, {TR_EDITOR_TOOL_EARTHQUAKE_POINT, TR_EDITOR_TOOL_EARTHQUAKE_CUSTOM, TR_EDITOR_TOOL_EARTHQUAKE_REMOVE, -1}, + {TR_EDITOR_TOOL_WATER, TR_EDITOR_TOOL_MARSHLAND, -1}, }; static struct { @@ -195,6 +196,13 @@ static void button_menu_item(const generic_button *button) case 1: editor_tool_set_type(TOOL_EARTHQUAKE_CUSTOM); break; case 2: editor_tool_set_type(TOOL_EARTHQUAKE_CUSTOM_REMOVE); break; } + break; + case MENU_WATER: + switch (index) { + case 0: editor_tool_set_type(TOOL_WATER); break; + case 1: editor_tool_set_type(TOOL_MARSHLAND); break; + } + break; } data.selected_submenu = MENU_NONE; window_editor_map_show(); diff --git a/src/window/editor/build_menu.h b/src/window/editor/build_menu.h index 4ef7b044ed..ae6d0d2f26 100644 --- a/src/window/editor/build_menu.h +++ b/src/window/editor/build_menu.h @@ -11,7 +11,8 @@ enum { MENU_NATIVE_BUILDINGS = 5, MENU_ANIMAL_POINTS = 6, MENU_EARTHQUAKE = 7, - MENU_NUM_ITEMS = 8 + MENU_WATER = 8, + MENU_NUM_ITEMS = 9 }; void window_editor_build_menu_show(int submenu);