diff --git a/data/lua/wml-flow.lua b/data/lua/wml-flow.lua new file mode 100644 index 000000000000..84e8e45d2ce9 --- /dev/null +++ b/data/lua/wml-flow.lua @@ -0,0 +1,257 @@ + + +function wml_actions.command(cfg) + utils.handle_event_commands(cfg, "plain") +end + +-- we can't create functions with names that are Lua keywords (eg if, while) +-- instead, we store the following anonymous functions directly into +-- the table, using the [] operator, rather than by using the point syntax + +wml_actions["if"] = function(cfg) + if not (helper.get_child(cfg, 'then') or helper.get_child(cfg, 'elseif') or helper.get_child(cfg, 'else')) then + helper.wml_error("[if] didn't find any [then], [elseif], or [else] children.") + end + + if wesnoth.eval_conditional(cfg) then -- evaluate [if] tag + for then_child in helper.child_range(cfg, "then") do + local action = utils.handle_event_commands(then_child, "conditional") + if action ~= "none" then break end + end + return -- stop after executing [then] tags + end + + for elseif_child in helper.child_range(cfg, "elseif") do + if wesnoth.eval_conditional(elseif_child) then -- we'll evaluate the [elseif] tags one by one + for then_tag in helper.child_range(elseif_child, "then") do + local action = utils.handle_event_commands(then_tag, "conditional") + if action ~= "none" then break end + end + return -- stop on first matched condition + end + end + + -- no matched condition, 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 break end + end +end + +wml_actions["while"] = function( cfg ) + if helper.child_count(cfg, "do") == 0 then + helper.wml_error "[while] does not contain any [do] tags" + end + + -- execute [do] up to 65536 times + for i = 1, 65536 do + if wesnoth.eval_conditional( cfg ) then + for do_child in helper.child_range( cfg, "do" ) do + local action = utils.handle_event_commands(do_child, "loop") + if action == "break" then + utils.set_exiting("none") + return + elseif action == "continue" then + utils.set_exiting("none") + break + elseif action ~= "none" then + return + end + end + else return end + end +end + +wml_actions["break"] = function(cfg) + utils.set_exiting("break") +end + +wml_actions["return"] = function(cfg) + utils.set_exiting("return") +end + +function wml_actions.continue(cfg) + utils.set_exiting("continue") +end + +wesnoth.wml_actions["for"] = function(cfg) + if helper.child_count(cfg, "do") == 0 then + helper.wml_error "[for] does not contain any [do] tags" + end + + local loop_lim = {} + local first + if cfg.array ~= nil then + if cfg.reverse then + first = wesnoth.get_variable(cfg.array .. ".length") - 1 + loop_lim.last = 0 + loop_lim.step = -1 + else + first = 0 + loop_lim.last = '$($' .. cfg.array .. ".length - 1)" + loop_lim.step = 1 + end + else + -- Get a literal config to fetch end and step; + -- this done is to delay expansion of variables + local cfg_lit = helper.literal(cfg) + first = cfg.start or 0 + loop_lim.last = cfg_lit["end"] or first + if cfg.step then loop_lim.step = cfg_lit.step end + end + loop_lim = wesnoth.tovconfig(loop_lim) + if loop_lim.step == 0 then -- Sanity check + helper.wml_error("[for] has a step of 0!") + end + if (first < loop_lim.last and loop_lim.step <= 0) or (first > loop_lim.last and loop_lim.step >= 0) then + -- Sanity check: If they specify something like start,end,step=1,4,-1 + -- then we do nothing + return + end + local i_var = cfg.variable or "i" + local save_i = utils.start_var_scope(i_var) + wesnoth.set_variable(i_var, first) + local function loop_condition() + local sentinel = loop_lim.last + if loop_lim.step then + sentinel = sentinel + loop_lim.step + elseif loop_lim.last < first then + sentinel = sentinel - 1 + else + sentinel = sentinel + 1 + end + if loop_lim.step > 0 then + return wesnoth.get_variable(i_var) < sentinel + else + return wesnoth.get_variable(i_var) > sentinel + end + end + while loop_condition() do + for do_child in helper.child_range( cfg, "do" ) do + local action = utils.handle_event_commands(do_child, "loop") + if action == "break" then + utils.set_exiting("none") + goto exit + elseif action == "continue" then + utils.set_exiting("none") + break + elseif action ~= "none" then + goto exit + end + end + wesnoth.set_variable(i_var, wesnoth.get_variable(i_var) + loop_lim.step) + end + ::exit:: + utils.end_var_scope(i_var, save_i) +end + +wml_actions["repeat"] = function(cfg) + if helper.child_count(cfg, "do") == 0 then + helper.wml_error "[repeat] does not contain any [do] tags" + end + + local times = cfg.times or 1 + for i = 1, times do + for do_child in helper.child_range( cfg, "do" ) do + local action = utils.handle_event_commands(do_child, "loop") + if action == "break" then + utils.set_exiting("none") + return + elseif action == "continue" then + utils.set_exiting("none") + break + elseif action ~= "none" then + return + end + end + end +end + +function wml_actions.foreach(cfg) + if helper.child_count(cfg, "do") == 0 then + helper.wml_error "[foreach] does not contain any [do] tags" + end + + local array_name = cfg.variable or helper.wml_error "[foreach] missing required variable= attribute" + local array = helper.get_variable_array(array_name) + if #array == 0 then return end -- empty and scalars unwanted + local item_name = cfg.item_var or "this_item" + local this_item = utils.start_var_scope(item_name) -- if this_item is already set + local i_name = cfg.index_var or "i" + local i = utils.start_var_scope(i_name) -- if i is already set + local array_length = wesnoth.get_variable(array_name .. ".length") + + for index, value in ipairs(array) do + -- Some protection against external modification + -- It's not perfect, though - it'd be nice if *any* change could be detected + if array_length ~= wesnoth.get_variable(array_name .. ".length") then + helper.wml_error("WML array length changed during [foreach] iteration") + end + wesnoth.set_variable(item_name, value) + -- set index variable + wesnoth.set_variable(i_name, index-1) -- here -1, because of WML array + -- perform actions + for do_child in helper.child_range(cfg, "do") do + local action = utils.handle_event_commands(do_child, "loop") + if action == "break" then + utils.set_exiting("none") + goto exit + elseif action == "continue" then + utils.set_exiting("none") + break + elseif action ~= "none" then + goto exit + end + end + -- set back the content, in case the author made some modifications + if not cfg.readonly then + array[index] = wesnoth.get_variable(item_name) + end + end + ::exit:: + + -- house cleaning + utils.end_var_scope(item_name, this_item) + utils.end_var_scope(i_name, i) + + --[[ + This forces the readonly key to be taken literally. + + If readonly=yes, then this line guarantees that the array + is unchanged after the [foreach] loop ends. + + If readonly=no, then this line updates the array with any + changes the user has applied through the $this_item + variable (or whatever variable was given in item_var). + + Note that altering the array via indexing (with the index_var) + is not supported; any such changes will be reverted by this line. + ]] + helper.set_variable_array(array_name, array) +end + +function wml_actions.switch(cfg) + local var_value = wesnoth.get_variable(cfg.variable) + local found = false + + -- Execute all the [case]s where the value matches. + for v in helper.child_range(cfg, "case") do + for w in utils.split(v.value) do + if w == tostring(var_value) then + local action = utils.handle_event_commands(v, "switch") + found = true + if action ~= "none" then goto exit end + break + end + end + end + ::exit:: + + -- Otherwise execute [else] statements. + if not found then + for v in helper.child_range(cfg, "else") do + local action = utils.handle_event_commands(v, "switch") + if action ~= "none" then break end + end + end +end \ No newline at end of file diff --git a/data/lua/wml-tags.lua b/data/lua/wml-tags.lua index 21c1b1a78001..da547ad42dfb 100644 --- a/data/lua/wml-tags.lua +++ b/data/lua/wml-tags.lua @@ -11,10 +11,16 @@ function wesnoth.game_events.on_save() return {} end +wesnoth.require "lua/wml-flow.lua" wesnoth.require "lua/wml/objectives.lua" wesnoth.require "lua/wml/items.lua" wesnoth.require "lua/wml/message.lua" wesnoth.require "lua/wml/object.lua" +wesnoth.require "lua/wml/modify_unit.lua" +wesnoth.require "lua/wml/harm_unit.lua" +wesnoth.require "lua/wml/find_path.lua" +wesnoth.require "lua/wml/endlevel.lua" +wesnoth.require "lua/wml/random_placement.lua" local helper = wesnoth.require "lua/helper.lua" local location_set = wesnoth.require "lua/location_set.lua" @@ -260,262 +266,6 @@ function wml_actions.music(cfg) wesnoth.set_music(cfg) end -function wml_actions.command(cfg) - utils.handle_event_commands(cfg, "plain") -end - --- we can't create functions with names that are Lua keywords (eg if, while) --- instead, we store the following anonymous functions directly into --- the table, using the [] operator, rather than by using the point syntax - -wml_actions["if"] = function(cfg) - if not (helper.get_child(cfg, 'then') or helper.get_child(cfg, 'elseif') or helper.get_child(cfg, 'else')) then - helper.wml_error("[if] didn't find any [then], [elseif], or [else] children.") - end - - if wesnoth.eval_conditional(cfg) then -- evaluate [if] tag - for then_child in helper.child_range(cfg, "then") do - local action = utils.handle_event_commands(then_child, "conditional") - if action ~= "none" then break end - end - return -- stop after executing [then] tags - end - - for elseif_child in helper.child_range(cfg, "elseif") do - if wesnoth.eval_conditional(elseif_child) then -- we'll evaluate the [elseif] tags one by one - for then_tag in helper.child_range(elseif_child, "then") do - local action = utils.handle_event_commands(then_tag, "conditional") - if action ~= "none" then break end - end - return -- stop on first matched condition - end - end - - -- no matched condition, 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 break end - end -end - -wml_actions["while"] = function( cfg ) - if helper.child_count(cfg, "do") == 0 then - helper.wml_error "[while] does not contain any [do] tags" - end - - -- execute [do] up to 65536 times - for i = 1, 65536 do - if wesnoth.eval_conditional( cfg ) then - for do_child in helper.child_range( cfg, "do" ) do - local action = utils.handle_event_commands(do_child, "loop") - if action == "break" then - utils.set_exiting("none") - return - elseif action == "continue" then - utils.set_exiting("none") - break - elseif action ~= "none" then - return - end - end - else return end - end -end - -wml_actions["break"] = function(cfg) - utils.set_exiting("break") -end - -wml_actions["return"] = function(cfg) - utils.set_exiting("return") -end - -function wml_actions.continue(cfg) - utils.set_exiting("continue") -end - -wesnoth.wml_actions["for"] = function(cfg) - if helper.child_count(cfg, "do") == 0 then - helper.wml_error "[for] does not contain any [do] tags" - end - - local loop_lim = {} - local first - if cfg.array ~= nil then - if cfg.reverse then - first = wesnoth.get_variable(cfg.array .. ".length") - 1 - loop_lim.last = 0 - loop_lim.step = -1 - else - first = 0 - loop_lim.last = '$($' .. cfg.array .. ".length - 1)" - loop_lim.step = 1 - end - else - -- Get a literal config to fetch end and step; - -- this done is to delay expansion of variables - local cfg_lit = helper.literal(cfg) - first = cfg.start or 0 - loop_lim.last = cfg_lit["end"] or first - if cfg.step then loop_lim.step = cfg_lit.step end - end - loop_lim = wesnoth.tovconfig(loop_lim) - if loop_lim.step == 0 then -- Sanity check - helper.wml_error("[for] has a step of 0!") - end - if (first < loop_lim.last and loop_lim.step <= 0) or (first > loop_lim.last and loop_lim.step >= 0) then - -- Sanity check: If they specify something like start,end,step=1,4,-1 - -- then we do nothing - return - end - local i_var = cfg.variable or "i" - local save_i = utils.start_var_scope(i_var) - wesnoth.set_variable(i_var, first) - local function loop_condition() - local sentinel = loop_lim.last - if loop_lim.step then - sentinel = sentinel + loop_lim.step - elseif loop_lim.last < first then - sentinel = sentinel - 1 - else - sentinel = sentinel + 1 - end - if loop_lim.step > 0 then - return wesnoth.get_variable(i_var) < sentinel - else - return wesnoth.get_variable(i_var) > sentinel - end - end - while loop_condition() do - for do_child in helper.child_range( cfg, "do" ) do - local action = utils.handle_event_commands(do_child, "loop") - if action == "break" then - utils.set_exiting("none") - goto exit - elseif action == "continue" then - utils.set_exiting("none") - break - elseif action ~= "none" then - goto exit - end - end - wesnoth.set_variable(i_var, wesnoth.get_variable(i_var) + loop_lim.step) - end - ::exit:: - utils.end_var_scope(i_var, save_i) -end - -wml_actions["repeat"] = function(cfg) - if helper.child_count(cfg, "do") == 0 then - helper.wml_error "[repeat] does not contain any [do] tags" - end - - local times = cfg.times or 1 - for i = 1, times do - for do_child in helper.child_range( cfg, "do" ) do - local action = utils.handle_event_commands(do_child, "loop") - if action == "break" then - utils.set_exiting("none") - return - elseif action == "continue" then - utils.set_exiting("none") - break - elseif action ~= "none" then - return - end - end - end -end - -function wml_actions.foreach(cfg) - if helper.child_count(cfg, "do") == 0 then - helper.wml_error "[foreach] does not contain any [do] tags" - end - - local array_name = cfg.variable or helper.wml_error "[foreach] missing required variable= attribute" - local array = helper.get_variable_array(array_name) - if #array == 0 then return end -- empty and scalars unwanted - local item_name = cfg.item_var or "this_item" - local this_item = utils.start_var_scope(item_name) -- if this_item is already set - local i_name = cfg.index_var or "i" - local i = utils.start_var_scope(i_name) -- if i is already set - local array_length = wesnoth.get_variable(array_name .. ".length") - - for index, value in ipairs(array) do - -- Some protection against external modification - -- It's not perfect, though - it'd be nice if *any* change could be detected - if array_length ~= wesnoth.get_variable(array_name .. ".length") then - helper.wml_error("WML array length changed during [foreach] iteration") - end - wesnoth.set_variable(item_name, value) - -- set index variable - wesnoth.set_variable(i_name, index-1) -- here -1, because of WML array - -- perform actions - for do_child in helper.child_range(cfg, "do") do - local action = utils.handle_event_commands(do_child, "loop") - if action == "break" then - utils.set_exiting("none") - goto exit - elseif action == "continue" then - utils.set_exiting("none") - break - elseif action ~= "none" then - goto exit - end - end - -- set back the content, in case the author made some modifications - if not cfg.readonly then - array[index] = wesnoth.get_variable(item_name) - end - end - ::exit:: - - -- house cleaning - utils.end_var_scope(item_name, this_item) - utils.end_var_scope(i_name, i) - - --[[ - This forces the readonly key to be taken literally. - - If readonly=yes, then this line guarantees that the array - is unchanged after the [foreach] loop ends. - - If readonly=no, then this line updates the array with any - changes the user has applied through the $this_item - variable (or whatever variable was given in item_var). - - Note that altering the array via indexing (with the index_var) - is not supported; any such changes will be reverted by this line. - ]] - helper.set_variable_array(array_name, array) -end - -function wml_actions.switch(cfg) - local var_value = wesnoth.get_variable(cfg.variable) - local found = false - - -- Execute all the [case]s where the value matches. - for v in helper.child_range(cfg, "case") do - for w in utils.split(v.value) do - if w == tostring(var_value) then - local action = utils.handle_event_commands(v, "switch") - found = true - if action ~= "none" then goto exit end - break - end - end - end - ::exit:: - - -- Otherwise execute [else] statements. - if not found then - for v in helper.child_range(cfg, "else") do - local action = utils.handle_event_commands(v, "switch") - if action ~= "none" then break end - end - 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" @@ -748,77 +498,6 @@ function wml_actions.unhide_unit(cfg) wml_actions.redraw {} end -function wml_actions.modify_unit(cfg) - local unit_variable = "LUA_modify_unit" - - local function handle_attributes(cfg, unit_path, toplevel) - for current_key, current_value in pairs(helper.shallow_parsed(cfg)) do - if type(current_value) ~= "table" and (not toplevel or current_key ~= "type") then - wesnoth.set_variable(string.format("%s.%s", unit_path, current_key), current_value) - end - end - end - - local function handle_child(cfg, unit_path) - local children_handled = {} - local cfg = helper.shallow_parsed(cfg) - handle_attributes(cfg, unit_path) - - for current_index, current_table in ipairs(cfg) do - local current_tag = current_table[1] - local tag_index = children_handled[current_tag] or 0 - handle_child(current_table[2], string.format("%s.%s[%u]", - unit_path, current_tag, tag_index)) - children_handled[current_tag] = tag_index + 1 - end - end - - local filter = helper.get_child(cfg, "filter") or helper.wml_error "[modify_unit] missing required [filter] tag" - local function handle_unit(unit_num) - local children_handled = {} - local unit_path = string.format("%s[%u]", unit_variable, unit_num) - local this_unit = wesnoth.get_variable(unit_path) - wesnoth.set_variable("this_unit", this_unit) - handle_attributes(cfg, unit_path, true) - - for current_index, current_table in ipairs(helper.shallow_parsed(cfg)) do - local current_tag = current_table[1] - if current_tag == "filter" then - -- nothing - elseif current_tag == "object" or current_tag == "trait" or current_tag == "advancement" then - local unit = wesnoth.get_variable(unit_path) - unit = wesnoth.create_unit(unit) - wesnoth.add_modification(unit, current_tag, current_table[2]) - unit = unit.__cfg; - wesnoth.set_variable(unit_path, unit) - else - local tag_index = children_handled[current_tag] or 0 - handle_child(current_table[2], string.format("%s.%s[%u]", - unit_path, current_tag, tag_index)) - children_handled[current_tag] = tag_index + 1 - end - end - - if cfg.type then - if cfg.type ~= "" then wesnoth.set_variable(unit_path .. ".advances_to", cfg.type) end - wesnoth.set_variable(unit_path .. ".experience", wesnoth.get_variable(unit_path .. ".max_experience")) - end - wml_actions.kill({ id = this_unit.id, animate = false }) - wml_actions.unstore_unit { variable = unit_path } - end - - wml_actions.store_unit { {"filter", filter}, variable = unit_variable } - local max_index = wesnoth.get_variable(unit_variable .. ".length") - 1 - - local this_unit = utils.start_var_scope("this_unit") - for current_unit = 0, max_index do - handle_unit(current_unit) - end - utils.end_var_scope("this_unit", this_unit) - - wesnoth.set_variable(unit_variable) -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)) @@ -949,207 +628,6 @@ function wml_actions.unpetrify(cfg) end end -function wml_actions.harm_unit(cfg) - local filter = helper.get_child(cfg, "filter") or helper.wml_error("[harm_unit] missing required [filter] tag") - -- we need to use shallow_literal field, to avoid raising an error if $this_unit (not yet assigned) is used - if not helper.shallow_literal(cfg).amount then helper.wml_error("[harm_unit] has missing required amount= attribute") end - local variable = cfg.variable -- kept out of the way to avoid problems - local _ = wesnoth.textdomain "wesnoth" - -- #textdomain wesnoth - local harmer - - local function toboolean( value ) -- helper for animate fields - -- units will be animated upon leveling or killing, even - -- with special attacker and defender values - if value then return true - else return false end - end - - local this_unit = utils.start_var_scope("this_unit") - - for index, unit_to_harm in ipairs(wesnoth.get_units(filter)) do - if unit_to_harm.valid then - -- block to support $this_unit - wesnoth.set_variable ( "this_unit" ) -- clearing this_unit - wesnoth.set_variable("this_unit", unit_to_harm.__cfg) -- cfg field needed - local amount = tonumber(cfg.amount) - local animate = cfg.animate -- attacker and defender are special values - local delay = cfg.delay or 500 - local kill = cfg.kill - local fire_event = cfg.fire_event - local primary_attack = helper.get_child(cfg, "primary_attack") - local secondary_attack = helper.get_child(cfg, "secondary_attack") - local harmer_filter = helper.get_child(cfg, "filter_second") - local experience = cfg.experience - local resistance_multiplier = tonumber(cfg.resistance_multiplier) or 1 - if harmer_filter then harmer = wesnoth.get_units(harmer_filter)[1] end - -- end of block to support $this_unit - - if animate then - if animate ~= "defender" and harmer and harmer.valid then - wesnoth.scroll_to_tile(harmer.x, harmer.y, true) - wml_actions.animate_unit { - flag = "attack", - hits = true, - with_bars = true, - T.filter { id = harmer.id }, - T.primary_attack ( primary_attack ), - T.secondary_attack ( secondary_attack ), - T.facing { x = unit_to_harm.x, y = unit_to_harm.y }, - } - end - wesnoth.scroll_to_tile(unit_to_harm.x, unit_to_harm.y, true) - end - - -- the two functions below are taken straight from the C++ engine, utils.cpp and actions.cpp, with a few unuseful parts removed - -- may be moved in helper.lua in 1.11 - local function round_damage( base_damage, bonus, divisor ) - local rounding - if base_damage == 0 then return 0 - else - if bonus < divisor or divisor == 1 then - rounding = divisor / 2 - 0 - else - rounding = divisor / 2 - 1 - end - return math.max( 1, math.floor( ( base_damage * bonus + rounding ) / divisor ) ) - end - end - - local function calculate_damage( base_damage, alignment, tod_bonus, resistance, modifier ) - local damage_multiplier = 100 - if alignment == "lawful" then - damage_multiplier = damage_multiplier + tod_bonus - elseif alignment == "chaotic" then - damage_multiplier = damage_multiplier - tod_bonus - elseif alignment == "liminal" then - damage_multiplier = damage_multiplier - math.abs( tod_bonus ) - else -- neutral, do nothing - end - local resistance_modified = resistance * modifier - damage_multiplier = damage_multiplier * resistance_modified - local damage = round_damage( base_damage, damage_multiplier, 10000 ) -- if harmer.status.slowed, this may be 20000 ? - return damage - end - - local damage = calculate_damage( - amount, - cfg.alignment or "neutral", - wesnoth.get_time_of_day( { unit_to_harm.x, unit_to_harm.y, true } ).lawful_bonus, - wesnoth.unit_resistance( unit_to_harm, cfg.damage_type or "dummy" ), - resistance_multiplier - ) - - if unit_to_harm.hitpoints <= damage then - if kill == false then damage = unit_to_harm.hitpoints - 1 - else damage = unit_to_harm.hitpoints - end - end - - unit_to_harm.hitpoints = unit_to_harm.hitpoints - damage - local text = string.format("%d%s", damage, "\n") - local add_tab = false - local gender = unit_to_harm.__cfg.gender - - local function set_status(name, male_string, female_string, sound) - if not cfg[name] or unit_to_harm.status[name] then return end - if gender == "female" then - text = string.format("%s%s%s", text, tostring(female_string), "\n") - else - text = string.format("%s%s%s", text, tostring(male_string), "\n") - end - - unit_to_harm.status[name] = true - add_tab = true - - if animate and sound then -- for unhealable, that has no sound - wesnoth.play_sound (sound) - end - end - - if not unit_to_harm.status.unpoisonable then - set_status("poisoned", _"poisoned", _"female^poisoned", "poison.ogg") - end - set_status("slowed", _"slowed", _"female^slowed", "slowed.wav") - set_status("petrified", _"petrified", _"female^petrified", "petrified.ogg") - set_status("unhealable", _"unhealable", _"female^unhealable") - - -- Extract unit and put it back to update animation if status was changed - wesnoth.extract_unit(unit_to_harm) - wesnoth.put_unit(unit_to_harm) - - if add_tab then - text = string.format("%s%s", "\t", text) - end - - if animate and animate ~= "attacker" then - if harmer and harmer.valid then - wml_actions.animate_unit { - flag = "defend", - hits = true, - with_bars = true, - T.filter { id = unit_to_harm.id }, - T.primary_attack ( primary_attack ), - T.secondary_attack ( secondary_attack ), - T.facing { x = harmer.x, y = harmer.y }, - } - else - wml_actions.animate_unit { - flag = "defend", - hits = true, - with_bars = true, - T.filter { id = unit_to_harm.id }, - T.primary_attack ( primary_attack ), - T.secondary_attack ( secondary_attack ), - } - end - end - - wesnoth.float_label( unit_to_harm.x, unit_to_harm.y, string.format( "%s", text ) ) - - local function calc_xp( level ) -- to calculate the experience in case of kill - if level == 0 then return 4 - else return level * 8 end - end - - if experience ~= false and harmer and harmer.valid and wesnoth.is_enemy( unit_to_harm.side, harmer.side ) then -- no XP earned for harming friendly units - if kill ~= false and unit_to_harm.hitpoints <= 0 then - harmer.experience = harmer.experience + calc_xp( unit_to_harm.__cfg.level ) - else - unit_to_harm.experience = unit_to_harm.experience + harmer.__cfg.level - harmer.experience = harmer.experience + unit_to_harm.__cfg.level - end - end - - if kill ~= false and unit_to_harm.hitpoints <= 0 then - wml_actions.kill { id = unit_to_harm.id, animate = toboolean( animate ), fire_event = fire_event } - end - - if animate then - wesnoth.delay(delay) - end - - if variable then - wesnoth.set_variable(string.format("%s[%d]", variable, index - 1), { harm_amount = damage }) - end - - -- both units may no longer be alive at this point, so double check - if experience ~= false and unit_to_harm and unit_to_harm.valid then - unit_to_harm:advance(toboolean(animate), fire_event ~= false) - end - - if experience ~= false and harmer and harmer.valid then - harmer:advance(toboolean(animate), fire_event ~= false) - end - end - - wml_actions.redraw {} - end - - wesnoth.set_variable ( "this_unit" ) -- clearing this_unit - utils.end_var_scope("this_unit", this_unit) -end - function wml_actions.heal_unit(cfg) wesnoth.heal_unit(cfg) end @@ -1255,103 +733,6 @@ function wml_actions.add_ai_behavior(cfg) } end -function wml_actions.find_path(cfg) - local filter_unit = helper.get_child(cfg, "traveler") or helper.wml_error("[find_path] missing required [traveler] tag") - -- only the first unit matching - local unit = wesnoth.get_units(filter_unit)[1] or helper.wml_error("[find_path]'s filter didn't match any unit") - local filter_location = helper.get_child(cfg, "destination") or helper.wml_error( "[find_path] missing required [destination] tag" ) - -- support for $this_unit - local this_unit = utils.start_var_scope("this_unit") - - wesnoth.set_variable ( "this_unit" ) -- clearing this_unit - wesnoth.set_variable("this_unit", unit.__cfg) -- cfg field needed - - local variable = cfg.variable or "path" - local ignore_units = false - local ignore_teleport = false - - if cfg.check_zoc == false then --if we do not want to check the ZoCs, we must ignore units - ignore_units = true - end - if cfg.check_teleport == false then --if we do not want to check teleport, we must ignore it - ignore_teleport = true - end - - local allow_multiple_turns = cfg.allow_multiple_turns - local viewing_side - - if not cfg.check_visibility then viewing_side = 0 end -- if check_visiblity then shroud is taken in account - - local locations = wesnoth.get_locations(filter_location) -- only the location with the lowest distance and lowest movement cost will match. If there will still be more than 1, only the 1st maching one. - local max_cost = nil - if not allow_multiple_turns then max_cost = unit.moves end --to avoid wrong calculation on already moved units - local current_distance, current_cost = math.huge, math.huge - local current_location = {} - - local width,heigth,border = wesnoth.get_map_size() -- data for test below - - for index, location in ipairs(locations) do - -- we test if location passed to pathfinder is invalid (border); if is, do nothing, do not return and continue the cycle - if location[1] == 0 or location[1] == ( width + 1 ) or location[2] == 0 or location[2] == ( heigth + 1 ) then - else - local distance = helper.distance_between ( unit.x, unit.y, location[1], location[2] ) - -- if we pass an unreachable locations an high value will be returned - local path, cost = wesnoth.find_path( unit, location[1], location[2], { max_cost = max_cost, ignore_units = ignore_units, ignore_teleport = ignore_teleport, viewing_side = viewing_side } ) - - if ( distance < current_distance and cost <= current_cost ) or ( cost < current_cost and distance <= current_distance ) then -- to avoid changing the hex with one with less distance and more cost, or vice versa - current_distance = distance - current_cost = cost - current_location = location - end - end - end - - if #current_location == 0 then wesnoth.message("WML warning","[find_path]'s filter didn't match any location") - else - local path, cost = wesnoth.find_path( unit, current_location[1], current_location[2], { max_cost = max_cost, ignore_units = ignore_units, ignore_teleport = ignore_teleport, viewing_side = viewing_side } ) - local turns - - if cost == 0 then -- if location is the same, of course it doesn't cost any MP - turns = 0 - else - turns = math.ceil( ( ( cost - unit.moves ) / unit.max_moves ) + 1 ) - end - - if cost >= 42424242 then -- it's the high value returned for unwalkable or busy terrains - wesnoth.set_variable ( string.format("%s", variable), { hexes = 0 } ) -- set only length, nil all other values - -- support for $this_unit - wesnoth.set_variable ( "this_unit" ) -- clearing this_unit - utils.end_var_scope("this_unit", this_unit) - return end - - if not allow_multiple_turns and turns > 1 then -- location cannot be reached in one turn - wesnoth.set_variable ( string.format("%s", variable), { hexes = 0 } ) - -- support for $this_unit - wesnoth.set_variable ( "this_unit" ) -- clearing this_unit - utils.end_var_scope("this_unit", this_unit) - return end -- skip the cycles below - - wesnoth.set_variable ( string.format( "%s", variable ), { hexes = current_distance, from_x = unit.x, from_y = unit.y, to_x = current_location[1], to_y = current_location[2], movement_cost = cost, required_turns = turns } ) - - for index, path_loc in ipairs(path) do - local sub_path, sub_cost = wesnoth.find_path( unit, path_loc[1], path_loc[2], { max_cost = max_cost, ignore_units = ignore_units, ignore_teleport = ignore_teleport, viewing_side = viewing_side } ) - local sub_turns - - if sub_cost == 0 then - sub_turns = 0 - else - sub_turns = math.ceil( ( ( sub_cost - unit.moves ) / unit.max_moves ) + 1 ) - end - - wesnoth.set_variable ( string.format( "%s.step[%d]", variable, index - 1 ), { x = path_loc[1], y = path_loc[2], terrain = wesnoth.get_terrain( path_loc[1], path_loc[2] ), movement_cost = sub_cost, required_turns = sub_turns } ) -- this structure takes less space in the inspection window - end - end - - -- support for $this_unit - wesnoth.set_variable ( "this_unit" ) -- clearing this_unit - utils.end_var_scope("this_unit", this_unit) -end - function wml_actions.store_starting_location(cfg) local writer = utils.vwriter.init(cfg, "location") for possibly_wrong_index, side in ipairs(wesnoth.get_sides(cfg)) do @@ -1459,98 +840,6 @@ function wml_actions.end_turn(cfg) wesnoth.end_turn() end -function wml_actions.endlevel(cfg) - local parsed = helper.parsed(cfg) - if wesnoth.check_end_level_disabled() then - wesnoth.message("Repeated [endlevel] execution, ignoring") - return - end - - local next_scenario = cfg.next_scenario - if next_scenario then - wesnoth.set_next_scenario(next_scenario) - end - - local end_text = cfg.end_text - local end_text_duration = cfg.end_text_duration - if end_text or end_text_duration then - wesnoth.set_end_campaign_text(end_text or "", end_text_duration) - end - - local end_credits = cfg.end_credits - if end_credits ~= nil then - wesnoth.set_end_campaign_credits(end_credits) - end - - local side_results = {} - for result in helper.child_range(parsed, "result") do - local side = result.side or helper.wml_error("[result] in [endlevel] missing required side= key") - side_results[side] = result - end - local there_is_a_human_victory = false - local there_is_a_human_defeat = false - local there_is_a_local_human_victory = false - local there_is_a_local_human_defeat = false - local bool_int = function(b) - if b == true then - return 1 - elseif b == false then - return 0 - else - return b - end - end - for k,v in ipairs(wesnoth.sides) do - local side_result = side_results[v.side] or {} - local victory_or_defeat = side_result.result or cfg.result or "victory" - local victory = victory_or_defeat == "victory" - if victory_or_defeat ~= "victory" and victory_or_defeat ~= "defeat" then - return helper.wml_error("invalid result= key in [endlevel] '" .. victory_or_defeat .."'") - end - if v.controller == "human" or v.controller == "network" then - if victory then - there_is_a_human_victory = true - else - there_is_a_human_defeat = true - end - end - if v.controller == "human" then - if victory then - there_is_a_local_human_victory = true - else - there_is_a_local_human_defeat = true - end - end - if side_result.bonus ~= nil then - v.carryover_bonus = bool_int(side_result.bonus) - elseif cfg.bonus ~= nil then - v.carryover_bonus = bool_int(cfg.bonus) - end - if side_result.carryover_add ~= nil then - v.carryover_add = side_result.carryover_add - elseif cfg.carryover_add ~= nil then - v.carryover_add = cfg.carryover_add - end - if side_result.carryover_percentage ~= nil then - v.carryover_percentage = side_result.carryover_percentage - elseif cfg.carryover_percentage ~= nil then - v.carryover_percentage = cfg.carryover_percentage - end - end - local proceed_to_next_level = there_is_a_human_victory or (not there_is_a_human_defeat and cfg.result ~= "defeat") - local victory = there_is_a_local_human_victory or (not there_is_a_local_human_defeat and proceed_to_next_level) - wesnoth.end_level { - music = cfg.music, - carryover_report = cfg.carryover_report, - save = cfg.save, - replay_save = cfg.replay_save, - linger_mode = cfg.linger_mode, - reveal_map = cfg.reveal_map, - proceed_to_next_level = proceed_to_next_level, - result = victory and "victory" or "defeat", - } -end - function wml_actions.event(cfg) if cfg.remove then wml_actions.remove_event(cfg) @@ -1661,79 +950,6 @@ function wml_actions.unsynced(cfg) end) end -wesnoth.wml_actions.random_placement = function(cfg) - local dist_le = nil - - local parsed = helper.shallow_parsed(cfg) - -- TODO: In most cases this tag is used to place units, so maybe make include_borders=no the default for [filter_location]? - local filter = helper.get_child(parsed, "filter_location") or {} - local command = helper.get_child(parsed, "command") or helper.wml_error("[random_placement] missing required [command] subtag") - local distance = cfg.min_distance or 0 - local num_items = cfg.num_items or helper.wml_error("[random_placement] missing required 'num_items' attribute") - local variable = cfg.variable or helper.wml_error("[random_placement] missing required 'variable' attribute") - local allow_less = cfg.allow_less == true - local variable_previous = utils.start_var_scope(variable) - - if distance < 0 then - -- optimisation for distance = -1 - dist_le = function() return false end - elseif distance == 0 then - -- optimisation for distance = 0 - dist_le = function(x1,y1,x2,y2) return x1 == x2 and y1 == y2 end - else - -- optimisation: cloasure is faster than string lookups. - local math_abs = math.abs - -- same effect as helper.distance_between(x1,y1,x2,y2) <= distance but faster. - dist_le = function(x1,y1,x2,y2) - local d_x = math_abs(x1-x2) - if d_x > distance then - return false - end - if d_x % 2 ~= 0 then - if x1 % 2 == 0 then - y2 = y2 - 0.5 - else - y2 = y2 + 0.5 - end - end - local d_y = math_abs(y1-y2) - return d_x + 2*d_y <= 2*distance - end - end - - local locs = wesnoth.get_locations(filter) - if type(num_items) == "string" then - num_items = math.floor(loadstring("local size = " .. #locs .. "; return " .. num_items)()) - print("num_items=" .. num_items .. ", #locs=" .. #locs) - end - local size = #locs - for i = 1, num_items do - if size == 0 then - if allow_less then - print("placed only " .. i .. " items") - return - else - helper.wml_error("[random_placement] failed to place items. only " .. i .. " items were placed") - end - end - local index = wesnoth.random(size) - local point = locs[index] - wesnoth.set_variable(variable .. ".x", point[1]) - wesnoth.set_variable(variable .. ".y", point[2]) - wesnoth.set_variable(variable .. ".n", i) - for j = size, 1, -1 do - if dist_le(locs[j][1], locs[j][2], point[1], point[2]) then - -- optimisation: swapping elements and storing size in an extra variable is faster than table.remove(locs, j) - locs[j] = locs[size] - size = size - 1 - end - end - wesnoth.wml_actions.command (command) - end - utils.end_var_scope(variable, variable_previous) - -end - local function on_board(x, y) if type(x) ~= "number" or type(y) ~= "number" then return false diff --git a/data/lua/wml/endlevel.lua b/data/lua/wml/endlevel.lua new file mode 100644 index 000000000000..5590ccba0932 --- /dev/null +++ b/data/lua/wml/endlevel.lua @@ -0,0 +1,93 @@ + + +function wml_actions.endlevel(cfg) + local parsed = helper.parsed(cfg) + if wesnoth.check_end_level_disabled() then + wesnoth.message("Repeated [endlevel] execution, ignoring") + return + end + + local next_scenario = cfg.next_scenario + if next_scenario then + wesnoth.set_next_scenario(next_scenario) + end + + local end_text = cfg.end_text + local end_text_duration = cfg.end_text_duration + if end_text or end_text_duration then + wesnoth.set_end_campaign_text(end_text or "", end_text_duration) + end + + local end_credits = cfg.end_credits + if end_credits ~= nil then + wesnoth.set_end_campaign_credits(end_credits) + end + + local side_results = {} + for result in helper.child_range(parsed, "result") do + local side = result.side or helper.wml_error("[result] in [endlevel] missing required side= key") + side_results[side] = result + end + local there_is_a_human_victory = false + local there_is_a_human_defeat = false + local there_is_a_local_human_victory = false + local there_is_a_local_human_defeat = false + local bool_int = function(b) + if b == true then + return 1 + elseif b == false then + return 0 + else + return b + end + end + for k,v in ipairs(wesnoth.sides) do + local side_result = side_results[v.side] or {} + local victory_or_defeat = side_result.result or cfg.result or "victory" + local victory = victory_or_defeat == "victory" + if victory_or_defeat ~= "victory" and victory_or_defeat ~= "defeat" then + return helper.wml_error("invalid result= key in [endlevel] '" .. victory_or_defeat .."'") + end + if v.controller == "human" or v.controller == "network" then + if victory then + there_is_a_human_victory = true + else + there_is_a_human_defeat = true + end + end + if v.controller == "human" then + if victory then + there_is_a_local_human_victory = true + else + there_is_a_local_human_defeat = true + end + end + if side_result.bonus ~= nil then + v.carryover_bonus = bool_int(side_result.bonus) + elseif cfg.bonus ~= nil then + v.carryover_bonus = bool_int(cfg.bonus) + end + if side_result.carryover_add ~= nil then + v.carryover_add = side_result.carryover_add + elseif cfg.carryover_add ~= nil then + v.carryover_add = cfg.carryover_add + end + if side_result.carryover_percentage ~= nil then + v.carryover_percentage = side_result.carryover_percentage + elseif cfg.carryover_percentage ~= nil then + v.carryover_percentage = cfg.carryover_percentage + end + end + local proceed_to_next_level = there_is_a_human_victory or (not there_is_a_human_defeat and cfg.result ~= "defeat") + local victory = there_is_a_local_human_victory or (not there_is_a_local_human_defeat and proceed_to_next_level) + wesnoth.end_level { + music = cfg.music, + carryover_report = cfg.carryover_report, + save = cfg.save, + replay_save = cfg.replay_save, + linger_mode = cfg.linger_mode, + reveal_map = cfg.reveal_map, + proceed_to_next_level = proceed_to_next_level, + result = victory and "victory" or "defeat", + } +end \ No newline at end of file diff --git a/data/lua/wml/find_path.lua b/data/lua/wml/find_path.lua new file mode 100644 index 000000000000..23be65cd0c5e --- /dev/null +++ b/data/lua/wml/find_path.lua @@ -0,0 +1,98 @@ + + +function wml_actions.find_path(cfg) + local filter_unit = helper.get_child(cfg, "traveler") or helper.wml_error("[find_path] missing required [traveler] tag") + -- only the first unit matching + local unit = wesnoth.get_units(filter_unit)[1] or helper.wml_error("[find_path]'s filter didn't match any unit") + local filter_location = helper.get_child(cfg, "destination") or helper.wml_error( "[find_path] missing required [destination] tag" ) + -- support for $this_unit + local this_unit = utils.start_var_scope("this_unit") + + wesnoth.set_variable ( "this_unit" ) -- clearing this_unit + wesnoth.set_variable("this_unit", unit.__cfg) -- cfg field needed + + local variable = cfg.variable or "path" + local ignore_units = false + local ignore_teleport = false + + if cfg.check_zoc == false then --if we do not want to check the ZoCs, we must ignore units + ignore_units = true + end + if cfg.check_teleport == false then --if we do not want to check teleport, we must ignore it + ignore_teleport = true + end + + local allow_multiple_turns = cfg.allow_multiple_turns + local viewing_side + + if not cfg.check_visibility then viewing_side = 0 end -- if check_visiblity then shroud is taken in account + + local locations = wesnoth.get_locations(filter_location) -- only the location with the lowest distance and lowest movement cost will match. If there will still be more than 1, only the 1st maching one. + local max_cost = nil + if not allow_multiple_turns then max_cost = unit.moves end --to avoid wrong calculation on already moved units + local current_distance, current_cost = math.huge, math.huge + local current_location = {} + + local width,heigth,border = wesnoth.get_map_size() -- data for test below + + for index, location in ipairs(locations) do + -- we test if location passed to pathfinder is invalid (border); if is, do nothing, do not return and continue the cycle + if location[1] == 0 or location[1] == ( width + 1 ) or location[2] == 0 or location[2] == ( heigth + 1 ) then + else + local distance = helper.distance_between ( unit.x, unit.y, location[1], location[2] ) + -- if we pass an unreachable locations an high value will be returned + local path, cost = wesnoth.find_path( unit, location[1], location[2], { max_cost = max_cost, ignore_units = ignore_units, ignore_teleport = ignore_teleport, viewing_side = viewing_side } ) + + if ( distance < current_distance and cost <= current_cost ) or ( cost < current_cost and distance <= current_distance ) then -- to avoid changing the hex with one with less distance and more cost, or vice versa + current_distance = distance + current_cost = cost + current_location = location + end + end + end + + if #current_location == 0 then wesnoth.message("WML warning","[find_path]'s filter didn't match any location") + else + local path, cost = wesnoth.find_path( unit, current_location[1], current_location[2], { max_cost = max_cost, ignore_units = ignore_units, ignore_teleport = ignore_teleport, viewing_side = viewing_side } ) + local turns + + if cost == 0 then -- if location is the same, of course it doesn't cost any MP + turns = 0 + else + turns = math.ceil( ( ( cost - unit.moves ) / unit.max_moves ) + 1 ) + end + + if cost >= 42424242 then -- it's the high value returned for unwalkable or busy terrains + wesnoth.set_variable ( string.format("%s", variable), { hexes = 0 } ) -- set only length, nil all other values + -- support for $this_unit + wesnoth.set_variable ( "this_unit" ) -- clearing this_unit + utils.end_var_scope("this_unit", this_unit) + return end + + if not allow_multiple_turns and turns > 1 then -- location cannot be reached in one turn + wesnoth.set_variable ( string.format("%s", variable), { hexes = 0 } ) + -- support for $this_unit + wesnoth.set_variable ( "this_unit" ) -- clearing this_unit + utils.end_var_scope("this_unit", this_unit) + return end -- skip the cycles below + + wesnoth.set_variable ( string.format( "%s", variable ), { hexes = current_distance, from_x = unit.x, from_y = unit.y, to_x = current_location[1], to_y = current_location[2], movement_cost = cost, required_turns = turns } ) + + for index, path_loc in ipairs(path) do + local sub_path, sub_cost = wesnoth.find_path( unit, path_loc[1], path_loc[2], { max_cost = max_cost, ignore_units = ignore_units, ignore_teleport = ignore_teleport, viewing_side = viewing_side } ) + local sub_turns + + if sub_cost == 0 then + sub_turns = 0 + else + sub_turns = math.ceil( ( ( sub_cost - unit.moves ) / unit.max_moves ) + 1 ) + end + + wesnoth.set_variable ( string.format( "%s.step[%d]", variable, index - 1 ), { x = path_loc[1], y = path_loc[2], terrain = wesnoth.get_terrain( path_loc[1], path_loc[2] ), movement_cost = sub_cost, required_turns = sub_turns } ) -- this structure takes less space in the inspection window + end + end + + -- support for $this_unit + wesnoth.set_variable ( "this_unit" ) -- clearing this_unit + utils.end_var_scope("this_unit", this_unit) +end \ No newline at end of file diff --git a/data/lua/wml/harm_unit.lua b/data/lua/wml/harm_unit.lua new file mode 100644 index 000000000000..e57bfdf2109e --- /dev/null +++ b/data/lua/wml/harm_unit.lua @@ -0,0 +1,202 @@ + + +function wml_actions.harm_unit(cfg) + local filter = helper.get_child(cfg, "filter") or helper.wml_error("[harm_unit] missing required [filter] tag") + -- we need to use shallow_literal field, to avoid raising an error if $this_unit (not yet assigned) is used + if not helper.shallow_literal(cfg).amount then helper.wml_error("[harm_unit] has missing required amount= attribute") end + local variable = cfg.variable -- kept out of the way to avoid problems + local _ = wesnoth.textdomain "wesnoth" + -- #textdomain wesnoth + local harmer + + local function toboolean( value ) -- helper for animate fields + -- units will be animated upon leveling or killing, even + -- with special attacker and defender values + if value then return true + else return false end + end + + local this_unit = utils.start_var_scope("this_unit") + + for index, unit_to_harm in ipairs(wesnoth.get_units(filter)) do + if unit_to_harm.valid then + -- block to support $this_unit + wesnoth.set_variable ( "this_unit" ) -- clearing this_unit + wesnoth.set_variable("this_unit", unit_to_harm.__cfg) -- cfg field needed + local amount = tonumber(cfg.amount) + local animate = cfg.animate -- attacker and defender are special values + local delay = cfg.delay or 500 + local kill = cfg.kill + local fire_event = cfg.fire_event + local primary_attack = helper.get_child(cfg, "primary_attack") + local secondary_attack = helper.get_child(cfg, "secondary_attack") + local harmer_filter = helper.get_child(cfg, "filter_second") + local experience = cfg.experience + local resistance_multiplier = tonumber(cfg.resistance_multiplier) or 1 + if harmer_filter then harmer = wesnoth.get_units(harmer_filter)[1] end + -- end of block to support $this_unit + + if animate then + if animate ~= "defender" and harmer and harmer.valid then + wesnoth.scroll_to_tile(harmer.x, harmer.y, true) + wml_actions.animate_unit { + flag = "attack", + hits = true, + with_bars = true, + T.filter { id = harmer.id }, + T.primary_attack ( primary_attack ), + T.secondary_attack ( secondary_attack ), + T.facing { x = unit_to_harm.x, y = unit_to_harm.y }, + } + end + wesnoth.scroll_to_tile(unit_to_harm.x, unit_to_harm.y, true) + end + + -- the two functions below are taken straight from the C++ engine, utils.cpp and actions.cpp, with a few unuseful parts removed + -- may be moved in helper.lua in 1.11 + local function round_damage( base_damage, bonus, divisor ) + local rounding + if base_damage == 0 then return 0 + else + if bonus < divisor or divisor == 1 then + rounding = divisor / 2 - 0 + else + rounding = divisor / 2 - 1 + end + return math.max( 1, math.floor( ( base_damage * bonus + rounding ) / divisor ) ) + end + end + + local function calculate_damage( base_damage, alignment, tod_bonus, resistance, modifier ) + local damage_multiplier = 100 + if alignment == "lawful" then + damage_multiplier = damage_multiplier + tod_bonus + elseif alignment == "chaotic" then + damage_multiplier = damage_multiplier - tod_bonus + elseif alignment == "liminal" then + damage_multiplier = damage_multiplier - math.abs( tod_bonus ) + else -- neutral, do nothing + end + local resistance_modified = resistance * modifier + damage_multiplier = damage_multiplier * resistance_modified + local damage = round_damage( base_damage, damage_multiplier, 10000 ) -- if harmer.status.slowed, this may be 20000 ? + return damage + end + + local damage = calculate_damage( + amount, + cfg.alignment or "neutral", + wesnoth.get_time_of_day( { unit_to_harm.x, unit_to_harm.y, true } ).lawful_bonus, + wesnoth.unit_resistance( unit_to_harm, cfg.damage_type or "dummy" ), + resistance_multiplier + ) + + if unit_to_harm.hitpoints <= damage then + if kill == false then damage = unit_to_harm.hitpoints - 1 + else damage = unit_to_harm.hitpoints + end + end + + unit_to_harm.hitpoints = unit_to_harm.hitpoints - damage + local text = string.format("%d%s", damage, "\n") + local add_tab = false + local gender = unit_to_harm.__cfg.gender + + local function set_status(name, male_string, female_string, sound) + if not cfg[name] or unit_to_harm.status[name] then return end + if gender == "female" then + text = string.format("%s%s%s", text, tostring(female_string), "\n") + else + text = string.format("%s%s%s", text, tostring(male_string), "\n") + end + + unit_to_harm.status[name] = true + add_tab = true + + if animate and sound then -- for unhealable, that has no sound + wesnoth.play_sound (sound) + end + end + + if not unit_to_harm.status.unpoisonable then + set_status("poisoned", _"poisoned", _"female^poisoned", "poison.ogg") + end + set_status("slowed", _"slowed", _"female^slowed", "slowed.wav") + set_status("petrified", _"petrified", _"female^petrified", "petrified.ogg") + set_status("unhealable", _"unhealable", _"female^unhealable") + + -- Extract unit and put it back to update animation if status was changed + wesnoth.extract_unit(unit_to_harm) + wesnoth.put_unit(unit_to_harm) + + if add_tab then + text = string.format("%s%s", "\t", text) + end + + if animate and animate ~= "attacker" then + if harmer and harmer.valid then + wml_actions.animate_unit { + flag = "defend", + hits = true, + with_bars = true, + T.filter { id = unit_to_harm.id }, + T.primary_attack ( primary_attack ), + T.secondary_attack ( secondary_attack ), + T.facing { x = harmer.x, y = harmer.y }, + } + else + wml_actions.animate_unit { + flag = "defend", + hits = true, + with_bars = true, + T.filter { id = unit_to_harm.id }, + T.primary_attack ( primary_attack ), + T.secondary_attack ( secondary_attack ), + } + end + end + + wesnoth.float_label( unit_to_harm.x, unit_to_harm.y, string.format( "%s", text ) ) + + local function calc_xp( level ) -- to calculate the experience in case of kill + if level == 0 then return 4 + else return level * 8 end + end + + if experience ~= false and harmer and harmer.valid and wesnoth.is_enemy( unit_to_harm.side, harmer.side ) then -- no XP earned for harming friendly units + if kill ~= false and unit_to_harm.hitpoints <= 0 then + harmer.experience = harmer.experience + calc_xp( unit_to_harm.__cfg.level ) + else + unit_to_harm.experience = unit_to_harm.experience + harmer.__cfg.level + harmer.experience = harmer.experience + unit_to_harm.__cfg.level + end + end + + if kill ~= false and unit_to_harm.hitpoints <= 0 then + wml_actions.kill { id = unit_to_harm.id, animate = toboolean( animate ), fire_event = fire_event } + end + + if animate then + wesnoth.delay(delay) + end + + if variable then + wesnoth.set_variable(string.format("%s[%d]", variable, index - 1), { harm_amount = damage }) + end + + -- both units may no longer be alive at this point, so double check + if experience ~= false and unit_to_harm and unit_to_harm.valid then + unit_to_harm:advance(toboolean(animate), fire_event ~= false) + end + + if experience ~= false and harmer and harmer.valid then + harmer:advance(toboolean(animate), fire_event ~= false) + end + end + + wml_actions.redraw {} + end + + wesnoth.set_variable ( "this_unit" ) -- clearing this_unit + utils.end_var_scope("this_unit", this_unit) +end \ No newline at end of file diff --git a/data/lua/wml/modify_unit.lua b/data/lua/wml/modify_unit.lua new file mode 100644 index 000000000000..3ce4f0363a61 --- /dev/null +++ b/data/lua/wml/modify_unit.lua @@ -0,0 +1,72 @@ + + +function wml_actions.modify_unit(cfg) + local unit_variable = "LUA_modify_unit" + + local function handle_attributes(cfg, unit_path, toplevel) + for current_key, current_value in pairs(helper.shallow_parsed(cfg)) do + if type(current_value) ~= "table" and (not toplevel or current_key ~= "type") then + wesnoth.set_variable(string.format("%s.%s", unit_path, current_key), current_value) + end + end + end + + local function handle_child(cfg, unit_path) + local children_handled = {} + local cfg = helper.shallow_parsed(cfg) + handle_attributes(cfg, unit_path) + + for current_index, current_table in ipairs(cfg) do + local current_tag = current_table[1] + local tag_index = children_handled[current_tag] or 0 + handle_child(current_table[2], string.format("%s.%s[%u]", + unit_path, current_tag, tag_index)) + children_handled[current_tag] = tag_index + 1 + end + end + + local filter = helper.get_child(cfg, "filter") or helper.wml_error "[modify_unit] missing required [filter] tag" + local function handle_unit(unit_num) + local children_handled = {} + local unit_path = string.format("%s[%u]", unit_variable, unit_num) + local this_unit = wesnoth.get_variable(unit_path) + wesnoth.set_variable("this_unit", this_unit) + handle_attributes(cfg, unit_path, true) + + for current_index, current_table in ipairs(helper.shallow_parsed(cfg)) do + local current_tag = current_table[1] + if current_tag == "filter" then + -- nothing + elseif current_tag == "object" or current_tag == "trait" or current_tag == "advancement" then + local unit = wesnoth.get_variable(unit_path) + unit = wesnoth.create_unit(unit) + wesnoth.add_modification(unit, current_tag, current_table[2]) + unit = unit.__cfg; + wesnoth.set_variable(unit_path, unit) + else + local tag_index = children_handled[current_tag] or 0 + handle_child(current_table[2], string.format("%s.%s[%u]", + unit_path, current_tag, tag_index)) + children_handled[current_tag] = tag_index + 1 + end + end + + if cfg.type then + if cfg.type ~= "" then wesnoth.set_variable(unit_path .. ".advances_to", cfg.type) end + wesnoth.set_variable(unit_path .. ".experience", wesnoth.get_variable(unit_path .. ".max_experience")) + end + wml_actions.kill({ id = this_unit.id, animate = false }) + wml_actions.unstore_unit { variable = unit_path } + end + + wml_actions.store_unit { {"filter", filter}, variable = unit_variable } + local max_index = wesnoth.get_variable(unit_variable .. ".length") - 1 + + local this_unit = utils.start_var_scope("this_unit") + for current_unit = 0, max_index do + handle_unit(current_unit) + end + utils.end_var_scope("this_unit", this_unit) + + wesnoth.set_variable(unit_variable) +end \ No newline at end of file diff --git a/data/lua/wml/random_placement.lua b/data/lua/wml/random_placement.lua new file mode 100644 index 000000000000..20f8293f424d --- /dev/null +++ b/data/lua/wml/random_placement.lua @@ -0,0 +1,74 @@ + + +wesnoth.wml_actions.random_placement = function(cfg) + local dist_le = nil + + local parsed = helper.shallow_parsed(cfg) + -- TODO: In most cases this tag is used to place units, so maybe make include_borders=no the default for [filter_location]? + local filter = helper.get_child(parsed, "filter_location") or {} + local command = helper.get_child(parsed, "command") or helper.wml_error("[random_placement] missing required [command] subtag") + local distance = cfg.min_distance or 0 + local num_items = cfg.num_items or helper.wml_error("[random_placement] missing required 'num_items' attribute") + local variable = cfg.variable or helper.wml_error("[random_placement] missing required 'variable' attribute") + local allow_less = cfg.allow_less == true + local variable_previous = utils.start_var_scope(variable) + + if distance < 0 then + -- optimisation for distance = -1 + dist_le = function() return false end + elseif distance == 0 then + -- optimisation for distance = 0 + dist_le = function(x1,y1,x2,y2) return x1 == x2 and y1 == y2 end + else + -- optimisation: cloasure is faster than string lookups. + local math_abs = math.abs + -- same effect as helper.distance_between(x1,y1,x2,y2) <= distance but faster. + dist_le = function(x1,y1,x2,y2) + local d_x = math_abs(x1-x2) + if d_x > distance then + return false + end + if d_x % 2 ~= 0 then + if x1 % 2 == 0 then + y2 = y2 - 0.5 + else + y2 = y2 + 0.5 + end + end + local d_y = math_abs(y1-y2) + return d_x + 2*d_y <= 2*distance + end + end + + local locs = wesnoth.get_locations(filter) + if type(num_items) == "string" then + num_items = math.floor(loadstring("local size = " .. #locs .. "; return " .. num_items)()) + print("num_items=" .. num_items .. ", #locs=" .. #locs) + end + local size = #locs + for i = 1, num_items do + if size == 0 then + if allow_less then + print("placed only " .. i .. " items") + return + else + helper.wml_error("[random_placement] failed to place items. only " .. i .. " items were placed") + end + end + local index = wesnoth.random(size) + local point = locs[index] + wesnoth.set_variable(variable .. ".x", point[1]) + wesnoth.set_variable(variable .. ".y", point[2]) + wesnoth.set_variable(variable .. ".n", i) + for j = size, 1, -1 do + if dist_le(locs[j][1], locs[j][2], point[1], point[2]) then + -- optimisation: swapping elements and storing size in an extra variable is faster than table.remove(locs, j) + locs[j] = locs[size] + size = size - 1 + end + end + wesnoth.wml_actions.command (command) + end + utils.end_var_scope(variable, variable_previous) + +end \ No newline at end of file