From e1233fd0f244c12727bb9be349ee12dd6dac2edc Mon Sep 17 00:00:00 2001 From: Celtic Minstrel Date: Wed, 3 May 2017 02:25:35 -0400 Subject: [PATCH] Split several of the larger WML tags into their own file --- data/lua/wml-tags.lua | 619 +------------------------------- data/lua/wml/heal_unit.lua | 70 ++++ data/lua/wml/kill.lua | 72 ++++ data/lua/wml/modify_ai.lua | 35 ++ data/lua/wml/modify_side.lua | 109 ++++++ data/lua/wml/move_unit.lua | 64 ++++ data/lua/wml/role.lua | 111 ++++++ data/lua/wml/set_variable.lua | 118 ++++++ data/lua/wml/test_condition.lua | 53 +++ 9 files changed, 635 insertions(+), 616 deletions(-) create mode 100644 data/lua/wml/heal_unit.lua create mode 100644 data/lua/wml/kill.lua create mode 100644 data/lua/wml/modify_ai.lua create mode 100644 data/lua/wml/modify_side.lua create mode 100644 data/lua/wml/move_unit.lua create mode 100644 data/lua/wml/role.lua create mode 100644 data/lua/wml/set_variable.lua create mode 100644 data/lua/wml/test_condition.lua diff --git a/data/lua/wml-tags.lua b/data/lua/wml-tags.lua index efb24a3f84f..d3359959fed 100644 --- a/data/lua/wml-tags.lua +++ b/data/lua/wml-tags.lua @@ -13,6 +13,9 @@ end wesnoth.require "wml-flow" wesnoth.require "wml" +-- Note: When adding new WML tags, unless they're very simple, it's preferred to +-- add a new file in the "data/lua/wml" directory. The file will then automatically +-- be loaded by the above require statement. local helper = wesnoth.require "helper" local location_set = wesnoth.require "location_set" @@ -288,58 +291,6 @@ function wml_actions.volume(cfg) end end --- This is mainly for use in unit test macros, but maybe it can be useful elsewhere too -function wml_actions.test_condition(cfg) - local logger = cfg.logger or "warning" - - -- This function returns true if it managed to explain the failure - local function explain(current_cfg, expect) - for i,t in ipairs(current_cfg) do - local tag, this_cfg = t[1], t[2] - -- Some special cases - if tag == "or" or tag == "and" then - if explain(this_cfg, expect) then - return true - end - elseif tag == "not" then - if explain(this_cfg, not expect) then - return true - end - elseif tag == "true" or tag == "false" then - -- We don't explain these ones. - return true - elseif wesnoth.eval_conditional{t} == expect then - local explanation = "The following conditional test %s:" - if expect then - explanation = explanation:format("passed") - else - explanation = explanation:format("failed") - end - explanation = string.format("%s\n\t[%s]", explanation, tag) - for k,v in pairs(this_cfg) do - if type(k) ~= "number" then - local format = "%s\n\t\t%s=%s" - local literal = tostring(helper.literal(this_cfg)[k]) - if literal ~= v then - format = format .. "=%s" - end - explanation = string.format(format, explanation, k, literal, tostring(v)) - end - end - explanation = string.format("%s\n\t[/%s]", explanation, tag) - if tag == "variable" then - explanation = string.format("%s\n\tNote: The variable %s currently has the value %q.", explanation, this_cfg.name, tostring(wesnoth.get_variable(this_cfg.name))) - end - wesnoth.log(logger, explanation, true) - return true - end - end - end - - -- Use not twice here to convert nil to false - explain(cfg, not not cfg.result) -end - function wml_actions.scroll_to(cfg) local loc = wesnoth.get_locations( cfg )[1] if not loc then return end @@ -520,69 +471,6 @@ function wml_actions.unhide_unit(cfg) wml_actions.redraw {} end -function wml_actions.move_unit(cfg) - local coordinate_error = "invalid coordinate in [move_unit]" - local to_x = tostring(cfg.to_x or helper.wml_error(coordinate_error)) - local to_y = tostring(cfg.to_y or helper.wml_error(coordinate_error)) - local fire_event = cfg.fire_event - local muf_force_scroll = cfg.force_scroll - local check_passability = cfg.check_passability - if check_passability == nil then check_passability = true end - cfg = helper.literal(cfg) - cfg.to_x, cfg.to_y, cfg.fire_event = nil, nil, nil - local units = wesnoth.get_units(cfg) - - local pattern = "[^%s,]+" - for current_unit_index, current_unit in ipairs(units) do - if not fire_event or current_unit.valid then - local xs, ys = string.gmatch(to_x, pattern), string.gmatch(to_y, pattern) - local move_string_x = current_unit.x - local move_string_y = current_unit.y - local pass_check = nil - if check_passability then pass_check = current_unit end - - local x, y = xs(), ys() - local prevX, prevY = tonumber(current_unit.x), tonumber(current_unit.y) - while true do - x = tonumber(x) or helper.wml_error(coordinate_error) - y = tonumber(y) or helper.wml_error(coordinate_error) - if not (x == prevX and y == prevY) then x, y = wesnoth.find_vacant_tile(x, y, pass_check) end - if not x or not y then helper.wml_error("Could not find a suitable hex near to one of the target hexes in [move_unit].") end - move_string_x = string.format("%s,%u", move_string_x, x) - move_string_y = string.format("%s,%u", move_string_y, y) - local next_x, next_y = xs(), ys() - if not next_x and not next_y then break end - prevX, prevY = x, y - x, y = next_x, next_y - end - - if current_unit.x < x then current_unit.facing = "se" - elseif current_unit.x > x then current_unit.facing = "sw" - end - - wesnoth.extract_unit(current_unit) - local current_unit_cfg = current_unit.__cfg - wml_actions.move_unit_fake { - type = current_unit_cfg.type, - gender = current_unit_cfg.gender, - variation = current_unit_cfg.variation, - image_mods = current_unit.image_mods, - side = current_unit_cfg.side, - x = move_string_x, - y = move_string_y, - force_scroll = muf_force_scroll - } - local x2, y2 = current_unit.x, current_unit.y - current_unit.x, current_unit.y = x, y - wesnoth.put_unit(current_unit) - - if fire_event then - wesnoth.fire_event("moveto", x, y, x2, y2) - end - end - end -end - function wml_actions.capture_village(cfg) local side = cfg.side local filter_side = helper.get_child(cfg, "filter_side") @@ -651,74 +539,6 @@ function wml_actions.unpetrify(cfg) end end -function wml_actions.heal_unit(cfg) - local healers = helper.get_child(cfg, "filter_second") - if healers then - healers = wesnoth.get_units{ - ability_type = "heals", - T["and"](healers) - } - else - healers = {} - end - - local who = helper.get_child(cfg, "filter") - if who then - who = wesnoth.get_units(who) - else - who = wesnoth.get_units{ - x = wesnoth.current.event_context.x1, - y = wesnoth.current.event_context.y1 - } - end - - local heal_full = cfg.amount == "full" or cfg.amount == nil - local moves_full = cfg.moves == "full" - local heal_amount_set = false - for i,u in ipairs(who) do - local heal_amount = u.max_hitpoints - u.hitpoints - if heal_full then - u.hitpoints = u.max_hitpoints - else - heal_amount = tonumber(cfg.amount) or heal_amount - local new_hitpoints = math.max(1, math.min(u.max_hitpoints, u.hitpoints + heal_amount)) - heal_amount = new_hitpoints - u.hitpoints - u.hitpoints = new_hitpoints - end - - if moves_full then - u.moves = u.max_moves - else - u.moves = math.min(u.max_moves, u.moves + (cfg.moves or 0)) - end - - if cfg.restore_attacks then - u.attacks_left = u.max_attacks - end - - if cfg.restore_statuses == true or cfg.restore_statuses == nil then - u.status.poisoned = false - u.status.petrified = false - u.status.slowed = false - u.status.unhealable = false - end - - if not heal_amount_set then - heal_amount_set = true - wesnoth.set_variable("heal_amount", heal_amount) - end - - if cfg.animate then - local animator = wesnoth.create_animator() - animator:add(u, 'healed', 'hits', {value = heal_amount}) - if #healers > 0 then - animator:add(healers[1], 'healing', 'hits', {value = heal_amount}) - end - animator:run() - end - end -end - function wml_actions.transform_unit(cfg) local transform_to = cfg.transform_to @@ -764,39 +584,6 @@ function wml_actions.store_side(cfg) end end -function wml_actions.modify_ai(cfg) - local sides = utils.get_sides(cfg) - local component, final - if cfg.action == "add" or cfg.action == "change" then - local start = string.find(cfg.path, "[a-z_]+%[[a-z0-9_*]*%]$") - final = start and (string.find(cfg.path, '[', start, true) - 1) or -1 - start = start or string.find(cfg.path, "[^.]*$") or 1 - local comp_type = string.sub(cfg.path, start, final) - component = helper.get_child(cfg, comp_type) - if component == nil then - helper.wml_error("Missing component definition in [modify_ai]") - end - component = helper.parsed(component) - end - for i = 1, #sides do - if cfg.action == "add" then - wesnoth.add_ai_component(sides[i].side, cfg.path, component) - elseif cfg.action == "delete" or cfg.action == "try_delete" then - wesnoth.delete_ai_component(sides[i].side, cfg.path) - elseif cfg.action == "change" then - local id_start = final + 2 - local id_final = string.len(cfg.path) - 1 - local id = string.sub(cfg.path, id_start, id_final) - if id == "*" then - helper.wml_error("[modify_ai] can only change one component at a time") - elseif not component.id and not id:match("[0-9]+") then - component.id = id - end - wesnoth.change_ai_component(sides[i].side, cfg.path, component) - end - end -end - function wml_actions.add_ai_behavior(cfg) local unit = wesnoth.get_units(helper.get_child(cfg, "filter"))[1] or helper.wml_error("[add_ai_behavior]: no unit specified") @@ -978,75 +765,6 @@ function wml_actions.inspect(cfg) wesnoth.gamestate_inspector(cfg) end -local kill_recursion_preventer = location_set.create() -function wml_actions.kill(cfg) - local number_killed = 0 - local secondary_unit = helper.get_child(cfg, "secondary_unit") - local killer_loc = {0, 0} - if secondary_unit then - secondary_unit = wesnoth.get_units(secondary_unit)[1] - if cfg.fire_event then - if secondary_unit then - killer_loc = {secondary_unit.loc} - else - wesnoth.log("warn", "failed to match [secondary_unit] in [kill] with a single on-board unit") - end - end - end - local dead_men_walking = wesnoth.get_units(cfg) - for i,unit in ipairs(dead_men_walking) do - local death_loc = {x = tonumber(unit.x) or 0, y = tonumber(unit.y) or 0} - if not secondary_unit then killer_loc = death_loc end - local can_fire = false - - local recursion = (kill_recursion_preventer:get(death_loc.x, death_loc.y) or 0) + 1 - if cfg.fire_event then - kill_recursion_preventer:insert(death_loc.x, death_loc.y, recursion) - can_fire = true - if death_loc.x == wesnoth.current.event.x1 and death_loc.y == wesnoth.current.event.y1 then - if wesnoth.current.event.name == "die" or wesnoth.current.event.name == "last breath" then - if recursion >= 10 then - can_fire = false; - wesnoth.log("error", "tried to fire 'die' or 'last breath' event on unit from the unit's 'die' or 'last breath' event with first_time_only=no!") - end - end - end - end - if can_fire then - wesnoth.fire_event("last breath", death_loc, killer_loc) - end - if cfg.animate then - wesnoth.scroll_to_tile(death_loc) - local anim = wesnoth.create_animator() - -- Possible TODO: Add weapon selection? (That's kinda a pain right now; see animate_unit defn) - anim:add(unit, "death", "kill") - if secondary_unit then - anim:add(secondary_unit, "victory", "kill") - end - anim:run() - end - wml_actions.redraw{} - - if can_fire then - wesnoth.fire_event("die", death_loc, killer_loc) - end - if cfg.fire_event then - if recursion <= 1 then - kill_recursion_preventer:remove(death_loc.x, death_loc.y) - else - kill_recursion_preventer:insert(death_loc.x, death_loc.y, recursion) - end - end - -- Test that it's valid (and still on the map) first, in case the event erased (or extracted) it. - if unit.valid == "map" then unit:erase() end - - number_killed = number_killed + 1 - end - - -- TODO: Do I need to check recall lists or was that covered by the above loop? - return number_killed -end - function wml_actions.label( cfg ) local new_cfg = helper.parsed( cfg ) for index, location in ipairs( wesnoth.get_locations( cfg ) ) do @@ -1055,111 +773,6 @@ function wml_actions.label( cfg ) end end -local side_changes_needing_redraw = { - 'shroud', 'fog', 'reset_map', 'reset_view', 'shroud_data', - 'share_vision', 'share_maps', 'share_view', - 'color', 'flag', -} -function wml_actions.modify_side(cfg) - local sides = utils.get_sides(cfg) - for i,side in ipairs(sides) do - if cfg.team_name then - side.team_name = cfg.team_name - end - if cfg.user_team_name then - side.user_team_name = cfg.user_team_name - end - if cfg.controller then - side.controller = cfg.controller - end - if cfg.defeat_condition then - side.defeat_condition = cfg.defeat_condition - end - if cfg.recruit then - local recruits = {} - for recruit in utils.split(cfg.recruit) do - table.insert(recruits, recruit) - end - side.recruit = recruits - end - if cfg.village_support then - side.village_support = cfg.village_support - end - if cfg.village_gold then - side.village_gold = cfg.village_gold - end - if cfg.income then - side.base_income = cfg.income - end - if cfg.gold then - side.gold = cfg.gold - end - - if cfg.hidden ~= nil then - side.hidden = cfg.hidden - end - if cfg.color or cfg.flag then - wesnoth.set_side_id(side.side, cfg.flag, cfg.color) - end - if cfg.flag_icon then - side.flag_icon = cfg.flag_icon - end - if cfg.suppress_end_turn_confirmation ~= nil then - side.suppress_end_turn_confirmation = cfg.suppress_end_turn_confirmation - end - if cfg.scroll_to_leader ~= nil then - side.scroll_to_leader = cfg.scroll_to_leader - end - - if cfg.shroud ~= nil then - side.shroud = cfg.shroud - end - if cfg.reset_maps then - wesnoth.remove_shroud(side.side, "all") - end - if cfg.fog ~= nil then - side.fog = cfg.fog - end - if cfg.reset_view then - wesnoth.add_fog(side.side, {}, true) - end - if cfg.shroud_data then - wesnoth.remove_shroud(side.side, cfg.shroud_data) - end - - if cfg.share_vision then - side.share_vision = cfg.share_vision - end - -- Legacy support - if cfg.share_view ~= nil or cfg.share_maps ~= nil then - if cfg.share_view then - side.share_vision = 'all' - elseif cfg.share_maps then - side.share_vision = 'shroud' - else - side.share_vision = 'none' - end - end - - if cfg.switch_ai then - wesnoth.switch_ai(side.side, cfg.switch_ai) - end - local ai = {} - for next_ai in helper.child_range(cfg, "ai") do - table.insert(ai, T.ai(next_ai)) - end - if #ai > 0 then - wesnoth.append_ai(side.side, ai) - end - end - for i,key in ipairs(side_changes_needing_redraw) do - if cfg[key] ~= nil then - wml_actions.redraw{} - return - end - end -end - function wml_actions.open_help(cfg) wesnoth.open_help(cfg.topic) end @@ -1179,115 +792,6 @@ function wml_actions.print(cfg) wesnoth.print(cfg) end -function wml_actions.role(cfg) - -- role= and type= are handled differently than in other tags, - -- so we need to remove them from the filter - local role = cfg.role - local filter = helper.shallow_literal(cfg) - - if role == nil then - helper.wml_error("missing role= in [role]") - end - - local types = {} - - if cfg.type then - for value in utils.split(cfg.type) do - table.insert(types, utils.trim(value)) - end - end - - filter.role, filter.type = nil, nil - local search_map, search_recall, reassign = true, true, true - if cfg.search_recall_list == "only" then - search_map = false - elseif cfg.search_recall_list ~= nil then - search_recall = not not cfg.search_recall_list - end - if cfg.reassign ~= nil then - reassign = not not cfg.reassign - end - - -- pre-build a new [recall] from the [auto_recall] - -- copy only recall-specific attributes, no SUF at all - -- the SUF will be id= which we will add in a moment - -- keep this in sync with the C++ recall function!!! - local recall = nil - local child = helper.get_child(cfg, "auto_recall") - if child ~= nil then - if helper.get_nth_child(cfg, "auto_recall", 2) ~= nil then - wesnoth.log("debug", "More than one [auto_recall] found within [role]", true) - end - local original = helper.shallow_literal(child) - recall = {} - recall.x = original.x - recall.y = original.y - recall.show = original.show - recall.fire_event = original.fire_event - recall.check_passability = original.check_passability - end - - if not reassign then - if search_map then - local unit = wesnoth.get_units{role=role}[1] - if unit then - return - end - end - if recall and search_recall then - local unit = wesnoth.get_recall_units{role=role}[1] - if unit then - recall.id = unit.id - wml_actions.recall(recall) - return - end - end - end - - if search_map then - -- first attempt to match units on the map - local i = 1 - repeat - -- give precedence based on the order specified in type= - if #types > 0 then - filter.type = types[i] - end - local unit = wesnoth.get_units(filter)[1] - if unit then - unit.role = role - return - end - i = i + 1 - until #types == 0 or i > #types - end - - if search_recall then - -- then try to match units on the recall lists - i = 1 - repeat - if #types > 0 then - filter.type = types[i] - end - local unit = wesnoth.get_recall_units(filter)[1] - if unit then - unit.role = role - if recall then - recall.id = unit.id - wml_actions.recall(recall) - end - return - end - i = i + 1 - until #types == 0 or i > #types - end - - -- no matching unit found, try the [else] tags - for else_child in helper.child_range(cfg, "else") do - local action = utils.handle_event_commands(else_child, "conditional") - if action ~= "none" then return end - end -end - function wml_actions.unsynced(cfg) wesnoth.unsynced(function () wml_actions.command(cfg) @@ -1389,123 +893,6 @@ function wml_actions.reset_fog(cfg) end end -function wml_actions.set_variable(cfg) - local name = cfg.name or helper.wml_error "trying to set a variable with an empty name" - - if cfg.value ~= nil then -- check for nil because user may try to set a variable as false - wesnoth.set_variable(name, cfg.value) - end - - if cfg.literal ~= nil then - wesnoth.set_variable(name, helper.shallow_literal(cfg).literal) - end - - if cfg.to_variable then - wesnoth.set_variable(name, wesnoth.get_variable(cfg.to_variable)) - end - - if cfg.add then - wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) + (tonumber(cfg.add) or 0)) - end - - if cfg.sub then - wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) - (tonumber(cfg.sub) or 0)) - end - - if cfg.multiply then - wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) * (tonumber(cfg.multiply) or 0)) - end - - if cfg.divide then - local divide = tonumber(cfg.divide) or 0 - if divide == 0 then helper.wml_error("division by zero on variable " .. name) end - wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) / divide) - end - - if cfg.modulo then - local modulo = tonumber(cfg.modulo) or 0 - if modulo == 0 then helper.wml_error("division by zero on variable " .. name) end - wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) % modulo) - end - - if cfg.abs then - wesnoth.set_variable(name, math.abs(tonumber(wesnoth.get_variable(name)) or 0)) - end - - if cfg.root then - if cfg.root == "square" then - local radicand = tonumber(wesnoth.get_variable(name)) or 0 - if radicand < 0 then helper.wml_error("square root of negative number on variable " .. name) end - wesnoth.set_variable(name, math.sqrt(radicand)) - end - end - - if cfg.power then - wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) ^ (tonumber(cfg.power) or 0)) - end - - if cfg.round then - local var = tonumber(wesnoth.get_variable(name) or 0) - local round_val = cfg.round - if round_val == "ceil" then - wesnoth.set_variable(name, math.ceil(var)) - elseif round_val == "floor" then - wesnoth.set_variable(name, math.floor(var)) - else - local decimals, discarded = math.modf(tonumber(round_val) or 0) - local value = var * (10 ^ decimals) - value = helper.round(value) - value = value * (10 ^ -decimals) - wesnoth.set_variable(name, value) - end - end - - -- unlike the other math operations, ipart and fpart do not act on - -- the value already contained in the variable - -- but on the value assigned to the respective key - if cfg.ipart then - local ivalue, fvalue = math.modf(tonumber(cfg.ipart) or 0) - wesnoth.set_variable(name, ivalue) - end - - if cfg.fpart then - local ivalue, fvalue = math.modf(tonumber(cfg.fpart) or 0) - wesnoth.set_variable(name, fvalue) - end - - if cfg.string_length ~= nil then - wesnoth.set_variable(name, string.len(tostring(cfg.string_length))) - end - - if cfg.time then - if cfg.time == "stamp" then - wesnoth.set_variable(name, wesnoth.get_time_stamp()) - end - end - - if cfg.rand then - wesnoth.set_variable(name, helper.rand(tostring(cfg.rand))) - end - - local join_child = helper.get_child(cfg, "join") - if join_child then - local array_name = join_child.variable or helper.wml_error "missing variable= attribute in [join]" - local separator = join_child.separator - local key_name = join_child.key or "value" - local remove_empty = join_child.remove_empty - - local string_to_join = {} - - for i, element in ipairs(helper.get_variable_array(array_name)) do - if element[key_name] ~= nil or (not remove_empty) then - table.insert(string_to_join, tostring(element[key_name])) - end - end - - wesnoth.set_variable(name, table.concat(string_to_join, separator)) - end -end - function wesnoth.wml_actions.change_theme(cfg) wesnoth.game_config.theme = cfg.theme end diff --git a/data/lua/wml/heal_unit.lua b/data/lua/wml/heal_unit.lua new file mode 100644 index 00000000000..d80a082594d --- /dev/null +++ b/data/lua/wml/heal_unit.lua @@ -0,0 +1,70 @@ +local helper = wesnoth.require "helper" +local T = helper.set_wml_tag_metatable {} + +function wesnoth.wml_actions.heal_unit(cfg) + local healers = helper.get_child(cfg, "filter_second") + if healers then + healers = wesnoth.get_units{ + ability_type = "heals", + T["and"](healers) + } + else + healers = {} + end + + local who = helper.get_child(cfg, "filter") + if who then + who = wesnoth.get_units(who) + else + who = wesnoth.get_units{ + x = wesnoth.current.event_context.x1, + y = wesnoth.current.event_context.y1 + } + end + + local heal_full = cfg.amount == "full" or cfg.amount == nil + local moves_full = cfg.moves == "full" + local heal_amount_set = false + for i,u in ipairs(who) do + local heal_amount = u.max_hitpoints - u.hitpoints + if heal_full then + u.hitpoints = u.max_hitpoints + else + heal_amount = tonumber(cfg.amount) or heal_amount + local new_hitpoints = math.max(1, math.min(u.max_hitpoints, u.hitpoints + heal_amount)) + heal_amount = new_hitpoints - u.hitpoints + u.hitpoints = new_hitpoints + end + + if moves_full then + u.moves = u.max_moves + else + u.moves = math.min(u.max_moves, u.moves + (cfg.moves or 0)) + end + + if cfg.restore_attacks then + u.attacks_left = u.max_attacks + end + + if cfg.restore_statuses == true or cfg.restore_statuses == nil then + u.status.poisoned = false + u.status.petrified = false + u.status.slowed = false + u.status.unhealable = false + end + + if not heal_amount_set then + heal_amount_set = true + wesnoth.set_variable("heal_amount", heal_amount) + end + + if cfg.animate then + local animator = wesnoth.create_animator() + animator:add(u, 'healed', 'hits', {value = heal_amount}) + if #healers > 0 then + animator:add(healers[1], 'healing', 'hits', {value = heal_amount}) + end + animator:run() + end + end +end diff --git a/data/lua/wml/kill.lua b/data/lua/wml/kill.lua new file mode 100644 index 00000000000..e3a321cd8b3 --- /dev/null +++ b/data/lua/wml/kill.lua @@ -0,0 +1,72 @@ +local helper = wesnoth.require "helper" +local location_set = wesnoth.require "location_set" + +local kill_recursion_preventer = location_set.create() + +function wesnoth.wml_actions.kill(cfg) + local number_killed = 0 + local secondary_unit = helper.get_child(cfg, "secondary_unit") + local killer_loc = {0, 0} + if secondary_unit then + secondary_unit = wesnoth.get_units(secondary_unit)[1] + if cfg.fire_event then + if secondary_unit then + killer_loc = {secondary_unit.loc} + else + wesnoth.log("warn", "failed to match [secondary_unit] in [kill] with a single on-board unit") + end + end + end + local dead_men_walking = wesnoth.get_units(cfg) + for i,unit in ipairs(dead_men_walking) do + local death_loc = {x = tonumber(unit.x) or 0, y = tonumber(unit.y) or 0} + if not secondary_unit then killer_loc = death_loc end + local can_fire = false + + local recursion = (kill_recursion_preventer:get(death_loc.x, death_loc.y) or 0) + 1 + if cfg.fire_event then + kill_recursion_preventer:insert(death_loc.x, death_loc.y, recursion) + can_fire = true + if death_loc.x == wesnoth.current.event.x1 and death_loc.y == wesnoth.current.event.y1 then + if wesnoth.current.event.name == "die" or wesnoth.current.event.name == "last breath" then + if recursion >= 10 then + can_fire = false; + wesnoth.log("error", "tried to fire 'die' or 'last breath' event on unit from the unit's 'die' or 'last breath' event with first_time_only=no!") + end + end + end + end + if can_fire then + wesnoth.fire_event("last breath", death_loc, killer_loc) + end + if cfg.animate then + wesnoth.scroll_to_tile(death_loc) + local anim = wesnoth.create_animator() + -- Possible TODO: Add weapon selection? (That's kinda a pain right now; see animate_unit defn) + anim:add(unit, "death", "kill") + if secondary_unit then + anim:add(secondary_unit, "victory", "kill") + end + anim:run() + end + wesnoth.wml_actions.redraw{} + + if can_fire then + wesnoth.fire_event("die", death_loc, killer_loc) + end + if cfg.fire_event then + if recursion <= 1 then + kill_recursion_preventer:remove(death_loc.x, death_loc.y) + else + kill_recursion_preventer:insert(death_loc.x, death_loc.y, recursion) + end + end + -- Test that it's valid (and still on the map) first, in case the event erased (or extracted) it. + if unit.valid == "map" then unit:erase() end + + number_killed = number_killed + 1 + end + + -- TODO: Do I need to check recall lists or was that covered by the above loop? + return number_killed +end diff --git a/data/lua/wml/modify_ai.lua b/data/lua/wml/modify_ai.lua new file mode 100644 index 00000000000..da4e90f501a --- /dev/null +++ b/data/lua/wml/modify_ai.lua @@ -0,0 +1,35 @@ +local helper = wesnoth.require "helper" +local utils = wesnoth.require "wml-utils" + +function wesnoth.wml_actions.modify_ai(cfg) + local sides = utils.get_sides(cfg) + local component, final + if cfg.action == "add" or cfg.action == "change" then + local start = string.find(cfg.path, "[a-z_]+%[[a-z0-9_*]*%]$") + final = start and (string.find(cfg.path, '[', start, true) - 1) or -1 + start = start or string.find(cfg.path, "[^.]*$") or 1 + local comp_type = string.sub(cfg.path, start, final) + component = helper.get_child(cfg, comp_type) + if component == nil then + helper.wml_error("Missing component definition in [modify_ai]") + end + component = helper.parsed(component) + end + for i = 1, #sides do + if cfg.action == "add" then + wesnoth.add_ai_component(sides[i].side, cfg.path, component) + elseif cfg.action == "delete" or cfg.action == "try_delete" then + wesnoth.delete_ai_component(sides[i].side, cfg.path) + elseif cfg.action == "change" then + local id_start = final + 2 + local id_final = string.len(cfg.path) - 1 + local id = string.sub(cfg.path, id_start, id_final) + if id == "*" then + helper.wml_error("[modify_ai] can only change one component at a time") + elseif not component.id and not id:match("[0-9]+") then + component.id = id + end + wesnoth.change_ai_component(sides[i].side, cfg.path, component) + end + end +end diff --git a/data/lua/wml/modify_side.lua b/data/lua/wml/modify_side.lua new file mode 100644 index 00000000000..efd175a7eb4 --- /dev/null +++ b/data/lua/wml/modify_side.lua @@ -0,0 +1,109 @@ +local helper = wesnoth.require "helper" +local utils = wesnoth.require "wml-utils" +local T = helper.set_wml_tag_metatable {} + +local side_changes_needing_redraw = { + 'shroud', 'fog', 'reset_map', 'reset_view', 'shroud_data', + 'share_vision', 'share_maps', 'share_view', + 'color', 'flag', +} + +function wesnoth.wml_actions.modify_side(cfg) + local sides = utils.get_sides(cfg) + for i,side in ipairs(sides) do + if cfg.team_name then + side.team_name = cfg.team_name + end + if cfg.user_team_name then + side.user_team_name = cfg.user_team_name + end + if cfg.controller then + side.controller = cfg.controller + end + if cfg.defeat_condition then + side.defeat_condition = cfg.defeat_condition + end + if cfg.recruit then + local recruits = {} + for recruit in utils.split(cfg.recruit) do + table.insert(recruits, recruit) + end + side.recruit = recruits + end + if cfg.village_support then + side.village_support = cfg.village_support + end + if cfg.village_gold then + side.village_gold = cfg.village_gold + end + if cfg.income then + side.base_income = cfg.income + end + if cfg.gold then + side.gold = cfg.gold + end + + if cfg.hidden ~= nil then + side.hidden = cfg.hidden + end + if cfg.color or cfg.flag then + wesnoth.set_side_id(side.side, cfg.flag, cfg.color) + end + if cfg.flag_icon then + side.flag_icon = cfg.flag_icon + end + if cfg.suppress_end_turn_confirmation ~= nil then + side.suppress_end_turn_confirmation = cfg.suppress_end_turn_confirmation + end + if cfg.scroll_to_leader ~= nil then + side.scroll_to_leader = cfg.scroll_to_leader + end + + if cfg.shroud ~= nil then + side.shroud = cfg.shroud + end + if cfg.reset_maps then + wesnoth.remove_shroud(side.side, "all") + end + if cfg.fog ~= nil then + side.fog = cfg.fog + end + if cfg.reset_view then + wesnoth.add_fog(side.side, {}, true) + end + if cfg.shroud_data then + wesnoth.remove_shroud(side.side, cfg.shroud_data) + end + + if cfg.share_vision then + side.share_vision = cfg.share_vision + end + -- Legacy support + if cfg.share_view ~= nil or cfg.share_maps ~= nil then + if cfg.share_view then + side.share_vision = 'all' + elseif cfg.share_maps then + side.share_vision = 'shroud' + else + side.share_vision = 'none' + end + end + + if cfg.switch_ai then + wesnoth.switch_ai(side.side, cfg.switch_ai) + end + local ai = {} + for next_ai in helper.child_range(cfg, "ai") do + table.insert(ai, T.ai(next_ai)) + end + if #ai > 0 then + wesnoth.append_ai(side.side, ai) + end + end + for i,key in ipairs(side_changes_needing_redraw) do + if cfg[key] ~= nil then + wesnoth.wml_actions.redraw{} + return + end + end +end diff --git a/data/lua/wml/move_unit.lua b/data/lua/wml/move_unit.lua new file mode 100644 index 00000000000..f160bd80747 --- /dev/null +++ b/data/lua/wml/move_unit.lua @@ -0,0 +1,64 @@ +local helper = wesnoth.require "helper" + +function wesnoth.wml_actions.move_unit(cfg) + local coordinate_error = "invalid coordinate in [move_unit]" + local to_x = tostring(cfg.to_x or helper.wml_error(coordinate_error)) + local to_y = tostring(cfg.to_y or helper.wml_error(coordinate_error)) + local fire_event = cfg.fire_event + local muf_force_scroll = cfg.force_scroll + local check_passability = cfg.check_passability + if check_passability == nil then check_passability = true end + cfg = helper.literal(cfg) + cfg.to_x, cfg.to_y, cfg.fire_event = nil, nil, nil + local units = wesnoth.get_units(cfg) + + local pattern = "[^%s,]+" + for current_unit_index, current_unit in ipairs(units) do + if not fire_event or current_unit.valid then + local xs, ys = string.gmatch(to_x, pattern), string.gmatch(to_y, pattern) + local move_string_x = current_unit.x + local move_string_y = current_unit.y + local pass_check = nil + if check_passability then pass_check = current_unit end + + local x, y = xs(), ys() + local prevX, prevY = tonumber(current_unit.x), tonumber(current_unit.y) + while true do + x = tonumber(x) or helper.wml_error(coordinate_error) + y = tonumber(y) or helper.wml_error(coordinate_error) + if not (x == prevX and y == prevY) then x, y = wesnoth.find_vacant_tile(x, y, pass_check) end + if not x or not y then helper.wml_error("Could not find a suitable hex near to one of the target hexes in [move_unit].") end + move_string_x = string.format("%s,%u", move_string_x, x) + move_string_y = string.format("%s,%u", move_string_y, y) + local next_x, next_y = xs(), ys() + if not next_x and not next_y then break end + prevX, prevY = x, y + x, y = next_x, next_y + end + + if current_unit.x < x then current_unit.facing = "se" + elseif current_unit.x > x then current_unit.facing = "sw" + end + + wesnoth.extract_unit(current_unit) + local current_unit_cfg = current_unit.__cfg + wesnoth.wml_actions.move_unit_fake { + type = current_unit_cfg.type, + gender = current_unit_cfg.gender, + variation = current_unit_cfg.variation, + image_mods = current_unit.image_mods, + side = current_unit_cfg.side, + x = move_string_x, + y = move_string_y, + force_scroll = muf_force_scroll + } + local x2, y2 = current_unit.x, current_unit.y + current_unit.x, current_unit.y = x, y + wesnoth.put_unit(current_unit) + + if fire_event then + wesnoth.fire_event("moveto", x, y, x2, y2) + end + end + end +end diff --git a/data/lua/wml/role.lua b/data/lua/wml/role.lua new file mode 100644 index 00000000000..70df6a97555 --- /dev/null +++ b/data/lua/wml/role.lua @@ -0,0 +1,111 @@ +local helper = wesnoth.require "helper" +local utils = wesnoth.require "wml-utils" + +function wesnoth.wml_actions.role(cfg) + -- role= and type= are handled differently than in other tags, + -- so we need to remove them from the filter + local role = cfg.role + local filter = helper.shallow_literal(cfg) + + if role == nil then + helper.wml_error("missing role= in [role]") + end + + local types = {} + + if cfg.type then + for value in utils.split(cfg.type) do + table.insert(types, utils.trim(value)) + end + end + + filter.role, filter.type = nil, nil + local search_map, search_recall, reassign = true, true, true + if cfg.search_recall_list == "only" then + search_map = false + elseif cfg.search_recall_list ~= nil then + search_recall = not not cfg.search_recall_list + end + if cfg.reassign ~= nil then + reassign = not not cfg.reassign + end + + -- pre-build a new [recall] from the [auto_recall] + -- copy only recall-specific attributes, no SUF at all + -- the SUF will be id= which we will add in a moment + -- keep this in sync with the C++ recall function!!! + local recall = nil + local child = helper.get_child(cfg, "auto_recall") + if child ~= nil then + if helper.get_nth_child(cfg, "auto_recall", 2) ~= nil then + wesnoth.log("debug", "More than one [auto_recall] found within [role]", true) + end + local original = helper.shallow_literal(child) + recall = {} + recall.x = original.x + recall.y = original.y + recall.show = original.show + recall.fire_event = original.fire_event + recall.check_passability = original.check_passability + end + + if not reassign then + if search_map then + local unit = wesnoth.get_units{role=role}[1] + if unit then + return + end + end + if recall and search_recall then + local unit = wesnoth.get_recall_units{role=role}[1] + if unit then + recall.id = unit.id + wesnoth.wml_actions.recall(recall) + return + end + end + end + + if search_map then + -- first attempt to match units on the map + local i = 1 + repeat + -- give precedence based on the order specified in type= + if #types > 0 then + filter.type = types[i] + end + local unit = wesnoth.get_units(filter)[1] + if unit then + unit.role = role + return + end + i = i + 1 + until #types == 0 or i > #types + end + + if search_recall then + -- then try to match units on the recall lists + i = 1 + repeat + if #types > 0 then + filter.type = types[i] + end + local unit = wesnoth.get_recall_units(filter)[1] + if unit then + unit.role = role + if recall then + recall.id = unit.id + wesnoth.wml_actions.recall(recall) + end + return + end + i = i + 1 + until #types == 0 or i > #types + end + + -- no matching unit found, try the [else] tags + for else_child in helper.child_range(cfg, "else") do + local action = utils.handle_event_commands(else_child, "conditional") + if action ~= "none" then return end + end +end diff --git a/data/lua/wml/set_variable.lua b/data/lua/wml/set_variable.lua new file mode 100644 index 00000000000..8926402c5b0 --- /dev/null +++ b/data/lua/wml/set_variable.lua @@ -0,0 +1,118 @@ +local helper = wesnoth.require "helper" + +function wesnoth.wml_actions.set_variable(cfg) + local name = cfg.name or helper.wml_error "trying to set a variable with an empty name" + + if cfg.value ~= nil then -- check for nil because user may try to set a variable as false + wesnoth.set_variable(name, cfg.value) + end + + if cfg.literal ~= nil then + wesnoth.set_variable(name, helper.shallow_literal(cfg).literal) + end + + if cfg.to_variable then + wesnoth.set_variable(name, wesnoth.get_variable(cfg.to_variable)) + end + + if cfg.add then + wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) + (tonumber(cfg.add) or 0)) + end + + if cfg.sub then + wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) - (tonumber(cfg.sub) or 0)) + end + + if cfg.multiply then + wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) * (tonumber(cfg.multiply) or 0)) + end + + if cfg.divide then + local divide = tonumber(cfg.divide) or 0 + if divide == 0 then helper.wml_error("division by zero on variable " .. name) end + wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) / divide) + end + + if cfg.modulo then + local modulo = tonumber(cfg.modulo) or 0 + if modulo == 0 then helper.wml_error("division by zero on variable " .. name) end + wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) % modulo) + end + + if cfg.abs then + wesnoth.set_variable(name, math.abs(tonumber(wesnoth.get_variable(name)) or 0)) + end + + if cfg.root then + if cfg.root == "square" then + local radicand = tonumber(wesnoth.get_variable(name)) or 0 + if radicand < 0 then helper.wml_error("square root of negative number on variable " .. name) end + wesnoth.set_variable(name, math.sqrt(radicand)) + end + end + + if cfg.power then + wesnoth.set_variable(name, (tonumber(wesnoth.get_variable(name)) or 0) ^ (tonumber(cfg.power) or 0)) + end + + if cfg.round then + local var = tonumber(wesnoth.get_variable(name) or 0) + local round_val = cfg.round + if round_val == "ceil" then + wesnoth.set_variable(name, math.ceil(var)) + elseif round_val == "floor" then + wesnoth.set_variable(name, math.floor(var)) + else + local decimals, discarded = math.modf(tonumber(round_val) or 0) + local value = var * (10 ^ decimals) + value = helper.round(value) + value = value * (10 ^ -decimals) + wesnoth.set_variable(name, value) + end + end + + -- unlike the other math operations, ipart and fpart do not act on + -- the value already contained in the variable + -- but on the value assigned to the respective key + if cfg.ipart then + local ivalue, fvalue = math.modf(tonumber(cfg.ipart) or 0) + wesnoth.set_variable(name, ivalue) + end + + if cfg.fpart then + local ivalue, fvalue = math.modf(tonumber(cfg.fpart) or 0) + wesnoth.set_variable(name, fvalue) + end + + if cfg.string_length ~= nil then + wesnoth.set_variable(name, string.len(tostring(cfg.string_length))) + end + + if cfg.time then + if cfg.time == "stamp" then + wesnoth.set_variable(name, wesnoth.get_time_stamp()) + end + end + + if cfg.rand then + wesnoth.set_variable(name, helper.rand(tostring(cfg.rand))) + end + + local join_child = helper.get_child(cfg, "join") + if join_child then + local array_name = join_child.variable or helper.wml_error "missing variable= attribute in [join]" + local separator = join_child.separator + local key_name = join_child.key or "value" + local remove_empty = join_child.remove_empty + + local string_to_join = {} + + for i, element in ipairs(helper.get_variable_array(array_name)) do + if element[key_name] ~= nil or (not remove_empty) then + table.insert(string_to_join, tostring(element[key_name])) + end + end + + wesnoth.set_variable(name, table.concat(string_to_join, separator)) + end +end diff --git a/data/lua/wml/test_condition.lua b/data/lua/wml/test_condition.lua new file mode 100644 index 00000000000..77248c0e5b8 --- /dev/null +++ b/data/lua/wml/test_condition.lua @@ -0,0 +1,53 @@ +local helper = wesnoth.require "helper" + +-- This function returns true if it managed to explain the failure +local function explain(current_cfg, expect) + for i,t in ipairs(current_cfg) do + local tag, this_cfg = t[1], t[2] + -- Some special cases + if tag == "or" or tag == "and" then + if explain(this_cfg, expect) then + return true + end + elseif tag == "not" then + if explain(this_cfg, not expect) then + return true + end + elseif tag == "true" or tag == "false" then + -- We don't explain these ones. + return true + elseif wesnoth.eval_conditional{t} == expect then + local explanation = "The following conditional test %s:" + if expect then + explanation = explanation:format("passed") + else + explanation = explanation:format("failed") + end + explanation = string.format("%s\n\t[%s]", explanation, tag) + for k,v in pairs(this_cfg) do + if type(k) ~= "number" then + local format = "%s\n\t\t%s=%s" + local literal = tostring(helper.literal(this_cfg)[k]) + if literal ~= v then + format = format .. "=%s" + end + explanation = string.format(format, explanation, k, literal, tostring(v)) + end + end + explanation = string.format("%s\n\t[/%s]", explanation, tag) + if tag == "variable" then + explanation = string.format("%s\n\tNote: The variable %s currently has the value %q.", explanation, this_cfg.name, tostring(wesnoth.get_variable(this_cfg.name))) + end + wesnoth.log(logger, explanation, true) + return true + end + end +end + +-- This is mainly for use in unit test macros, but maybe it can be useful elsewhere too +function wesnoth.wml_actions.test_condition(cfg) + local logger = cfg.logger or "warning" + + -- Use not twice here to convert nil to false + explain(cfg, not not cfg.result) +end