From 010acd870d316de9e89e984a236d809d638965ec Mon Sep 17 00:00:00 2001 From: Celtic Minstrel Date: Sun, 11 Dec 2016 18:08:08 -0500 Subject: [PATCH] Properly port [animate_unit] to Lua --- changelog | 4 + data/lua/wml-tags.lua | 5 +- data/lua/wml/animate_unit.lua | 114 ++++++++++++++++++++++ src/scripting/game_lua_kernel.cpp | 157 ++++++++++++++++++++++++++++-- src/scripting/game_lua_kernel.hpp | 1 + src/units/udisplay.cpp | 125 ------------------------ src/units/udisplay.hpp | 11 --- 7 files changed, 271 insertions(+), 146 deletions(-) create mode 100644 data/lua/wml/animate_unit.lua diff --git a/changelog b/changelog index e68af22203bd..2365b1e55cb8 100644 --- a/changelog +++ b/changelog @@ -13,6 +13,10 @@ Version 1.13.6+dev: * The wesnoth.place_shroud and wesnoth.clear_shroud functions can alter shroud data for a single side. They accept a list of locations, a shroud data string, or the special value "all". + * New wesnoth.is_fogged and wesnoth.is_shrouded calls test the visibility level + of a hex for a particular side. + * New wesnoth.create_animator call produces an object that can be used to run + animations for a group of units * New Lua API functions for altering AI: * wesnoth.switch_ai replaces the entire AI with a definition from a file * wesnoth.append_ai appends AI parameters to the configuation; supports goals, diff --git a/data/lua/wml-tags.lua b/data/lua/wml-tags.lua index 1da6ea19938b..7e78ccd57ba1 100644 --- a/data/lua/wml-tags.lua +++ b/data/lua/wml-tags.lua @@ -13,6 +13,7 @@ end wesnoth.require "lua/wml-flow.lua" wesnoth.require "lua/wml/objectives.lua" +wesnoth.require "lua/wml/animate_unit.lua" wesnoth.require "lua/wml/items.lua" wesnoth.require "lua/wml/message.lua" wesnoth.require "lua/wml/object.lua" @@ -916,10 +917,6 @@ function wml_actions.scroll(cfg) wesnoth.scroll(cfg) end -function wml_actions.animate_unit(cfg) - wesnoth.animate_unit(cfg) -end - function wml_actions.color_adjust(cfg) wesnoth.color_adjust(cfg) end diff --git a/data/lua/wml/animate_unit.lua b/data/lua/wml/animate_unit.lua new file mode 100644 index 000000000000..68161e485fd1 --- /dev/null +++ b/data/lua/wml/animate_unit.lua @@ -0,0 +1,114 @@ +local helper = wesnoth.require "lua/helper.lua" +local T = helper.set_wml_tag_metatable{} + +local function get_fake_attack(unit, cfg) + -- This hacky-looking code is because the only way to create an + -- attack object in Lua is by adding the attack to a unit. + -- In this case, it's immediately deleted afterwards. + local n = #unit.attacks + 1 + unit.attacks[n] = cfg + local atk = unit.attacks[n] + unit.attacks[n] = nil + return atk +end + +local function get_real_attack(unit, filter) + for i, atk in ipairs(unit.attacks) do + if atk:matches(filter) then return atk end + end +end + +local function add_animation(anim, cfg) + cfg = helper.shallow_parsed(cfg) + local filter = helper.get_child(cfg, "filter") + local unit + if filter then + unit = wesnoth.get_units{ + limit = 1, + T["and"](filter) + }[1] + else + unit = wesnoth.get_unit( + wesnoth.current.event_context.x1, + wesnoth.current.event_context.y1 + ) + end + + if unit and not wesnoth.is_fogged(wesnoth.current.side, u.loc) then + local primary = helper.get_child(cfg, "primary_attack") + local secondary = helper.get_child(cfg, "secondary_attack") + local get_attack = get_real_attack + if cfg.flag == "death" or cfg.flag == "victory" then + -- death and victory animations need a special case + -- In order to correctly fire certain animations, a dummy attack + -- is required. This is especially evident in Wose death animations. + get_attack = get_fake_attack + end + if primary then + primary = get_attack(unit, primary) + end + if secondary then + secondary = get_attack(unit, secondary) + end + + local hits = cfg.hits + if hits == true then + hits = 'hit' + elseif hits == false then + hits = 'miss' + end + + local color = {0xff, 0xff, 0xff} + if cfg.red or cfg.green or cfg.blue then + -- This tonumber() or 0 is to ensure they're all definitely numbers + -- It works because tonumber() returns nil if its argument is not a number + color = { + tonumber(cfg.red) or 0, + tonumber(cfg.green) or 0, + tonumber(cfg.blue) or 0 + } + end + + -- TODO: The last argument is currently unused + -- (should make the game not scroll if view locked or prefs disables it) + wesnoth.scroll_to_tile(unit.x, unit.y, true, false, true, false) + + local facing = helper.get_child(cfg, "facing") + if facing then + local facing_loc = wesnoth.get_locations(facing)[1] + if facing_loc then + local dir = wesnoth.map_location_ops.get_relative_dir(unit.x, unit.y, facing_loc[1], facing_loc[2]) + facing = wesnoth.map_location_ops.get_direction(unit.x, unit.y, dir) + else + facing = nil + end + end + + local text = cfg.text + if cfg.female_text and unit.gender == 'female' then + text = cfg.female_text + elseif cfg.male_text and unit.gender == 'male' then + text = cfg.male_text + end + + anim:add(unit, cfg.flag, hits, { + facing = facing, + value = {tonumber(cfg.value) or 0, tonumber(cfg.value_second) or 0}, + with_bars = not not cfg.with_bars, + text = text, + color = color, + primary = primary, + secondary = secondary + }) + end + + for c in helper.child_range("animate") do + add_animation(anim, c) + end +end + +function wesnoth.wml_actions.animate_unit(cfg) + local anim = wesnoth.create_animator() + add_animation(anim, cfg) + anim:run() +end \ No newline at end of file diff --git a/src/scripting/game_lua_kernel.cpp b/src/scripting/game_lua_kernel.cpp index 16c7afbc3fdf..aa9e8a9801b9 100644 --- a/src/scripting/game_lua_kernel.cpp +++ b/src/scripting/game_lua_kernel.cpp @@ -335,16 +335,145 @@ static int intf_get_viewing_side(lua_State *L) } } -int game_lua_kernel::intf_animate_unit(lua_State *L) +static const char animatorKey[] = "unit animator"; + +static int impl_animator_collect(lua_State* L) { + unit_animator& anim = *static_cast(luaL_checkudata(L, 1, animatorKey)); + anim.set_all_standing(); + anim.~unit_animator(); + return 0; +} + +static int impl_add_animation(lua_State* L) { - // if (game_display_) - { - events::command_disabler disable_commands; - unit_display::wml_animation(luaW_checkvconfig(L, 1), get_event_info().loc1); + unit_animator& anim = *static_cast(luaL_checkudata(L, 1, animatorKey)); + unit& u = luaW_checkunit(L, 2); + std::string which = luaL_checkstring(L, 3); + + using hit_type = unit_animation::hit_type; + std::string hits_str = luaL_checkstring(L, 4); + hit_type hits = hit_type::string_to_enum(hits_str, hit_type::INVALID); + + map_location dest; + int v1 = 0, v2 = 0; + bool bars = false; + std::string text; + color_t color{255, 255, 255}; + const_attack_ptr primary, secondary; + + if(lua_istable(L, 5)) { + lua_getfield(L, 5, "facing"); + if(!luaW_tolocation(L, -1, dest)) { + // luaW_tolocation may set the location to (0,0) if it fails + dest = map_location(); + if(!lua_isnoneornil(L, -1)) { + return luaW_type_error(L, -1, "location table"); + } + } + lua_pop(L, 1); + + lua_getfield(L, 5, "value"); + if(lua_isnumber(L, -1)) { + v1 = lua_tonumber(L, -1); + } else if(lua_istable(L, -1)) { + lua_rawgeti(L, 1, 1); + v1 = lua_tonumber(L, -1); + lua_pop(L, 1); + lua_rawgeti(L, 1, 2); + v2 = lua_tonumber(L, -1); + lua_pop(L, 2); + } else if(!lua_isnoneornil(L, -1)) { + return luaW_type_error(L, -1, "number or array of two numbers"); + } + lua_pop(L, 1); + + lua_getfield(L, 5, "with_bars"); + if(lua_isboolean(L, -1)) { + bars = luaW_toboolean(L, -1); + } else if(!lua_isnoneornil(L, -1)) { + return luaW_type_error(L, -1, lua_typename(L, LUA_TBOOLEAN)); + } + lua_pop(L, 1); + + lua_getfield(L, 5, "text"); + if(lua_isstring(L, -1)) { + text = lua_tostring(L, -1); + } else if(!lua_isnoneornil(L, 01)) { + return luaW_type_error(L, -1, lua_typename(L, LUA_TSTRING)); + } + lua_pop(L, 1); + + lua_getfield(L, 5, "color"); + if(lua_istable(L, -1) && lua_rawlen(L, -1) == 3) { + lua_rawgeti(L, 1, 1); // red @ -3 + lua_rawgeti(L, 1, 2); // green @ -2 + lua_rawgeti(L, 1, 3); // blue @ -1 + color = color_t(lua_tonumber(L, -3), lua_tonumber(L, -2), lua_tonumber(L, -1)); + lua_pop(L, 3); + } else if(!lua_isnoneornil(L, -1)) { + return luaW_type_error(L, -1, "array of three numbers"); + } + lua_pop(L, 1); + + lua_getfield(L, 5, "primary"); + primary = luaW_toweapon(L, -1); + if(!primary && !lua_isnoneornil(L, -1)) { + return luaW_type_error(L, -1, "weapon"); + } + lua_pop(L, 1); + + lua_getfield(L, 5, "secondary"); + secondary = luaW_toweapon(L, -1); + if(!secondary && !lua_isnoneornil(L, -1)) { + return luaW_type_error(L, -1, "weapon"); + } + lua_pop(L, 1); } + + anim.add_animation(&u, which, u.get_location(), dest, v1, bars, text, color, hits, primary.get(), secondary.get(), v2); + return 0; +} + +static int impl_run_animation(lua_State* L) +{ + unit_animator& anim = *static_cast(luaL_checkudata(L, 1, animatorKey)); + anim.start_animations(); + anim.wait_for_end(); return 0; } +static int impl_clear_animation(lua_State* L) +{ + unit_animator& anim = *static_cast(luaL_checkudata(L, 1, animatorKey)); + anim.clear(); + return 0; +} + +static int impl_animator_get(lua_State* L) +{ + const char* m = lua_tostring(L, 2); + return luaW_getmetafield(L, 1, m); +} + +static int intf_create_animator(lua_State* L) +{ + new(L) unit_animator; + if(luaL_newmetatable(L, animatorKey)) { + luaL_Reg metafuncs[] = { + {"__gc", impl_animator_collect}, + {"__index", impl_animator_get}, + {"add", impl_add_animation}, + {"run", impl_run_animation}, + {"clear", impl_clear_animation}, + }; + luaL_setfuncs(L, metafuncs, 0); + lua_pushstring(L, "__metatable"); + lua_setfield(L, -2, animatorKey); + } + lua_setmetatable(L, -2); + return 1; +} + int game_lua_kernel::intf_gamestate_inspector(lua_State *L) { if (game_display_) { @@ -3774,6 +3903,20 @@ int game_lua_kernel::intf_log(lua_State *L) return 0; } +int game_lua_kernel::intf_get_fog_or_shroud(lua_State *L, bool fog) +{ + int side = luaL_checknumber(L, 1); + map_location loc = luaW_checklocation(L, 2); + if(side < 1 || static_cast(side) > teams().size()) { + std::string error = "side " + std::to_string(side) + " does not exist"; + return luaL_argerror(L, 1, error.c_str()); + } + + team& t = teams()[side - 1]; + lua_pushboolean(L, fog ? t.fogged(loc) : t.shrouded(loc)); + return 1; +} + /** * Implements the lifting and resetting of fog via WML. * Keeping affect_normal_fog as false causes only the fog override to be affected. @@ -3888,6 +4031,7 @@ game_lua_kernel::game_lua_kernel(game_state & gs, play_controller & pc, reports { "add_modification", &intf_add_modification }, { "advance_unit", &intf_advance_unit }, { "copy_unit", &intf_copy_unit }, + { "create_animator", &intf_create_animator }, { "create_unit", &intf_create_unit }, { "debug", &intf_debug }, { "debug_ai", &intf_debug_ai }, @@ -3914,7 +4058,6 @@ game_lua_kernel::game_lua_kernel(game_state & gs, play_controller & pc, reports { "add_sound_source", &dispatch<&game_lua_kernel::intf_add_sound_source > }, { "allow_end_turn", &dispatch<&game_lua_kernel::intf_allow_end_turn > }, { "allow_undo", &dispatch<&game_lua_kernel::intf_allow_undo > }, - { "animate_unit", &dispatch<&game_lua_kernel::intf_animate_unit > }, { "append_ai", &intf_append_ai }, { "clear_menu_item", &dispatch<&game_lua_kernel::intf_clear_menu_item > }, { "clear_messages", &dispatch<&game_lua_kernel::intf_clear_messages > }, @@ -3981,6 +4124,8 @@ game_lua_kernel::game_lua_kernel(game_state & gs, play_controller & pc, reports { "deselect_hex", &dispatch<&game_lua_kernel::intf_deselect_hex > }, { "select_unit", &dispatch<&game_lua_kernel::intf_select_unit > }, { "skip_messages", &dispatch<&game_lua_kernel::intf_skip_messages > }, + { "is_fogged", &dispatch2<&game_lua_kernel::intf_get_fog_or_shroud, true > }, + { "is_shrouded", &dispatch2<&game_lua_kernel::intf_get_fog_or_shroud, false > }, { "is_skipping_messages", &dispatch<&game_lua_kernel::intf_is_skipping_messages > }, { "set_end_campaign_credits", &dispatch<&game_lua_kernel::intf_set_end_campaign_credits > }, { "set_end_campaign_text", &dispatch<&game_lua_kernel::intf_set_end_campaign_text > }, diff --git a/src/scripting/game_lua_kernel.hpp b/src/scripting/game_lua_kernel.hpp index 13efaa9f1691..adda3943e16b 100644 --- a/src/scripting/game_lua_kernel.hpp +++ b/src/scripting/game_lua_kernel.hpp @@ -170,6 +170,7 @@ class game_lua_kernel : public lua_kernel_base int intf_get_sound_source(lua_State *L); int intf_log(lua_State *L); int intf_toggle_fog(lua_State *L, const bool clear); + int intf_get_fog_or_shroud(lua_State *L, bool fog); //private helpers std::string synced_state(); diff --git a/src/units/udisplay.cpp b/src/units/udisplay.cpp index 11af4271efa7..061525a82b36 100644 --- a/src/units/udisplay.cpp +++ b/src/units/udisplay.cpp @@ -767,129 +767,4 @@ void unit_healing(unit &healed, const std::vector &healers, int healing, animator.set_all_standing(); } -void wml_animation_internal(unit_animator &animator, const vconfig &cfg, const map_location &default_location = map_location::null_location()); - -void wml_animation(const vconfig &cfg, const map_location &default_location) -{ - game_display &disp = *resources::screen; - if (disp.video().update_locked() || disp.video().faked()) return; - unit_animator animator; - wml_animation_internal(animator, cfg, default_location); - animator.start_animations(); - animator.wait_for_end(); - animator.set_all_standing(); -} - -void wml_animation_internal(unit_animator &animator, const vconfig &cfg, const map_location &default_location) -{ - unit_const_ptr u; - - unit_map::const_iterator u_it = resources::gameboard->units().find(default_location); - if (u_it.valid()) { - u = u_it.get_shared_ptr(); - } - - // Search for a valid unit filter, - // and if we have one, look for the matching unit - vconfig filter = cfg.child("filter"); - if(!filter.null()) { - const unit_filter ufilt(filter, resources::filter_con); - u = ufilt.first_match_on_map(); - } - - // We have found a unit that matches the filter - if (u && !resources::screen->fogged(u->get_location())) - { - attack_type *primary = nullptr; - attack_type *secondary = nullptr; - color_t text_color; - unit_animation::hit_type hits= unit_animation::hit_type::INVALID; - const_attack_itors attacks = u->attacks(); - const_attack_itors::const_iterator itor; - attack_ptr dummy_primary; - attack_ptr dummy_secondary; - - // death and victory animations are handled here because usually - // the code iterates through all the unit's attacks - // but in these two specific cases we need to create dummy attacks - // to fire correctly certain animations - // this is especially evident with the Wose's death animations - if (cfg["flag"] == "death" || cfg["flag"] == "victory") { - filter = cfg.child("primary_attack"); - if(!filter.null()) { - dummy_primary.reset(new attack_type(filter.get_config())); - primary = dummy_primary.get(); - } - filter = cfg.child("secondary_attack"); - if(!filter.null()) { - dummy_secondary.reset(new attack_type(filter.get_config())); - secondary = dummy_secondary.get(); - } - } - - else { - filter = cfg.child("primary_attack"); - if(!filter.null()) { - for(itor = attacks.begin(); itor != attacks.end(); ++itor){ - if(itor->matches_filter(filter.get_parsed_config())) { - primary = &*itor; - break; - } - } - } - - filter = cfg.child("secondary_attack"); - if(!filter.null()) { - for(itor = attacks.begin(); itor != attacks.end(); ++itor){ - if(itor->matches_filter(filter.get_parsed_config())) { - secondary = &*itor; - break; - } - } - } - } - - if(cfg["hits"] == "yes" || cfg["hits"] == "hit") { - hits = unit_animation::hit_type::HIT; - } - if(cfg["hits"] == "no" || cfg["hits"] == "miss") { - hits = unit_animation::hit_type::MISS; - } - if( cfg["hits"] == "kill" ) { - hits = unit_animation::hit_type::KILL; - } - if(cfg["red"].empty() && cfg["green"].empty() && cfg["blue"].empty()) { - text_color = color_t(0xff,0xff,0xff); - } else { - text_color = color_t(cfg["red"], cfg["green"], cfg["blue"]); - } - resources::screen->scroll_to_tile(u->get_location(), game_display::ONSCREEN, true, false); - vconfig t_filter_data = cfg.child("facing"); - map_location secondary_loc = map_location::null_location(); - if(!t_filter_data.empty()) { - terrain_filter t_filter(t_filter_data, resources::filter_con); - std::set locs; - t_filter.get_locations(locs); - if (!locs.empty() && u->get_location() != *locs.begin()) { - map_location::DIRECTION dir =u->get_location().get_relative_dir(*locs.begin()); - u->set_facing(dir); - secondary_loc = u->get_location().get_direction(dir); - } - } - config::attribute_value text = u->gender() == unit_race::FEMALE ? cfg["female_text"] : cfg["male_text"]; - if(text.blank()) { - text = cfg["text"]; - } - animator.add_animation(&*u, cfg["flag"], u->get_location(), - secondary_loc, cfg["value"], cfg["with_bars"].to_bool(), - text.str(), text_color, hits, primary, secondary, - cfg["value_second"]); - } - const vconfig::child_list sub_anims = cfg.get_children("animate"); - vconfig::child_list::const_iterator anim_itor; - for(anim_itor = sub_anims.begin(); anim_itor != sub_anims.end();++anim_itor) { - wml_animation_internal(animator, *anim_itor); - } - -} } // end unit_display namespace diff --git a/src/units/udisplay.hpp b/src/units/udisplay.hpp index 55d5c068220e..80782e7401a5 100644 --- a/src/units/udisplay.hpp +++ b/src/units/udisplay.hpp @@ -132,17 +132,6 @@ void unit_recruited(const map_location& loc, void unit_healing(unit &healed, const std::vector &healers, int healing, const std::string & extra_text=""); - -/** - * Parse a standard WML for animations and play the corresponding animation. - * Returns once animation is played. - * - * This is used for the animate_unit action, but can easily be generalized if - * other wml-described animations are needed. - */ -void wml_animation(const vconfig &cfg, - const map_location& default_location=map_location::null_location()); - } #endif