diff --git a/data/campaigns/World_Conquest/lua/campaign/enemy_themed.lua b/data/campaigns/World_Conquest/lua/campaign/enemy_themed.lua index 663a85cf5c3..600f4235832 100644 --- a/data/campaigns/World_Conquest/lua/campaign/enemy_themed.lua +++ b/data/campaigns/World_Conquest/lua/campaign/enemy_themed.lua @@ -31,7 +31,7 @@ local function wct_map_enemy_themed(race, pet, castle, village, chance) return end --give themed castle - wesnoth.current.map[boss] = wesnoth.map.replace_base("K" .. castle) + wesnoth.current.map[boss] = wesnoth.map.replace.base("K" .. castle) wesnoth.wml_actions.terrain { terrain="C" .. castle, wml.tag["and"] { diff --git a/data/campaigns/World_Conquest/lua/map/postgeneration_utils/engine.lua b/data/campaigns/World_Conquest/lua/map/postgeneration_utils/engine.lua index c97b3e5ff88..663a4c45f66 100644 --- a/data/campaigns/World_Conquest/lua/map/postgeneration_utils/engine.lua +++ b/data/campaigns/World_Conquest/lua/map/postgeneration_utils/engine.lua @@ -34,7 +34,7 @@ function set_terrain_impl(data) for j = 1, num_tiles do local loc = locs[i][j] if chance >= 1000 or chance >= mathx.random(1000) then - map[loc] = wesnoth.map['replace_' .. layer](mathx.random_choice(terrains)) + map[loc] = wesnoth.map.replace[layer](mathx.random_choice(terrains)) nlocs_changed = nlocs_changed + 1 end end diff --git a/data/lua/cave_map_generator.lua b/data/lua/cave_map_generator.lua index f5eb4dc29f8..0e25dc8ca45 100644 --- a/data/lua/cave_map_generator.lua +++ b/data/lua/cave_map_generator.lua @@ -5,32 +5,65 @@ local random = mathx.random local callbacks = {} function callbacks.generate_map(params) - local map = MG.create_map(params.map_width, params.map_height, params.terrain_wall) + local map = wesnoth.map.create(params.map_width, params.map_height, params.terrain_wall) local function build_chamber(x, y, locs_set, size, jagged) - if locs_set:get(x,y) or not map:on_board(x, y) or size == 0 then + if locs_set:get(x,y) or not map:on_board(x, y, true) or size == 0 then return end locs_set:insert(x,y) - for xn, yn in MG.adjacent_tiles(x, y) do + for xn, yn in map:iter_adjacent(x, y) do if random(100) <= 100 - jagged then build_chamber(xn, yn, locs_set, size - 1, jagged) end end end - local function clear_tile(x, y) - if not map:on_board(x,y) then + local function clear_tile(x, y, terrain_clear) + if not map:on_board(x,y,true) then return end - if map:get_tile(x,y) == params.terrain_castle or map:get_tile(x,y) == params.terrain_keep then + if map[{x,y}] == params.terrain_castle or map[{x,y}] == params.terrain_keep then return end + local tile = mathx.random_choice(terrain_clear or params.terrain_clear) + map[{x, y}] = wesnoth.map.replace.both(tile) local r = random(1000) if r <= params.village_density then - map:set_tile(x, y, params.terrain_village) + local village = mathx.random_choice(params.terrain_village) + map[{x, y}] = wesnoth.map.replace.if_failed(village, 'overlay') + end + end + + local function place_road(to_x, to_y, from_x, from_y, road_ops, terrain_clear) + if not map:on_board(to_x, to_y, true) then + return + end + if map[{to_x, to_y}] == params.terrain_castle or map[{to_x, to_y}] == params.terrain_keep then + return + end + local tile_op = road_ops[map[{to_x, to_y}]] + if tile_op then + if tile_op.convert_to_bridge and from_x and from_y then + local bridges = {} + for elem in tile_op.convert_to_bridge:gmatch("[^%s,][^,]*") do + table.insert(bridges, elem) + end + local dir = wesnoth.map.get_relative_dir(from_x, from_y, to_x, to_y) + if dir == 'n' or dir == 's' then + map[{to_x, to_y}] = wesnoth.map.replace.if_failed(bridges[1], 'overlay') + elseif dir == 'sw' or dir == 'ne' then + map[{to_x, to_y}] = wesnoth.map.replace.if_failed(bridges[2], 'overlay') + elseif dir == 'se' or dir == 'nw' then + map[{to_x, to_y}] = wesnoth.map.replace.if_failed(bridges[3], 'overlay') + end + elseif tile_op.convert_to then + local tile = mathx.random_choice(tile_op.convert_to) + map[{to_x, to_y}] = wesnoth.map.replace.both(tile) + end else - map:set_tile(x, y, params.terrain_clear) + local tile = mathx.random_choice(terrain_clear or params.terrain_clear) + map[{to_x, to_y}] = wesnoth.map.replace.both(tile) end end @@ -39,25 +72,43 @@ function callbacks.generate_map(params) local passages = {} for chamber in wml.child_range(params, "chamber") do + if chamber.ignore then goto continue end local chance = tonumber(chamber.chance) or 100 - local x = chamber.x - local y = chamber.y + local x, y = MG.random_location(chamber.x, chamber.y) + -- Note: x,y run from (0,0) to (w+1,h+1) + if chamber.relative_to == "top-right" then + x = map.width - x - 1 + elseif chamber.relative_to == "bottom-right" then + x = map.width - x - 1 + y = map.height - y - 1 + elseif chamber.relative_to == "bottom-left" then + y = map.height - y - 1 + elseif chamber.relative_to == "top-middle" then + x = math.ceil(map.width / 2) + x + elseif chamber.relative_to == "bottom-middle" then + x = math.ceil(map.width / 2) + x + y = map.height - y - 1 + elseif chamber.relative_to == "middle-left" then + y = math.ceil(map.height / 2) + y + elseif chamber.relative_to == "middle-right" then + y = math.ceil(map.height / 2) + y + x = map.width - x - 1 + elseif chamber.relative_to == "center" then + x = math.ceil(map.width / 2) + x + y = math.ceil(map.height / 2) + y + end -- Default is "top-left" which means no adjustments needed local id = chamber.id if chance == 0 or random(100) > chance then -- Set chance to 0 so that the scenario generator can tell which chambers were used params.chance = 0 goto continue end + if type(chamber.require_player) == "number" and chamber.require_player > params.nplayers then + params.chance = 0 + goto continue + end -- Ditto, set it to 100 params.chance = 100 - if type(x) == "string" then - local x_min, x_max = x:match("(%d+)-(%d+)") - x = random(tonumber(x_min), tonumber(x_max)) - end - if type(y) == "string" then - local y_min, y_max = y:match("(%d+)-(%d+)") - y = random(tonumber(y_min), tonumber(y_max)) - end local locs_set = LS.create() build_chamber(x, y, locs_set, chamber.size or 3, chamber.jagged or 0) local items = {} @@ -71,19 +122,29 @@ function callbacks.generate_map(params) locs_set = locs_set, id = id, items = items, + data = chamber, }) chambers_by_id[id] = chambers[#chambers] for passage in wml.child_range(chamber, "passage") do + if passage.ignore then goto continue end local dst = chambers_by_id[passage.destination] if dst ~= nil then + local road_costs, road_ops = {}, {} + for road in wml.child_range(passage, "road_cost") do + road_costs[road.terrain] = road.cost + road_ops[road.terrain] = road + end table.insert(passages, { start_x = x, start_y = y, dest_x = dst.center_x, dest_y = dst.center_y, data = passage, + costs = road_costs, + roads = road_ops, }) end + ::continue:: end ::continue:: end @@ -91,8 +152,8 @@ function callbacks.generate_map(params) for i,v in ipairs(chambers) do local locs_list = {} for x, y in v.locs_set:stable_iter() do - clear_tile(x, y) - if map:on_inner_board(x, y) then + clear_tile(x, y, v.data.terrain_clear) + if map:on_board(x, y, false) then table.insert(locs_list, {x,y}) end end @@ -103,13 +164,13 @@ function callbacks.generate_map(params) local x, y = table.unpack(loc) if item.id then - map:add_location(x, y, item.id) + map.special_locations[item.id] = {x, y} end if item.place_castle then - map:set_tile(x, y, params.terrain_keep) - for x2, y2 in MG.adjacent_tiles(x, y) do - map:set_tile(x2, y2, params.terrain_castle) + map[{x, y}] = wesnoth.map.replace.both(params.terrain_keep) + for x2, y2 in map:iter_adjacent(x, y) do + map[{x2, y2}] = wesnoth.map.replace.both(params.terrain_castle) end end end @@ -127,8 +188,10 @@ function callbacks.generate_map(params) return math.huge end local res = 1.0 - if map:get_tile(x, y) == params.terrain_wall then - res = laziness + local tile = map[{x, y}] + res = v.costs[tile] or 1.0 + if tile == params.terrain_wall then + res = laziness * res end if windiness > 1 then res = res * random(windiness) @@ -140,8 +203,18 @@ function callbacks.generate_map(params) for j, loc in ipairs(path) do local locs_set = LS.create() build_chamber(loc[1], loc[2], locs_set, width, jagged) + local prev_x, prev_y for x,y in locs_set:stable_iter() do - clear_tile(x, y) + local r = 1000 + local ter_to_place + if v.data.place_villages then r = random(1000) end + if r <= params.village_density then + ter_to_place = v.data.terrain_village or params.terrain_village + else + ter_to_place = v.data.terrain_clear or params.terrain_clear + end + place_road(x, y, prev_x, prev_y, v.roads, ter_to_place) + prev_x, prev_y = x, y end end end @@ -159,11 +232,11 @@ function callbacks.generate_map(params) wml.error("Unknown transformation '" .. t .. "'") end end - map[transforms[random(#transforms)]](map) + MG[transforms[random(#transforms)]](map) end end - return tostring(map) + return map.data end function callbacks.generate_scenario(params) @@ -172,9 +245,16 @@ function callbacks.generate_scenario(params) scenario.map_data = callbacks.generate_map(params) for chamber in wml.child_range(params, "chamber") do local chamber_items = wml.get_child(chamber, "items") - if chamber.chance == 100 and chamber_items then - -- TODO: Should we support [event]same_location_as_previous=yes? + if (chamber.chance or 100) == 100 and chamber_items then for i,tag in ipairs(chamber_items) do + if tag.tag == 'event' and tag.contents.same_location_as_previous then + local evt_data = tag.contents; + evt_data.same_location_as_previous = nil + table.insert(evt_data, wml.tag.filter{ + x = chamber.x, + y = chamber.y + }) + end table.insert(scenario, tag) end end diff --git a/data/lua/core/map.lua b/data/lua/core/map.lua index 01a5d16c06c..b096434c035 100644 --- a/data/lua/core/map.lua +++ b/data/lua/core/map.lua @@ -34,6 +34,13 @@ function wesnoth.map.read_location(...) return nil, 0 end +function wesnoth.map.nearest_loc(to, candidates) + local F = wesnoth.require "functional" + return F.choose(candidates, function(loc) + return -wesnoth.map.distance_between(to, loc) + end) +end + if wesnoth.kernel_type() ~= "Application Lua Kernel" then -- possible terrain string inputs: -- A A^ A^B ^ ^B @@ -79,7 +86,7 @@ if wesnoth.kernel_type() ~= "Application Lua Kernel" then if base == '' then -- ^ or ^B -- There's no way to find a base to replace with in this case. -- Could use the existing base, but that's not really replacing both, is it? - error('replace_both: no base terrain specified') + error('replace.both: no base terrain specified') elseif overlay == '' then -- A^ -- This would normally mean replace base while preserving overlay, -- but we actually want to replace base and clear overlay. @@ -89,6 +96,13 @@ if wesnoth.kernel_type() ~= "Application Lua Kernel" then return code end end + + wesnoth.map.replace = { + both = wesnoth.map.replace_both, + base = wesnoth.map.replace_base, + overlay = wesnoth.map.replace_overlay, + if_failed = wesnoth.map.replace_if_failed, + } ---Iterate over on-map hexes adjacent to a given hex. ---@param map terrain_map @@ -195,9 +209,9 @@ if wesnoth.kernel_type() == "Game Lua Kernel" then elseif key == 'terrain' then wesnoth.current.map[self] = val elseif key == 'base_terrain' then - wesnoth.current.map[self] = wesnoth.map.replace_base(val) + wesnoth.current.map[self] = wesnoth.map.replace.base(val) elseif key == 'overlay_terrain' then - wesnoth.current.map[self] = wesnoth.map.replace_overlay(val) + wesnoth.current.map[self] = wesnoth.map.replace.overlay(val) elseif key == 1 then self.x = val elseif key == 2 then @@ -306,9 +320,9 @@ if wesnoth.kernel_type() == "Game Lua Kernel" then if new_ter == '' or type(new_ter) ~= 'string' then error('set_terrain: expected terrain string') end if replace_if_failed then mode = mode or 'both' - new_ter = wesnoth.map.replace_if_failed(new_ter, mode) + new_ter = wesnoth.map.replace.if_failed(new_ter, mode) elseif mode == 'both' or mode == 'base' or mode == 'overlay' then - new_ter = wesnoth.map['replace_' .. mode](new_ter) + new_ter = wesnoth.map.replace[mode](new_ter) elseif mode ~= nil then error('set_terrain: invalid mode') end diff --git a/data/lua/mapgen_helper.lua b/data/lua/mapgen_helper.lua index 6d914d17947..24d41001a99 100644 --- a/data/lua/mapgen_helper.lua +++ b/data/lua/mapgen_helper.lua @@ -1,13 +1,37 @@ local LS = wesnoth.require "location_set" +---@class mapgen_helper local mapgen_helper, map_mt = {}, {__index = {}} -function mapgen_helper.create_map(width,height,default_terrain) - local map = setmetatable({w = width, h = height}, map_mt) - for i = 1, width * height do - table.insert(map, default_terrain or 'Gg') +---@class map_wrapper +---@field __map terrain_map + +---Create a map. +---@deprecated Use wesnoth.map.create instead +---@param width integer +---@param height integer +---@param default_terrain string +---@return map_wrapper +local function create_map(width,height,default_terrain) + return setmetatable({__map = wesnoth.map.create(width, height, default_terrain)}, map_mt) +end + +local function rand_from_ranges(list) + if type(list) == 'number' then return math.tointeger(list) end + local choices = {} + for n in stringx.iter_ranges(list) do + table.insert(choices, n) end - return map + return math.tointeger(mathx.random_choice(choices)) or 0 +end + +---Select a random location from lists of coordinates. +---@param x_list string +---@param y_list string +---@return integer +---@return integer +function mapgen_helper.random_location(x_list, y_list) + return rand_from_ranges(x_list), rand_from_ranges(y_list) end local valid_transforms = { @@ -16,91 +40,120 @@ local valid_transforms = { flip_xy = true, } +---Test whether a string is a valid transform +---@param t string +---@return boolean function mapgen_helper.is_valid_transform(t) return valid_transforms[t] end -local function loc_to_index(map,x,y) - return x + 1 + y * map.w -end - +---Set the tile at the specified location +---@param map map_wrapper +---@param x integer +---@param y integer +---@param val string function map_mt.__index.set_tile(map, x, y, val) - map[loc_to_index(map, x, y)] = val + map.__map[{x, y}] = val end +---Set the tile at the specified location +---@param map map_wrapper +---@param x integer +---@param y integer +---@return string function map_mt.__index.get_tile(map, x, y) - return map[loc_to_index(map,x,y)] + return map.__map[{x, y}] end +---Test if a tile is on the board (including the border) +---@param map map_wrapper +---@param x integer +---@param y integer +---@return boolean function map_mt.__index.on_board(map, x, y) - return x >= 0 and y >= 0 and x < map.w and y < map.h + return map.__map:on_board(x, y, true) end +---Test if a tile is on the board (excluding the border) +---@param map map_wrapper +---@param x integer +---@param y integer +---@return boolean function map_mt.__index.on_inner_board(map, x, y) - return x >= 1 and y >= 1 and x < map.w - 1 and y < map.h - 1 + return map.__map:on_board(x, y, false) end +---Add a named location at the given coordinates +---@param map map_wrapper +---@param x integer +---@param y integer +---@param name string function map_mt.__index.add_location(map, x, y, name) - if not map.locations then - map.locations = LS.create() - end - if map.locations:get(x, y) then - table.insert(map.locations:get(x, y), name) - else - map.locations:insert(x, y, {name}) - end + map.__map.special_locations[name] = {x, y} end -function map_mt.__index.flip_x(map) - for y = 0, map.h - 1 do - for x = 0, map.w - 1 do - local i = loc_to_index(map, x, y) - local j = loc_to_index(map, map.w - x - 1, y) - map[i], map[j] = map[j], map[i] +----Flip the map horizontally +---@param map terrain_map +function mapgen_helper.flip_x(map) + for x, y in map:iter(true) do + if x <= map.width / 2 or y <= map.height / 2 then + local x_opp = map.width - x - 1 + map[{x,y}], map[{x_opp,y}] = map[{x_opp,y}], map[{x,y}] end end -end - -function map_mt.__index.flip_y(map) - for x = 0, map.w - 1 do - for y = 0, map.h - 1 do - local i = loc_to_index(map, x, y) - local j = loc_to_index(map, x, map.h - y - 1) - map[i], map[j] = map[j], map[i] - end + for id, loc in map.special_locations do + loc.x = map.width - loc.x - 1 + map.special_locations[id] = loc end end -function map_mt.__index.flip_xy(map) - map:flip_x() - map:flip_y() +---Flip the map vertically +---@param map terrain_map +function mapgen_helper.flip_y(map) + for x, y in map:iter(true) do + if x <= map.width / 2 or y <= map.height / 2 then + local y_opp = map.height - y - 1 + map[{x,y}], map[{x,y_opp}] = map[{x,y_opp}], map[{x,y}] + end + end + for id, loc in map.special_locations do + loc.y = map.height - loc.y - 1 + map.special_locations[id] = loc + end end +---Flip the map diagonally +---@param map terrain_map +function mapgen_helper.flip_xy(map) + for x, y in map:iter(true) do + if x <= map.width / 2 or y <= map.height / 2 then + local x_opp = map.width - x - 1 + local y_opp = map.height - y - 1 + map[{x,y}], map[{x_opp,y_opp}] = map[{x_opp,y_opp}], map[{x,y}] + end + end + for id, loc in map.special_locations do + loc.x = map.width - loc.x - 1 + loc.y = map.height - loc.y - 1 + map.special_locations[id] = loc + end +end + +---Convert to string +---@param map map_wrapper +---@return string function map_mt.__tostring(map) - local map_builder = {} - -- The coordinates are 0-based to match the in-game coordinates - for y = 0, map.h - 1 do - local string_builder = {} - for x = 0, map.w - 1 do - local tile_string = map:get_tile(x, y) - if map.locations and map.locations:get(x,y) then - for i,v in ipairs(map.locations:get(x,y)) do - tile_string = v .. ' ' .. tile_string - end - end - table.insert(string_builder, tile_string) - end - assert(#string_builder == map.w) - table.insert(map_builder, table.concat(string_builder, ', ')) - end - assert(#map_builder == map.h) - return table.concat(map_builder, '\n') + return map.__map.data end local adjacent_offset = { { {0,-1}, {1,-1}, {1,0}, {0,1}, {-1,0}, {-1,-1} }, { {0,-1}, {1,0}, {1,1}, {0,1}, {-1,1}, {-1,0} } } +---Iterates over adjacent tiles +---@param x integer +---@param y integer +---@return fun():integer,integer function mapgen_helper.adjacent_tiles(x, y) local offset = adjacent_offset[2 - (x % 2)] local i = 0 @@ -114,4 +167,84 @@ function mapgen_helper.adjacent_tiles(x, y) end end +---@alias relative_anchor +---|'center' +---|'top-left' +---|'top-middle' +---|'top-right' +---|'bottom-left' +---|'bottom-middle' +---|'bottom-right' +---|'middle-left' +---|'middle-right' + +---@class map_chamber : WMLTable +---@field ignore boolean +---@field id string +---@field x string +---@field y string +---@field terrain_clear string +---@field size integer +---@field jagged integer +---@field chance integer +---@field side integer +---@field relative_to relative_anchor +---@field require_player integer + +---@class map_passage : WMLTable +---@field ignore boolean +---@field id string +---@field place_villages boolean +---@field destination string +---@field terrain_clear string +---@field windiness integer +---@field laziness integer +---@field width integer +---@field jagged integer +---@field chance integer + +---Get a chamber by ID or index from the map generator settings. +---@param params WMLTable +---@param id_or_idx string|integer +---@return map_chamber? +function mapgen_helper.get_chamber(params, id_or_idx) + if type(id_or_idx) == 'number' then + local cfg, i = wml.get_nth_child(params, 'chamber', id_or_idx) + if not cfg then return nil end + return params[i].contents + elseif type(id_or_idx) == 'string' then + local cfg, i = wml.get_child(params, 'chamber', id_or_idx) + if not cfg then return nil end + return params[i].contents + end +end + +---Get a passage by ID or index from the map generator settings. +---@param chamber map_chamber +---@param id_or_idx string|integer +---@return map_passage? +function mapgen_helper.get_passage(chamber, id_or_idx) + if type(id_or_idx) == 'number' then + local cfg, i = wml.get_nth_child(chamber, 'passage', id_or_idx) + if not cfg then return nil end + return chamber[i].contents + elseif type(id_or_idx) == 'string' then + local cfg, i = wml.get_child(chamber, 'passage', id_or_idx) + if not cfg then return nil end + return chamber[i].contents + end +end + +mapgen_helper.create_map = wesnoth.deprecate_api('mapgen_helper.create_map', 'wesnoth.map.create', 1, nil, create_map) +mapgen_helper.adjacent_tiles = wesnoth.deprecate_api('mapgen_helper.adjacent_tiles', 'wesnoth.map.iter_adjacent', 1, nil, mapgen_helper.adjacent_tiles) +map_mt.__index.set_tile = wesnoth.deprecate_api('oldmap:set_tile(x,y,ter)', 'map[{x,y}]=ter', 1, nil, map_mt.__index.set_tile) +map_mt.__index.get_tile = wesnoth.deprecate_api('oldmap:get_tile(x,y)', 'map[{x,y}]', 1, nil, map_mt.__index.get_tile) +map_mt.__index.on_board = wesnoth.deprecate_api('oldmap:on_board(x,y)', 'map:on_board(x,y,true)', 1, nil, map_mt.__index.on_board) +map_mt.__index.on_inner_board = wesnoth.deprecate_api('oldmap:on_inner_board(x,y)', 'map:on_board(x,y,false)', 1, nil, map_mt.__index.on_inner_board) +map_mt.__index.add_location = wesnoth.deprecate_api('oldmap:add_location(x,y,name)', 'map.special_locations[name]={x,y}', 1, nil, map_mt.__index.add_location) +map_mt.__index.flip_x = wesnoth.deprecate_api('oldmap:flip_x()', 'mapgen_helper.flip_x(map)', 1, nil, function(m) mapgen_helper.flip_x(m) end) +map_mt.__index.flip_y = wesnoth.deprecate_api('oldmap:flip_y()', 'mapgen_helper.flip_y(map)', 1, nil, function(m) mapgen_helper.flip_y(m) end) +map_mt.__index.flip_xy = wesnoth.deprecate_api('oldmap:flip_xy()', 'mapgen_helper.flip_xy(map)', 1, nil, function(m) mapgen_helper.flip_xy(m) end) +map_mt.__tostring = wesnoth.deprecate_api('tostring(map)', 'map.data', 1, nil, map_mt.__tostring) + return mapgen_helper diff --git a/data/lua/wml-tags.lua b/data/lua/wml-tags.lua index dd5dd106da6..0a8e62f8ff5 100644 --- a/data/lua/wml-tags.lua +++ b/data/lua/wml-tags.lua @@ -476,8 +476,8 @@ function wml_actions.terrain(cfg) cfg.terrain = nil for i, loc in ipairs(wesnoth.map.find(cfg)) do local replacement = cfg.replace_if_failed - and wesnoth.map.replace_if_failed(terrain, layer) - or wesnoth.map['replace_' .. layer](terrain) + and wesnoth.map.replace.if_failed(terrain, layer) + or wesnoth.map.replace[layer](terrain) wesnoth.current.map[loc] = replacement end end diff --git a/data/multiplayer/gui/cave_map_settings.cfg b/data/multiplayer/gui/cave_map_settings.cfg new file mode 100644 index 00000000000..336520b684f --- /dev/null +++ b/data/multiplayer/gui/cave_map_settings.cfg @@ -0,0 +1,141 @@ +#textdomain wesnoth-lib + +{gui/macros} + +#define MAP_OPTION_CONTROL ID LABEL TYPE WML + [row] + [column] + grow_factor=0 + horizontal_grow=true + border=all + border_size=5 + [label] + definition=default + label={LABEL} + text_alignment=right + [/label] + [/column] + [column] + grow_factor=1 + horizontal_grow=true + border=all + border_size=10 + [{TYPE}] + id={ID} + {WML} + [/{TYPE}] + [/column] + [column] + grow_factor=0 + horizontal_grow=true + border=all + border_size=5 + {GUI_FORCE_WIDGET_MINIMUM_SIZE 100 0 ( + [label] + id={ID}_label + definition=default + [/label] + )} + [/column] + [/row] +#enddef + +[resolution] + definition = "default" + automatic_placement = true + vertical_placement = "center" + horizontal_placement = "center" + maximum_height = 600 + [tooltip] + id=tooltip + [/tooltip] + [helptip] + id=tooltip + [/helptip] + [grid] + [row] + grow_factor=0 + [column] + grow_factor=1 + border=all + border_size=5 + horizontal_alignment=left + [label] + definition=title + label=_"Map Generator Settings" + [/label] + [/column] + [/row] + [row] + grow_factor=1 + [column] + horizontal_grow=true + vertical_grow=true + [grid] + {MAP_OPTION_CONTROL players _"Players:" slider ( + definition=minimal + minimum_value=2 + maximum_value=8 + step_size=1 + )} + {MAP_OPTION_CONTROL width _"Width:" slider ( + definition=minimal + minimum_value=20 + maximum_value=100 + step_size=1 + )} + {MAP_OPTION_CONTROL height _"Height:" slider ( + definition=minimal + minimum_value=20 + maximum_value=100 + step_size=1 + )} + {MAP_OPTION_CONTROL village_density _"Villages:" slider ( + definition=minimal + minimum_value=0 + maximum_value=50 + step_size=1 + )} + #textdomain wesnoth-multiplayer + {MAP_OPTION_CONTROL jagged _"Chamber Jaggedness:" slider ( + definition=minimal + minimum_value=10 + maximum_value=50 + step_size=5 + )} + {MAP_OPTION_CONTROL lake_size _"Lake Size:" slider ( + definition=minimal + minimum_value=5 + maximum_value=50 + step_size=5 + )} + {MAP_OPTION_CONTROL windiness _"Passage Windiness:" slider ( + definition=minimal + minimum_value=1 + maximum_value=20 + step_size=1 + )} + #textdomain wesnoth-lib + {MAP_OPTION_CONTROL roads "" toggle_button ( + definition=checkbox + label=_"Roads Between Castles" + )} + [/grid] + [/column] + [/row] + [row] + grow_factor=0 + [column] + border=all + border_size=5 + horizontal_alignment=right + #textdomain wesnoth-lib + [button] + definition=default + id=ok + label=_"Close" + [/button] + [/column] + [/row] + [/grid] +[/resolution] diff --git a/data/multiplayer/gui/cave_map_settings.lua b/data/multiplayer/gui/cave_map_settings.lua new file mode 100644 index 00000000000..6fd0b8e43e1 --- /dev/null +++ b/data/multiplayer/gui/cave_map_settings.lua @@ -0,0 +1,119 @@ +local params = ... +local MG = wesnoth.require "mapgen_helper" + +local function pre_show(window) + window.players.value = params.nplayers + window.width.value = params.map_width + window.height.value = params.map_height + window.village_density.value = params.village_density + + local central_chamber = MG.get_chamber(params, 'central_chamber') + if central_chamber then + window.jagged.value = central_chamber.jagged + else + error('cave_map_settings requires a [chamber] with id=central_chamber') + end + + local lake = MG.get_chamber(params, 'lake') + if lake then + window.lake_size.value = lake.size + else + error('cave_map_settings requires a [chamber] with id=lake') + end + + local first_player = MG.get_chamber(params, 'player_1') + if first_player then + local tunnel = MG.get_passage(first_player, 1) + if tunnel then + window.windiness.value = tunnel.windiness + else + error('cave_map_settings requires that each player [chamber] contains at least two [passage] tags') + end + + local road = MG.get_passage(first_player, 2) + if road then + window.roads.selected = not road.ignore + else + error('cave_map_settings requires that each player [chamber] contains at least two [passage] tags') + end + else + error('cave_map_settings requires a [chamber] for each player with id=player_n where n is the player number') + end + + local all_players = {first_player} + for i = 2, #params do + local next_player = MG.get_chamber(params, 'player_' .. i) + if next_player then + table.insert(all_players, next_player) + else + break + end + end + + -- Init labels + window.players_label.label = window.players.value + window.width_label.label = window.width.value + window.height_label.label = window.height.value + window.village_density_label.label = window.village_density.value + window.jagged_label.label = window.jagged.value + window.lake_size_label.label = window.lake_size.value + window.windiness_label.label = window.windiness.value + + -- Callbacks... + function window.players.on_modified() + params.nplayers = window.players.value + window.players_label.label = params.nplayers + end + + function window.width.on_modified() + params.map_width = window.width.value + window.width_label.label = params.map_width + central_chamber.size = mathx.round((params.map_height + params.map_width) / 4) + end + + function window.height.on_modified() + params.map_height = window.height.value + window.height_label.label = params.map_height + central_chamber.size = mathx.round((params.map_height + params.map_width) / 4) + end + + function window.village_density.on_modified() + params.village_density = window.village_density.value + -- Need wesnoth-lib for the village density label + local _ = wesnoth.textdomain "wesnoth-lib" + window.village_density_label.label = (_"$villages/1000 tiles"):vformat{villages = params.village_density} + end + + function window.jagged.on_modified() + local val = window.jagged.value + central_chamber.jagged = val + window.jagged_label.label = val + end + + function window.lake_size.on_modified() + local val = window.lake_size.value + lake.size = val + window.lake_size_label.label = val + end + + function window.windiness.on_modified() + local val = window.windiness.value + for i = 1, #all_players do + local tunnel = MG.get_passage(all_players[i], 1) + tunnel.windiness = val + end + window.windiness_label.label = val + end + + function window.roads.on_modified() + local val = not window.roads.selected + for i = 1, #all_players do + local road = MG.get_passage(all_players[i], 2) + road.ignore = val + end + end +end + +local dialog = wml.load "multiplayer/gui/cave_map_settings.cfg" +gui.show_dialog(wml.get_child(dialog, 'resolution'), pre_show) +return params diff --git a/data/multiplayer/scenarios/Random_Scenario_Cave.cfg b/data/multiplayer/scenarios/Random_Scenario_Cave.cfg new file mode 100644 index 00000000000..7c646543135 --- /dev/null +++ b/data/multiplayer/scenarios/Random_Scenario_Cave.cfg @@ -0,0 +1,212 @@ +#textdomain wesnoth-multiplayer + +#define CLEAR_TERRAINS +Rb,Rb,Rb,Rb,Rb,Rb,Rb,Rb^Tf,Rb^Ii,Sm,Sm,Uue,Rb^Fetd,Rb^Fdw#enddef + +#define ROAD_COSTS + [road_cost] + terrain=Rb + cost=2 + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Rb^Tf + cost=20 + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Rb^Fdw + cost=20 + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Uue + cost=10 + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Rb^Ii + cost=2 + convert_to=Ur^Ii + [/road_cost] + [road_cost] + terrain=Uue + cost=50 + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Sm + cost=75 + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Rb^Fetd + cost=75 + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Rb^Fdw + cost=75 + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Wwg + cost=100 + convert_to_bridge=Wwg^Bw|,Wwg^Bw/,Wwg^Bw\ + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Wwf + cost=100 + convert_to_bridge=Wwf^Bw|,Wwf^Bw/,Wwf^Bw\ + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Ur + cost=2 + convert_to=Ur + [/road_cost] + [road_cost] + terrain=Ur^Ii + cost=2 + convert_to=Ur^Ii + [/road_cost] + [road_cost] + terrain=Rb^Vu + cost=5 + convert_to=Ur^Vu + [/road_cost] + [road_cost] + terrain=Rb^Vud + cost=5 + convert_to=Ur^Vud + [/road_cost] + [road_cost] + terrain=Uue^Vud + cost=55 + convert_to=Ur^Vud + [/road_cost] + # We don't want to carve a new tunnel now, + # so give walls a ridiculously high cost. + [road_cost] + terrain=Xue + cost=1000 + convert_to=Uue + [/road_cost] +#enddef + +#define PLAYER_CHAMBER NUMBER X Y REL + [chamber] + id=player_{NUMBER} + require_player={NUMBER} + x={X} + y={Y} + relative_to={REL} + size=5 + jagged=2 + [item_location] + id={NUMBER} + place_castle=yes + [/item_location] + [items] + [side] + side={NUMBER} + [/side] + [/items] + [passage] + destination=central_chamber + windiness=3 + laziness=2 + jagged=3 + width=3 + terrain_clear={CLEAR_TERRAINS} + place_villages=yes + # Try not to plow thru the lake, but if we must, use ford + [road_cost] + terrain=Wwg + cost=100 + convert_to=Wwf + [/road_cost] + [/passage] + [passage] + ignore=no + destination=central_chamber + windiness=2 + jagged=2 + width=2 + {ROAD_COSTS} + [/passage] + [/chamber] +#enddef + +[multiplayer] + # This id is currently hardcoded by the random map generator of the editor + id=multiplayer_Random_Map_Cave + name= _ "Random map (Cave)" + description= _ "A random map set in a cave. Note: random maps are often unbalanced, but if you have time, you can regenerate them until you get a good one." + scenario_generation=lua + [generator] + id="cavegen" + config_name=_"Lua Cave Generator" + create_scenario=<< + return wesnoth.require("lua/cave_map_generator.lua").generate_scenario(...) + >> + user_config=<< + return wesnoth.dofile("multiplayer/gui/cave_map_settings.lua", ...) + >> + [scenario] + name= _ "Random map (Cave)" + id=multiplayer_Random_Map_Cave + {DEFAULT_MUSIC_PLAYLIST} + {DEFAULT_SCHEDULE} + [/scenario] + terrain_clear={CLEAR_TERRAINS} + terrain_wall=Xue + terrain_castle=Co + terrain_keep=Ko + terrain_village=Rb^Vu,Rb^Vu,Rb^Vu,Rb^Vu,Rb^Vu,Rb^Vud,Rb^Vud,Uue^Vud + + map_width=40 + map_height=40 + village_density=25 + nplayers=4 + + [chamber] + id=central_chamber + x=14-26 + y=14-26 + size=20 + jagged=30 + [/chamber] + [chamber] + id=lake + x=14-26 + y=14-26 + size=5 + jagged=12 + terrain_clear=Wwg + # Without this, there is a chance that the lake is entirely disconnected from everything else + [passage] + destination=central_chamber + windiness=5 + laziness=3 + jagged=4 + width=1 + terrain_clear=Wwg + [/passage] + [/chamber] + {PLAYER_CHAMBER 1 3-10 3-10 top-left} + {PLAYER_CHAMBER 2 3-10 3-10 bottom-right} + {PLAYER_CHAMBER 3 3-10 3-10 bottom-left} + {PLAYER_CHAMBER 4 3-10 3-10 top-right} + {PLAYER_CHAMBER 5 3-10 3-10 top-middle} + {PLAYER_CHAMBER 6 3-10 3-10 bottom-middle} + {PLAYER_CHAMBER 7 3-10 3-10 middle-left} + {PLAYER_CHAMBER 8 3-10 3-10 middle-right} + [/generator] +[/multiplayer] + +#undef CLEAR_TERRAINS +#undef ROAD_COSTS +#undef PLAYER_CHAMBER +#undef MAP_OPTION_CONTROL diff --git a/data/schema/core/mapgen/lua.cfg b/data/schema/core/mapgen/lua.cfg index f089944bb50..407e4485b03 100644 --- a/data/schema/core/mapgen/lua.cfg +++ b/data/schema/core/mapgen/lua.cfg @@ -1,22 +1,39 @@ #define CHAMBER_TAG_CONTENTS max=infinite + {DEFAULT_KEY ignore bool no} {SIMPLE_KEY id string} {SIMPLE_KEY x unsigned_range_list} {SIMPLE_KEY y unsigned_range_list} + {SIMPLE_KEY terrain_clear terrain_list} {SIMPLE_KEY size unsigned} {SIMPLE_KEY jagged unsigned} {DEFAULT_KEY chance unsigned 100} {SIMPLE_KEY side unsigned} + {SIMPLE_KEY relative_to relative_anchor} + {SIMPLE_KEY require_player unsigned} [tag] name="passage" max=infinite + {DEFAULT_KEY ignore bool no} + {SIMPLE_KEY id string} + {SIMPLE_KEY place_villages bool} {SIMPLE_KEY destination string} + {SIMPLE_KEY terrain_clear terrain_list} {SIMPLE_KEY windiness unsigned} {SIMPLE_KEY laziness unsigned} {SIMPLE_KEY width unsigned} {SIMPLE_KEY jagged unsigned} {DEFAULT_KEY chance unsigned 100} + [tag] + name="road_cost" + max=infinite + {REQUIRED_KEY terrain terrain_code} + {SIMPLE_KEY cost unsigned} + {SIMPLE_KEY convert_to terrain_code} + # TODO: This is not quite right, it has to be exactly 3 terrain codes (but default generator schema also gets that wrong) + {SIMPLE_KEY convert_to_bridge terrain_list} + [/tag] [/tag] [tag] name="item_location" @@ -47,13 +64,20 @@ {SIMPLE_KEY map_width unsigned} {SIMPLE_KEY map_height unsigned} {SIMPLE_KEY village_density unsigned} + {SIMPLE_KEY nplayers unsigned} {SIMPLE_KEY transform map_transform} {SIMPLE_KEY transform_chance unsigned} {SIMPLE_KEY terrain_wall terrain_code} {SIMPLE_KEY terrain_castle terrain_code} {SIMPLE_KEY terrain_keep terrain_code} - {SIMPLE_KEY terrain_village terrain_code} - {SIMPLE_KEY terrain_clear terrain_code} + {SIMPLE_KEY terrain_village terrain_list} + {SIMPLE_KEY terrain_clear terrain_list} + {LINK_TAG "scenario"} + [/then] + [/if] + [if] + glob_on_create_map=*cave_map_generator* + [then] [tag] name="chamber" {CHAMBER_TAG_CONTENTS} @@ -70,6 +94,11 @@ [tag] name="items" super="scenario" + [tag] + name="event" + super="event" + {SIMPLE_KEY same_location_as_previous bool} + [/tag] [/tag] [/tag] [tag] diff --git a/data/schema/game_config.cfg b/data/schema/game_config.cfg index 38f2f3b6dfb..b4654fdc9b3 100644 --- a/data/schema/game_config.cfg +++ b/data/schema/game_config.cfg @@ -508,6 +508,10 @@ name="theme_action" value="checkbox|radiobox|image|turbo" [/type] + [type] + name="relative_anchor" + value="center|top-(left|middle|right)|bottom-(left|middle|right)|middle-(left|right)" + [/type] [tag] name="root" min=1 diff --git a/src/gui/dialogs/editor/generator_settings.cpp b/src/gui/dialogs/editor/generator_settings.cpp index 48b56dc4328..7ef95a16644 100644 --- a/src/gui/dialogs/editor/generator_settings.cpp +++ b/src/gui/dialogs/editor/generator_settings.cpp @@ -21,6 +21,7 @@ #include "gui/widgets/slider.hpp" #include "gui/widgets/status_label_helper.hpp" #include "gettext.hpp" +#include "formula/string_utils.hpp" #include @@ -67,7 +68,12 @@ void generator_settings::pre_show() // Do this *after* assigning the 'update_*_label_` functions or the game will crash! adjust_minimum_size_by_players(); - gui2::bind_status_label(this, "villages", [](const slider& s) { return t_string(formatter() << s.get_value() << _("/1000 tiles")); }); + gui2::bind_status_label(this, "villages", [](const slider& s) { + std::string val = std::to_string(s.get_value()); + utils::string_map args; + args["villages"] = val; + return t_string(VGETTEXT("$villages/1000 tiles", args)); + }); gui2::bind_status_label(this, "castle_size"); gui2::bind_status_label(this, "landform", [](const slider& s) { return s.get_value() == 0 ? _("Inland") : (s.get_value() < max_coastal ? _("Coastal") : _("Island")); }); diff --git a/src/scripting/lua_terrainmap.cpp b/src/scripting/lua_terrainmap.cpp index 25978beb2d3..a4638be3cd9 100644 --- a/src/scripting/lua_terrainmap.cpp +++ b/src/scripting/lua_terrainmap.cpp @@ -298,6 +298,18 @@ static int impl_terrainmap_get(lua_State *L) luaL_setmetatable(L, maplocationKey); return 1; } + if(strcmp(m, "size") == 0) { + lua_pushinteger(L, tm.total_width()); + lua_pushinteger(L, tm.total_height()); + lua_pushinteger(L, tm.border_size()); + return 3; + } + if(strcmp(m, "playable_size") == 0) { + lua_pushinteger(L, tm.w()); + lua_pushinteger(L, tm.h()); + lua_pushinteger(L, tm.border_size()); + return 3; + } if(luaW_getglobal(L, "wesnoth", "map", m)) { return 1; } diff --git a/src/scripting/mapgen_lua_kernel.cpp b/src/scripting/mapgen_lua_kernel.cpp index 466463a82ec..95b4f79d3c1 100644 --- a/src/scripting/mapgen_lua_kernel.cpp +++ b/src/scripting/mapgen_lua_kernel.cpp @@ -253,11 +253,16 @@ mapgen_lua_kernel::mapgen_lua_kernel(const config* vars) // Map methods { "find", &intf_mg_get_locations }, { "find_in_radius", &intf_mg_get_tiles_radius }, + { "on_board", &intf_on_board }, + { "on_border", &intf_on_border }, + { "iter", &intf_terrainmap_iter }, + { "terrain_mask", &intf_terrain_mask }, // Static functions { "filter", &intf_terrainfilter_create }, { "create", &intf_terrainmap_create }, { "generate_height_map", &intf_default_generate_height_map }, { "generate", &intf_default_generate }, + { "replace_if_failed", &intf_replace_if_failed }, { nullptr, nullptr } }; @@ -302,9 +307,15 @@ void mapgen_lua_kernel::run_generator(const char * prog, const config & generato protected_call(1, 1, std::bind(&lua_kernel_base::throw_exception, this, std::placeholders::_1, std::placeholders::_2)); } -void mapgen_lua_kernel::user_config(const char * prog, const config & generator) +void mapgen_lua_kernel::user_config(const char * prog, config & generator) { run_generator(prog, generator); + if(!lua_isnoneornil(mState, -1) && !luaW_toconfig(mState, -1, generator)) { + std::string msg = "expected a string, found a "; + msg += lua_typename(mState, lua_type(mState, -1)); + lua_pop(mState, 1); + throw game::lua_error(msg.c_str(),"bad return value"); + } } int mapgen_lua_kernel::intf_get_variable(lua_State *L)