1 /*
2    Copyright (C) 2016 - 2018 by the Battle for Wesnoth Project https://www.wesnoth.org/
3 
4    This program is free software; you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation; either version 2 of the License, or
7    (at your option) any later version.
8    This program is distributed in the hope that it will be useful,
9    but WITHOUT ANY WARRANTY.
10 
11    See the COPYING file for more details.
12 */
13 
14 /**
15  * @file
16  * Fighting.
17  */
18 
19 #include "actions/advancement.hpp"
20 
21 #include "actions/vision.hpp"
22 
23 #include "ai/lua/aspect_advancements.hpp"
24 #include "game_events/pump.hpp"
25 #include "preferences/game.hpp"
26 #include "game_data.hpp" //resources::gamedata->phase()
27 #include "gettext.hpp"
28 #include "gui/dialogs/unit_advance.hpp"
29 #include "log.hpp"
30 #include "play_controller.hpp" //resources::controller
31 #include "random.hpp"
32 #include "resources.hpp"
33 #include "statistics.hpp"
34 #include "synced_user_choice.hpp"
35 #include "units/unit.hpp"
36 #include "units/abilities.hpp"
37 #include "units/animation_component.hpp"
38 #include "units/udisplay.hpp"
39 #include "units/helper.hpp" //number_of_possible_advances
40 #include "whiteboard/manager.hpp"
41 
42 static lg::log_domain log_engine("engine");
43 #define DBG_NG LOG_STREAM(debug, log_engine)
44 #define LOG_NG LOG_STREAM(info, log_engine)
45 #define WRN_NG LOG_STREAM(err, log_engine)
46 #define ERR_NG LOG_STREAM(err, log_engine)
47 
48 static lg::log_domain log_config("config");
49 #define LOG_CF LOG_STREAM(info, log_config)
50 
51 static lg::log_domain log_display("display");
52 #define LOG_DP LOG_STREAM(info, log_display)
53 
54 
55 namespace
56 {
advance_unit_dialog(const map_location & loc)57 	int advance_unit_dialog(const map_location &loc)
58 	{
59 		const unit& u = *resources::gameboard->units().find(loc);
60 		std::vector<unit_const_ptr> previews;
61 
62 		for (const std::string& advance : u.advances_to()) {
63 			preferences::encountered_units().insert(advance);
64 			previews.push_back(get_advanced_unit(u, advance));
65 		}
66 
67 		size_t num_real_advances = previews.size();
68 		bool always_display = false;
69 
70 		for (const config& advance : u.get_modification_advances()) {
71 			if (advance["always_display"]) {
72 				always_display = true;
73 			}
74 			previews.push_back(get_amla_unit(u, advance));
75 		}
76 
77 		if (previews.size() > 1 || always_display) {
78 			gui2::dialogs::unit_advance dlg(previews, num_real_advances);
79 
80 			if(dlg.show()) {
81 				return dlg.get_selected_index();
82 			}
83 
84 			// This should be unreachable, since canceling is disabled for the dialog
85 			assert(false && "Unit advance dialog was cancelled, which should be impossible.");
86 		}
87 
88 		return 0;
89 	}
90 
animate_unit_advancement(const map_location & loc,size_t choice,const bool & fire_event,const bool animate)91 	bool animate_unit_advancement(const map_location &loc, size_t choice, const bool &fire_event, const bool animate)
92 	{
93 		const events::command_disabler cmd_disabler;
94 
95 		unit_map::iterator u = resources::gameboard->units().find(loc);
96 		if (u == resources::gameboard->units().end()) {
97 			LOG_DP << "animate_unit_advancement suppressed: invalid unit\n";
98 			return false;
99 		}
100 		else if (!u->advances()) {
101 			LOG_DP << "animate_unit_advancement suppressed: unit does not advance\n";
102 			return false;
103 		}
104 
105 		const std::vector<std::string>& options = u->advances_to();
106 		std::vector<config> mod_options = u->get_modification_advances();
107 
108 		if (choice >= options.size() + mod_options.size()) {
109 			LOG_DP << "animate_unit_advancement suppressed: invalid option\n";
110 			return false;
111 		}
112 
113 		// When the unit advances, it fades to white, and then switches
114 		// to the new unit, then fades back to the normal color
115 
116 		if (animate && !CVideo::get_singleton().update_locked()) {
117 			unit_animator animator;
118 			bool with_bars = true;
119 			animator.add_animation(&*u, "levelout", u->get_location(), map_location(), 0, with_bars);
120 			animator.start_animations();
121 			animator.wait_for_end();
122 		}
123 
124 		if (choice < options.size()) {
125 			// chosen_unit is not a reference, since the unit may disappear at any moment.
126 			std::string chosen_unit = options[choice];
127 			::advance_unit(loc, chosen_unit, fire_event);
128 		}
129 		else {
130 			const config &mod_option = mod_options[choice - options.size()];
131 			::advance_unit(loc, &mod_option, fire_event);
132 		}
133 
134 		u = resources::gameboard->units().find(loc);
135 		game_display::get_singleton()->invalidate_unit();
136 
137 		if (animate && u != resources::gameboard->units().end() && !CVideo::get_singleton().update_locked()) {
138 			unit_animator animator;
139 			animator.add_animation(&*u, "levelin", u->get_location(), map_location(), 0, true);
140 			animator.start_animations();
141 			animator.wait_for_end();
142 			animator.set_all_standing();
143 			display::get_singleton()->invalidate(loc);
144 			events::pump();
145 		}
146 
147 		display::get_singleton()->invalidate_all();
148 
149 		return true;
150 	}
151 
152 	class unit_advancement_choice : public mp_sync::user_choice
153 	{
154 	public:
unit_advancement_choice(const map_location & loc,int total_opt,int side_num,const ai::unit_advancements_aspect * ai_advancement,bool force_dialog)155 		unit_advancement_choice(const map_location& loc, int total_opt, int side_num, const ai::unit_advancements_aspect* ai_advancement, bool force_dialog)
156 			: loc_ (loc), nb_options_(total_opt), side_num_(side_num), ai_advancement_(ai_advancement), force_dialog_(force_dialog)
157 		{
158 		}
159 
~unit_advancement_choice()160 		virtual ~unit_advancement_choice()
161 		{
162 		}
163 
query_user(int) const164 		virtual config query_user(int /*side*/) const
165 		{
166 			//the 'side' parameter might differ from side_num_-
167 			int res = 0;
168 			team t = resources::gameboard->get_team(side_num_);
169 			//i wonder how this got included here ?
170 			bool is_mp = resources::controller->is_networked_mp();
171 			bool is_current_side = resources::controller->current_side() == side_num_;
172 			//note, that the advancements for networked sides are also determined on the current playing side.
173 
174 			//to make mp games equal we only allow selecting advancements to the current side.
175 			//otherwise we'd give an unfair advantage to the side that hosts ai sides if units advance during ai turns.
176 			if(!CVideo::get_singleton().non_interactive() && (force_dialog_ || (t.is_local_human() && !t.is_droid() && !t.is_idle() && (is_current_side || !is_mp))))
177 			{
178 				res = advance_unit_dialog(loc_);
179 			}
180 			else if(t.is_local_ai() || t.is_network_ai() || t.is_empty())
181 			{
182 				res = randomness::generator->get_random_int(0, nb_options_-1);
183 
184 				//if ai_advancement_ is the default advancement the following code will
185 				//have no effect because get_advancements returns an empty list.
186 				if(ai_advancement_ != nullptr)
187 				{
188 					unit_map::iterator u = resources::gameboard->units().find(loc_);
189 					const std::vector<std::string>& options = u->advances_to();
190 					const std::vector<std::string>& allowed = ai_advancement_->get_advancements(u);
191 
192 					for(std::vector<std::string>::const_iterator a = options.begin(); a != options.end(); ++a) {
193 						if (std::find(allowed.begin(), allowed.end(), *a) != allowed.end()){
194 							res = std::distance(options.begin(), a);
195 							break;
196 						}
197 					}
198 				}
199 
200 			}
201 			else
202 			{
203 				//we are in the situation, that the unit is owned by a human, but he's not allowed to do this decision.
204 				//because it's a mp game and it's not his turn.
205 				//note that it doesn't matter whether we call randomness::generator->next_random() or rand().
206 				res = randomness::generator->get_random_int(0, nb_options_-1);
207 			}
208 			LOG_NG << "unit at position " << loc_ << "choose advancement number " << res << "\n";
209 			config retv;
210 			retv["value"] = res;
211 			return retv;
212 
213 		}
random_choice(int) const214 		virtual config random_choice(int /*side*/) const
215 		{
216 			config retv;
217 			retv["value"] = 0;
218 			return retv;
219 		}
description() const220 		virtual std::string description() const
221 		{
222 			// TRANSLATORS: In networked games, when one player has the choice
223 			// between multiple advancements of a unit, this text is sent to
224 			// other players. It will be embedded within a message.
225 			return _("waiting for^an advancement choice");
226 		}
227 	private:
228 		const map_location loc_;
229 		int nb_options_;
230 		int side_num_;
231 		const ai::unit_advancements_aspect* ai_advancement_;
232 		bool force_dialog_;
233 	};
234 }
235 
236 /*
237 advances the unit and stores data in the replay (or reads data from replay).
238 */
advance_unit_at(const advance_unit_params & params)239 void advance_unit_at(const advance_unit_params& params)
240 {
241 	//i just don't want infinite loops...
242 	// the 20 is picked rather randomly.
243 	for(int advacment_number = 0; advacment_number < 20; advacment_number++)
244 	{
245 		unit_map::iterator u = resources::gameboard->units().find(params.loc_);
246 		//this implies u.valid()
247 		if(!unit_helper::will_certainly_advance(u)) {
248 			return;
249 		}
250 
251 		if(params.fire_events_)
252 		{
253 			LOG_NG << "Firing pre advance event at " << params.loc_ <<".\n";
254 			resources::game_events->pump().fire("pre_advance", params.loc_);
255 			//TODO: maybe use id instead of location here ?.
256 			u = resources::gameboard->units().find(params.loc_);
257 			if(!unit_helper::will_certainly_advance(u))
258 			{
259 				LOG_NG << "pre advance event aborted advancing.\n";
260 				return;
261 			}
262 		}
263 		//we don't want to let side 1 decide it during start/prestart.
264 		int side_for = resources::gamedata->phase() == game_data::PLAY ? 0: u->side();
265 		config selected = mp_sync::get_user_choice("choose",
266 			unit_advancement_choice(params.loc_, unit_helper::number_of_possible_advances(*u), u->side(), params.ai_advancements_, params.force_dialog_), side_for);
267 		//calls actions::advance_unit.
268 		bool result = animate_unit_advancement(params.loc_, selected["value"], params.fire_events_, params.animate_);
269 
270 		DBG_NG << "animate_unit_advancement result = " << result << std::endl;
271 		u = resources::gameboard->units().find(params.loc_);
272 		// level 10 unit gives 80 XP and the highest mainline is level 5
273 		if (u.valid() && u->experience() > 80)
274 		{
275 			WRN_NG << "Unit has too many (" << u->experience() << ") XP left; cascade leveling goes on still." << std::endl;
276 		}
277 	}
278 	ERR_NG << "unit at " << params.loc_ << " tried to advance more than 20 times. Advancing was aborted" << std::endl;
279 }
280 
get_advanced_unit(const unit & u,const std::string & advance_to)281 unit_ptr get_advanced_unit(const unit &u, const std::string& advance_to)
282 {
283 	const unit_type *new_type = unit_types.find(advance_to);
284 	if (!new_type) {
285 		throw game::game_error("Could not find the unit being advanced"
286 			" to: " + advance_to);
287 	}
288 	unit_ptr new_unit = u.clone();
289 	new_unit->set_experience(new_unit->experience_overflow());
290 	new_unit->advance_to(*new_type);
291 	new_unit->heal_fully();
292 	new_unit->set_state(unit::STATE_POISONED, false);
293 	new_unit->set_state(unit::STATE_SLOWED, false);
294 	new_unit->set_state(unit::STATE_PETRIFIED, false);
295 	new_unit->set_user_end_turn(false);
296 	new_unit->set_hidden(false);
297 	return new_unit;
298 }
299 
300 
301 /**
302  * Returns the AMLA-advanced version of a unit (with traits and items retained).
303  */
get_amla_unit(const unit & u,const config & mod_option)304 unit_ptr get_amla_unit(const unit &u, const config &mod_option)
305 {
306 	unit_ptr amla_unit = u.clone();
307 	amla_unit->set_experience(amla_unit->experience_overflow());
308 	amla_unit->add_modification("advancement", mod_option);
309 	return amla_unit;
310 }
311 
312 
advance_unit(map_location loc,const advancement_option & advance_to,bool fire_event)313 void advance_unit(map_location loc, const advancement_option &advance_to, bool fire_event)
314 {
315 	unit_map::unit_iterator u = resources::gameboard->units().find(loc);
316 	if(!u.valid()) {
317 		return;
318 	}
319 	// original_type is not a reference, since the unit may disappear at any moment.
320 	std::string original_type = u->type_id();
321 
322 	// "advance" event.
323 	if(fire_event)
324 	{
325 		LOG_NG << "Firing advance event at " << loc <<".\n";
326 		resources::game_events->pump().fire("advance",loc);
327 
328 		if (!u.valid() || u->experience() < u->max_experience() ||
329 			u->type_id() != original_type)
330 		{
331 			LOG_NG << "WML has invalidated the advancing unit. Aborting.\n";
332 			return;
333 		}
334 		// In case WML moved the unit:
335 		loc = u->get_location();
336 	}
337 
338 	// This is not normally necessary, but if a unit loses power when leveling
339 	// (e.g. loses "jamming" or ambush), it could be discovered as a result of
340 	// the advancement.
341 	std::vector<int> not_seeing = actions::get_sides_not_seeing(*u);
342 
343 	// Create the advanced unit.
344 	bool use_amla = boost::get<std::string>(&advance_to) == nullptr;
345 	unit_ptr new_unit = use_amla ? get_amla_unit(*u, *boost::get<const config*>(advance_to)) :
346 	                           get_advanced_unit(*u, boost::get<std::string>(advance_to));
347 	new_unit->set_location(loc);
348 	if ( !use_amla )
349 	{
350 		statistics::advance_unit(*new_unit);
351 		preferences::encountered_units().insert(new_unit->type_id());
352 		LOG_CF << "Added '" << new_unit->type_id() << "' to the encountered units.\n";
353 	}
354 	u->anim_comp().clear_haloes();
355 	resources::gameboard->units().erase(loc);
356 	resources::whiteboard->on_kill_unit();
357 	u = resources::gameboard->units().insert(new_unit).first;
358 
359 	// Update fog/shroud.
360 	actions::shroud_clearer clearer;
361 	clearer.clear_unit(loc, *new_unit);
362 
363 	// "post_advance" event.
364 	if(fire_event)
365 	{
366 		LOG_NG << "Firing post_advance event at " << loc << ".\n";
367 		resources::game_events->pump().fire("post_advance",loc);
368 	}
369 
370 	// "sighted" event(s).
371 	clearer.fire_events();
372 	if ( u.valid() )
373 		actions::actor_sighted(*u, &not_seeing);
374 
375 	resources::whiteboard->on_gamestate_change();
376 }
377