1 /*
2    Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
3    Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5    This program is free software; you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 2 of the License, or
8    (at your option) any later version.
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY.
11 
12    See the COPYING file for more details.
13 */
14 
15 #include "help/help_topic_generators.hpp"
16 
17 #include "font/sdl_ttf.hpp"             // for line_width
18 #include "game_config.hpp"              // for debug, menu_contract, etc
19 #include "preferences/game.hpp"         // for encountered_terrains, etc
20 #include "gettext.hpp"                  // for _, gettext, N_
21 #include "language.hpp"                 // for string_table, symbol_table
22 #include "log.hpp"                      // for LOG_STREAM, logger, etc
23 #include "movetype.hpp"                 // for movetype, movetype::effects, etc
24 #include "units/race.hpp"               // for unit_race, etc
25 #include "terrain/terrain.hpp"          // for terrain_type
26 #include "terrain/translation.hpp"      // for operator==, ter_list, etc
27 #include "terrain/type_data.hpp"        // for terrain_type_data, etc
28 #include "tstring.hpp"                  // for t_string, operator<<
29 #include "units/helper.hpp"             // for resistance_color
30 #include "units/types.hpp"              // for unit_type, unit_type_data, etc
31 #include "video.hpp"                    // fore current_resolution
32 
33 #include <boost/optional.hpp>  // for optional
34 #include <iostream>                     // for operator<<, basic_ostream, etc
35 #include <map>                          // for map, etc
36 #include <set>
37 #include <SDL2/SDL.h>
38 
39 static lg::log_domain log_help("help");
40 #define WRN_HP LOG_STREAM(warn, log_help)
41 #define DBG_HP LOG_STREAM(debug, log_help)
42 
43 namespace help {
44 
45 struct terrain_movement_info
46 {
47 	const t_string name;
48 	const t_string id;
49 	const int defense;
50 	const int movement_cost;
51 	const int vision_cost;
52 	const int jamming_cost;
53 	const bool defense_cap;
54 
operator <help::terrain_movement_info55 	bool operator<(const terrain_movement_info& other) const
56 	{
57 		return translation::icompare(name, other.name) < 0;
58 	}
59 };
60 
best_str(bool best)61 static std::string best_str(bool best) {
62 	std::string lang_policy = (best ? _("Best of") : _("Worst of"));
63 	std::string color_policy = (best ? "green": "red");
64 
65 	return "<format>color='" + color_policy + "' text='" + lang_policy + "'</format>";
66 }
67 
68 typedef t_translation::ter_list::const_iterator ter_iter;
69 // Gets an english description of a terrain ter_list alias behavior: "Best of cave, hills", "Worst of Swamp, Forest" etc.
print_behavior_description(ter_iter start,ter_iter end,const ter_data_cache & tdata,bool first_level=true,bool begin_best=true)70 static std::string print_behavior_description(ter_iter start, ter_iter end, const ter_data_cache & tdata, bool first_level = true, bool begin_best = true)
71 {
72 
73 	if (start == end) return "";
74 	if (*start == t_translation::MINUS || *start == t_translation::PLUS) return print_behavior_description(start+1, end, tdata, first_level, *start == t_translation::PLUS); //absorb any leading mode changes by calling again, with a new default value begin_best.
75 
76 	boost::optional<ter_iter> last_change_pos;
77 
78 	bool best = begin_best;
79 	for (ter_iter i = start; i != end; ++i) {
80 		if ((best && *i == t_translation::MINUS) || (!best && *i == t_translation::PLUS)) {
81 			best = !best;
82 			last_change_pos = i;
83 		}
84 	}
85 
86 	std::stringstream ss;
87 
88 	if (!last_change_pos) {
89 		std::vector<std::string> names;
90 		for (ter_iter i = start; i != end; ++i) {
91 			const terrain_type tt = tdata->get_terrain_info(*i);
92 			if (!tt.editor_name().empty())
93 				names.push_back(tt.editor_name());
94 		}
95 
96 		if (names.empty()) return "";
97 		if (names.size() == 1) return names.at(0);
98 
99 		ss << best_str(best) << " ";
100 		if (!first_level) ss << "( ";
101 		ss << names.at(0);
102 
103 		for (size_t i = 1; i < names.size(); i++) {
104 			ss << ", " << names.at(i);
105 		}
106 
107 		if (!first_level) ss << " )";
108 	} else {
109 		std::vector<std::string> names;
110 		for (ter_iter i = *last_change_pos+1; i != end; ++i) {
111 			const terrain_type tt = tdata->get_terrain_info(*i);
112 			if (!tt.editor_name().empty())
113 				names.push_back(tt.editor_name());
114 		}
115 
116 		if (names.empty()) { //This alias list is apparently padded with junk at the end, so truncate it without adding more parens
117 			return print_behavior_description(start, *last_change_pos, tdata, first_level, begin_best);
118 		}
119 
120 		ss << best_str(best) << " ";
121 		if (!first_level) ss << "( ";
122 		ss << print_behavior_description(start, *last_change_pos-1, tdata, false, begin_best);
123 		// Printed the (parenthesized) leading part from before the change, now print the remaining names in this group.
124 		for (const std::string & s : names) {
125 			ss << ", " << s;
126 		}
127 		if (!first_level) ss << " )";
128 	}
129 	return ss.str();
130 }
131 
operator ()() const132 std::string terrain_topic_generator::operator()() const {
133 	std::stringstream ss;
134 
135 	if (!type_.icon_image().empty())
136 		ss << "<img>src='images/buttons/icon-base-32.png~RC(magenta>" << type_.id()
137 			<< ")~BLIT("<< "terrain/" << type_.icon_image() << "_30.png)" << "'</img> ";
138 
139 	if (!type_.editor_image().empty())
140 		ss << "<img>src='" << type_.editor_image() << "'</img> ";
141 
142 	if (!type_.help_topic_text().empty())
143 		ss << "\n\n" << type_.help_topic_text().str() << "\n";
144 	else
145 		ss << "\n";
146 
147 	ter_data_cache tdata = load_terrain_types_data();
148 
149 	if (!tdata) {
150 		WRN_HP << "When building terrain help topics, we couldn't acquire any terrain types data\n";
151 		return ss.str();
152 	}
153 
154 	if (!(type_.union_type().size() == 1 && type_.union_type()[0] == type_.number() && type_.is_nonnull())) {
155 
156 		const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number());
157 
158 		ss << "\n" << _("Base Terrain: ");
159 
160 		bool first = true;
161 		for (const t_translation::terrain_code& underlying_terrain : underlying_mvt_terrains) {
162 			const terrain_type& mvt_base = tdata->get_terrain_info(underlying_terrain);
163 
164 			if (mvt_base.editor_name().empty()) continue;
165 
166 			if (!first) {
167 				ss << ", ";
168 			} else {
169 				first = false;
170 			}
171 
172 			ss << make_link(mvt_base.editor_name(), ".." + terrain_prefix + mvt_base.id());
173 		}
174 
175 		ss << "\n";
176 
177 		ss << "\n" << _("Movement properties: ");
178 		ss << print_behavior_description(underlying_mvt_terrains.begin(), underlying_mvt_terrains.end(), tdata) << "\n";
179 
180 		const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number());
181 		ss << "\n" << _("Defense properties: ");
182 		ss << print_behavior_description(underlying_def_terrains.begin(), underlying_def_terrains.end(), tdata) << "\n";
183 	}
184 
185 	if (game_config::debug) {
186 
187 		ss << "\n";
188 		ss << "ID: "          << type_.id() << "\n";
189 
190 		ss << "Village: "     << (type_.is_village()   ? "Yes" : "No") << "\n";
191 		ss << "Gives Healing: " << type_.gives_healing() << "\n";
192 
193 		ss << "Keep: "        << (type_.is_keep()      ? "Yes" : "No") << "\n";
194 		ss << "Castle: "      << (type_.is_castle()    ? "Yes" : "No") << "\n";
195 
196 		ss << "Overlay: "     << (type_.is_overlay()   ? "Yes" : "No") << "\n";
197 		ss << "Combined: "    << (type_.is_combined()  ? "Yes" : "No") << "\n";
198 		ss << "Nonnull: "     << (type_.is_nonnull()   ? "Yes" : "No") << "\n";
199 
200 		ss << "Terrain string:"  << type_.number() << "\n";
201 
202 		ss << "Hide in Editor: " << (type_.hide_in_editor() ? "Yes" : "No") << "\n";
203 		ss << "Editor Group: "   << type_.editor_group() << "\n";
204 
205 		ss << "Light Bonus: "   << type_.light_bonus(0) << "\n";
206 
207 		ss << type_.income_description();
208 
209 		if (type_.editor_image().empty()) { // Note: this is purely temporary to help make a different help entry
210 			ss << "\nEditor Image: Empty\n";
211 		} else {
212 			ss << "\nEditor Image: " << type_.editor_image() << "\n";
213 		}
214 
215 		const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number());
216 		ss << "\nDebug Mvt Description String:";
217 		for (const t_translation::terrain_code & t : underlying_mvt_terrains) {
218 			ss << " " << t;
219 		}
220 
221 		const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number());
222 		ss << "\nDebug Def Description String:";
223 		for (const t_translation::terrain_code & t : underlying_def_terrains) {
224 			ss << " " << t;
225 		}
226 
227 	}
228 
229 	return ss.str();
230 }
231 
232 
233 //Typedef to help with formatting list of traits
234 //Maps localized trait name to trait help topic ID
235 typedef std::pair<std::string, std::string> trait_data;
236 
237 //Helper function for printing a list of trait data
print_trait_list(std::stringstream & ss,const std::vector<trait_data> & l)238 static void print_trait_list(std::stringstream & ss, const std::vector<trait_data> & l)
239 {
240 	size_t i = 0;
241 	ss << make_link(l[i].first, l[i].second);
242 
243 	// This doesn't skip traits with empty names
244 	for(i++; i < l.size(); i++) {
245 		ss << ", " << make_link(l[i].first,l[i].second);
246 	}
247 }
248 
operator ()() const249 std::string unit_topic_generator::operator()() const {
250 	// Force the lazy loading to build this unit.
251 	unit_types.build_unit_type(type_, unit_type::FULL);
252 
253 	std::stringstream ss;
254 	std::string clear_stringstream;
255 	const std::string detailed_description = type_.unit_description();
256 	const unit_type& female_type = type_.get_gender_unit_type(unit_race::FEMALE);
257 	const unit_type& male_type = type_.get_gender_unit_type(unit_race::MALE);
258 
259 	const int screen_width = CVideo::get_singleton().get_width();
260 
261 	ss << _("Level") << " " << type_.level();
262 	ss << "\n\n";
263 
264 	ss << "<img>src='" << male_type.image();
265 	ss << "~RC(" << male_type.flag_rgb() << ">red)";
266 	if (screen_width >= 1200) ss << "~XBRZ(2)";
267 	ss << "' box='no'</img> ";
268 
269 
270 	if (&female_type != &male_type) {
271 		ss << "<img>src='" << female_type.image();
272 		ss << "~RC(" << female_type.flag_rgb() << ">red)";
273 		if (screen_width >= 1200) ss << "~XBRZ(2)";
274 		ss << "' box='no'</img> ";
275 	}
276 
277 	const std::string &male_portrait = male_type.small_profile().empty() ?
278 		male_type.big_profile() : male_type.small_profile();
279 	const std::string &female_portrait = female_type.small_profile().empty() ?
280 		female_type.big_profile() : female_type.small_profile();
281 
282 	const bool has_male_portrait = !male_portrait.empty() && male_portrait != male_type.image() && male_portrait != "unit_image";
283 	const bool has_female_portrait = !female_portrait.empty() && female_portrait != male_portrait && female_portrait != female_type.image() && female_portrait != "unit_image";
284 
285 	int sz = (has_male_portrait && has_female_portrait ? 300 : 400);
286 	if (screen_width <= 1366) {
287 		sz = (has_male_portrait && has_female_portrait ? 200 : 300);
288 	} else if (screen_width >= 1920) {
289 		sz = 400;
290 	}
291 
292 	// TODO: figure out why the second checks don't match but the last does
293 	if (has_male_portrait) {
294 		ss << "<img>src='" << male_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz << ")' box='no' align='right' float='yes'</img> ";
295 	}
296 
297 
298 	if (has_female_portrait) {
299 		ss << "<img>src='" << female_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz << ")' box='no' align='right' float='yes'</img> ";
300 	}
301 
302 	ss << "\n\n\n";
303 
304 	// Print cross-references to units that this unit advances from/to.
305 	// Cross reference to the topics containing information about those units.
306 	const bool first_reverse_value = true;
307 	bool reverse = first_reverse_value;
308 	if (variation_.empty()) {
309 		do {
310 			std::vector<std::string> adv_units =
311 				reverse ? type_.advances_from() : type_.advances_to();
312 			bool first = true;
313 
314 			for (const std::string &adv : adv_units) {
315 				const unit_type *type = unit_types.find(adv, unit_type::HELP_INDEXED);
316 				if (!type || type->hide_help()) {
317 					continue;
318 				}
319 
320 				if (first) {
321 					if (reverse) {
322 						ss << _("Advances from: ");
323 					} else {
324 						ss << _("Advances to: ");
325 					}
326 					first = false;
327 				} else {
328 					ss << ", ";
329 				}
330 
331 				std::string lang_unit = type->type_name();
332 				std::string ref_id;
333 				if (description_type(*type) == FULL_DESCRIPTION) {
334 					const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
335 					ref_id = section_prefix + unit_prefix + type->id();
336 				} else {
337 					ref_id = unknown_unit_topic;
338 					lang_unit += " (?)";
339 				}
340 				ss << make_link(lang_unit, ref_id);
341 			}
342 			if (!first) {
343 				ss << "\n";
344 			}
345 
346 			reverse = !reverse; //switch direction
347 		} while(reverse != first_reverse_value); // don't restart
348 	}
349 
350 	const unit_type* parent = variation_.empty() ? &type_ :
351 		unit_types.find(type_.id(), unit_type::HELP_INDEXED);
352 	if (!variation_.empty()) {
353 		ss << _("Base unit: ") << make_link(parent->type_name(), ".." + unit_prefix + type_.id()) << "\n";
354 	} else {
355 		bool first = true;
356 		for (const std::string& base_id : utils::split(type_.get_cfg()["base_ids"])) {
357 			if (first) {
358 				ss << _("Base units: ");
359 				first = false;
360 			}
361 			const unit_type* base_type = unit_types.find(base_id, unit_type::HELP_INDEXED);
362 			const std::string section_prefix = base_type->show_variations_in_help() ? ".." : "";
363 			ss << make_link(base_type->type_name(), section_prefix + unit_prefix + base_id) << "\n";
364 		}
365 	}
366 
367 	bool first = true;
368 	for (const std::string &var_id : parent->variations()) {
369 		const unit_type &type = parent->get_variation(var_id);
370 
371 		if(type.hide_help()) {
372 			continue;
373 		}
374 
375 		if (first) {
376 			ss << _("Variations: ");
377 			first = false;
378 		} else {
379 			ss << ", ";
380 		}
381 
382 		std::string ref_id;
383 
384 		std::string var_name = type.variation_name();
385 		if (description_type(type) == FULL_DESCRIPTION) {
386 			ref_id = variation_prefix + type.id() + "_" + var_id;
387 		} else {
388 			ref_id = unknown_unit_topic;
389 			var_name += " (?)";
390 		}
391 
392 		ss << make_link(var_name, ref_id);
393 	}
394 	ss << "\n"; //added even if empty, to avoid shifting
395 
396 	// Print the race of the unit, cross-reference it to the respective topic.
397 	const std::string race_id = type_.race_id();
398 	std::string race_name = type_.race()->plural_name();
399 	if (race_name.empty()) {
400 		race_name = _ ("race^Miscellaneous");
401 	}
402 	ss << _("Race: ");
403 	ss << make_link(race_name, "..race_" + race_id);
404 	ss << "\n\n";
405 
406 	// Print the possible traits of the unit, cross-reference them
407 	// to their respective topics.
408 	if (config::const_child_itors traits = type_.possible_traits()) {
409 		std::vector<trait_data> must_have_traits;
410 		std::vector<trait_data> random_traits;
411 		int must_have_nameless_traits = 0;
412 
413 		for (const config & trait : traits) {
414 			const std::string& male_name = trait["male_name"].str();
415 			const std::string& female_name = trait["female_name"].str();
416 			std::string trait_name;
417 			if (type_.has_gender_variation(unit_race::MALE) && ! male_name.empty())
418 				trait_name = male_name;
419 			else if (type_.has_gender_variation(unit_race::FEMALE) && ! female_name.empty())
420 				trait_name = female_name;
421 			else if (! trait["name"].str().empty())
422 				trait_name = trait["name"].str();
423 			else
424 				continue; // Hidden trait
425 
426 			std::string lang_trait_name = translation::gettext(trait_name.c_str());
427 			if (lang_trait_name.empty() && trait["availability"].str() == "musthave") {
428 				++must_have_nameless_traits;
429 				continue;
430 			}
431 			const std::string ref_id = "traits_"+trait["id"].str();
432 			((trait["availability"].str() == "musthave") ? must_have_traits : random_traits).emplace_back(lang_trait_name, ref_id);
433 		}
434 
435 		bool line1 = !must_have_traits.empty();
436 		bool line2 = !random_traits.empty() && type_.num_traits() > must_have_traits.size();
437 
438 		if (line1) {
439 			std::string traits_label = _("Traits");
440 			ss << traits_label;
441 			if (line2) {
442 				std::stringstream must_have_count;
443 				must_have_count << " (" << must_have_traits.size() << ") : ";
444 				std::stringstream random_count;
445 				random_count << " (" << (type_.num_traits() - must_have_traits.size() - must_have_nameless_traits) << ") : ";
446 
447 				int second_line_whitespace = font::line_width(traits_label+must_have_count.str(), normal_font_size)
448 					- font::line_width(random_count.str(), normal_font_size);
449 				// This ensures that the second line is justified so that the ':' characters are aligned.
450 
451 				ss << must_have_count.str();
452 				print_trait_list(ss, must_have_traits);
453 				ss << "\n" << jump(second_line_whitespace) << random_count.str();
454 				print_trait_list(ss, random_traits);
455 			} else {
456 				ss << ": ";
457 				print_trait_list(ss, must_have_traits);
458 			}
459 			ss << "\n\n";
460 		} else {
461 			if (line2) {
462 				ss << _("Traits") << " (" << (type_.num_traits() - must_have_nameless_traits) << ") : ";
463 				print_trait_list(ss, random_traits);
464 				ss << "\n\n";
465 			}
466 		}
467 	}
468 
469 	// Print the abilities the units has, cross-reference them
470 	// to their respective topics. TODO: Update this according to musthave trait effects, similar to movetype below
471 	if(!type_.abilities_metadata().empty()) {
472 		ss << _("Abilities: ");
473 
474 		bool start = true;
475 
476 		for(auto iter = type_.abilities_metadata().begin(); iter != type_.abilities_metadata().end(); ++iter) {
477 			const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
478 
479 			if(iter->name.empty()) {
480 				continue;
481 			}
482 
483 			if(!start) {
484 				ss << ", ";
485 			} else {
486 				start = false;
487 			}
488 
489 			std::string lang_ability = translation::gettext(iter->name.c_str());
490 			ss << make_link(lang_ability, ref_id);
491 		}
492 
493 		ss << "\n\n";
494 	}
495 
496 	// Print the extra AMLA upgrade abilities, cross-reference them to their respective topics.
497 	if(!type_.adv_abilities_metadata().empty()) {
498 		ss << _("Ability Upgrades: ");
499 
500 		bool start = true;
501 
502 		for(auto iter = type_.adv_abilities_metadata().begin(); iter != type_.adv_abilities_metadata().end(); ++iter) {
503 			const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
504 
505 			if(iter->name.empty()) {
506 				continue;
507 			}
508 
509 			if(!start) {
510 				ss << ", ";
511 			} else {
512 				start = false;
513 			}
514 
515 			std::string lang_ability = translation::gettext(iter->name.c_str());
516 			ss << make_link(lang_ability, ref_id);
517 		}
518 
519 		ss << "\n\n";
520 	}
521 
522 	// Print some basic information such as HP and movement points.
523 	// TODO: Make this info update according to musthave traits, similar to movetype below.
524 
525 	// TRANSLATORS: This string is used in the help page of a single unit.  If the translation
526 	// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
527 	// unpleasant line breaks (issue #3256).
528 	ss << _("HP:") << font::nbsp << type_.hitpoints() << jump(30)
529 		// TRANSLATORS: This string is used in the help page of a single unit.  If the translation
530 		// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
531 		// unpleasant line breaks (issue #3256).
532 		<< _("Moves:") << font::nbsp << type_.movement() << jump(30);
533 	if (type_.vision() != type_.movement()) {
534 		// TRANSLATORS: This string is used in the help page of a single unit.  If the translation
535 		// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
536 		// unpleasant line breaks (issue #3256).
537 		ss << _("Vision:") << font::nbsp << type_.vision() << jump(30);
538 	}
539 	if (type_.jamming() > 0) {
540 		// TRANSLATORS: This string is used in the help page of a single unit.  If the translation
541 		// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
542 		// unpleasant line breaks (issue #3256).
543 		ss << _("Jamming:") << font::nbsp << type_.jamming() << jump(30);
544 	}
545 	// TRANSLATORS: This string is used in the help page of a single unit.  If the translation
546 	// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
547 	// unpleasant line breaks (issue #3256).
548 	ss << _("Cost:") << font::nbsp << type_.cost() << jump(30)
549 		// TRANSLATORS: This string is used in the help page of a single unit.  If the translation
550 		// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
551 		// unpleasant line breaks (issue #3256).
552 		<< _("Alignment:") << font::nbsp
553 		<< make_link(type_.alignment_description(type_.alignment(), type_.genders().front()), "time_of_day")
554 		<< jump(30);
555 	if (type_.can_advance()) {
556 		// TRANSLATORS: This string is used in the help page of a single unit.  It uses
557 		// non-breaking spaces to prevent unpleasant line breaks (issue #3256).  In the
558 		// translation use non-breaking spaces as appropriate for the target language.
559 		ss << _("Required\u00a0XP:") << font::nbsp << type_.experience_needed();
560 	}
561 
562 	// Print the detailed description about the unit.
563 		ss << "\n\n" << detailed_description;
564 
565 	// Print the different attacks a unit has, if it has any.
566 	if (!type_.attacks().empty()) {
567 		// Print headers for the table.
568 		ss << "\n\n<header>text='" << escape(_("unit help^Attacks"))
569 			<< "'</header>\n\n";
570 		table_spec table;
571 
572 		std::vector<item> first_row;
573 		// Dummy element, icons are below.
574 		first_row.push_back(item("", 0));
575 		push_header(first_row, _("unit help^Name"));
576 		push_header(first_row, _("Type"));
577 		push_header(first_row, _("Strikes"));
578 		push_header(first_row, _("Range"));
579 		push_header(first_row, _("Special"));
580 		table.push_back(first_row);
581 		// Print information about every attack.
582 		for(const attack_type& attack : type_.attacks()) {
583 			std::string lang_weapon = attack.name();
584 			std::string lang_type = string_table["type_" + attack.type()];
585 			std::vector<item> row;
586 			std::stringstream attack_ss;
587 			attack_ss << "<img>src='" << attack.icon() << "'</img>";
588 			row.emplace_back(attack_ss.str(),image_width(attack.icon()));
589 			push_tab_pair(row, lang_weapon);
590 			push_tab_pair(row, lang_type);
591 			attack_ss.str(clear_stringstream);
592 			attack_ss << attack.damage() << font::weapon_numbers_sep << attack.num_attacks()
593 				<< " " << attack.accuracy_parry_description();
594 			push_tab_pair(row, attack_ss.str());
595 			attack_ss.str(clear_stringstream);
596 			if (attack.min_range() > 1 || attack.max_range() > 1) {
597 				attack_ss << attack.min_range() << "-" << attack.max_range() << ' ';
598 			}
599 			attack_ss << string_table["range_" + attack.range()];
600 			push_tab_pair(row, attack_ss.str());
601 			attack_ss.str(clear_stringstream);
602 			// Show this attack's special, if it has any. Cross
603 			// reference it to the section describing the special.
604 			std::vector<std::pair<t_string, t_string>> specials = attack.special_tooltips();
605 			if (!specials.empty()) {
606 				std::string lang_special = "";
607 				const size_t specials_size = specials.size();
608 				for (size_t i = 0; i != specials_size; ++i) {
609 					const std::string ref_id = std::string("weaponspecial_")
610 						+ specials[i].first.base_str();
611 					lang_special = (specials[i].first);
612 					attack_ss << make_link(lang_special, ref_id);
613 					if (i+1 != specials_size) {
614 						attack_ss << ", "; //comma placed before next special
615 					}
616 				}
617 				row.emplace_back(attack_ss.str(), font::line_width(lang_special, normal_font_size));
618 			}
619 			table.push_back(row);
620 		}
621 		ss << generate_table(table);
622 	}
623 
624 	// Generate the movement type of the unit, with resistance, defense, movement, jamming and vision data updated according to any 'musthave' traits which always apply
625 	movetype movement_type = type_.movement_type();
626 	config::const_child_itors traits = type_.possible_traits();
627 	if (!traits.empty() && type_.num_traits() > 0) {
628 		for (const config & t : traits) {
629 			if (t["availability"].str() == "musthave") {
630 				for (const config & effect : t.child_range("effect")) {
631 					if (!effect.child("filter") // If this is musthave but has a unit filter, it might not always apply, so don't apply it in the help.
632 							&& movetype::effects.find(effect["apply_to"].str()) != movetype::effects.end()) {
633 						movement_type.merge(effect, effect["replace"].to_bool());
634 					}
635 				}
636 			}
637 		}
638 	}
639 
640 	// Print the resistance table of the unit.
641 	ss << "\n\n<header>text='" << escape(_("Resistances"))
642 		<< "'</header>\n\n";
643 	table_spec resistance_table;
644 	std::vector<item> first_res_row;
645 	push_header(first_res_row, _("Attack Type"));
646 	push_header(first_res_row, _("Resistance"));
647 	resistance_table.push_back(first_res_row);
648 	utils::string_map dam_tab = movement_type.damage_table();
649 	for(std::pair<std::string, std::string> dam_it : dam_tab) {
650 		std::vector<item> row;
651 		int resistance = 100;
652 		try {
653 			resistance -= std::stoi(dam_it.second);
654 		} catch(std::invalid_argument&) {}
655 		std::string resist = std::to_string(resistance) + '%';
656 		const size_t pos = resist.find('-');
657 		if (pos != std::string::npos) {
658 			resist.replace(pos, 1, font::unicode_minus);
659 		}
660 		std::string color = unit_helper::resistance_color(resistance);
661 		std::string lang_weapon = string_table["type_" + dam_it.first];
662 		push_tab_pair(row, lang_weapon);
663 		std::stringstream str;
664 		str << "<format>color=\"" << color << "\" text='"<< resist << "'</format>";
665 		const std::string markup = str.str();
666 		str.str(clear_stringstream);
667 		str << resist;
668 		row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
669 		resistance_table.push_back(row);
670 	}
671 	ss << generate_table(resistance_table);
672 
673 	if (ter_data_cache tdata = load_terrain_types_data()) {
674 		// Print the terrain modifier table of the unit.
675 		ss << "\n\n<header>text='" << escape(_("Terrain Modifiers"))
676 			<< "'</header>\n\n";
677 		std::vector<item> first_row;
678 		table_spec table;
679 		push_header(first_row, _("Terrain"));
680 		push_header(first_row, _("Defense"));
681 		push_header(first_row, _("Movement Cost"));
682 
683 		const bool has_terrain_defense_caps = movement_type.has_terrain_defense_caps(preferences::encountered_terrains());
684 		if (has_terrain_defense_caps) {
685 			push_header(first_row, _("Defense Cap"));
686 		}
687 
688 		const bool has_vision = type_.movement_type().has_vision_data();
689 		if (has_vision) {
690 			push_header(first_row, _("Vision Cost"));
691 		}
692 		const bool has_jamming = type_.movement_type().has_jamming_data();
693 		if (has_jamming) {
694 			push_header(first_row, _("Jamming Cost"));
695 		}
696 
697 		table.push_back(first_row);
698 
699 		std::set<terrain_movement_info> terrain_moves;
700 
701 		for (t_translation::terrain_code terrain : preferences::encountered_terrains()) {
702 			if (terrain == t_translation::FOGGED || terrain == t_translation::VOID_TERRAIN || t_translation::terrain_matches(terrain, t_translation::ALL_OFF_MAP)) {
703 				continue;
704 			}
705 			const terrain_type& info = tdata->get_terrain_info(terrain);
706 			const int moves = movement_type.movement_cost(terrain);
707 			const bool cannot_move = moves > type_.movement();
708 			if (cannot_move && info.hide_if_impassable()) {
709 				continue;
710 			}
711 
712 			if (info.union_type().size() == 1 && info.union_type()[0] == info.number() && info.is_nonnull()) {
713 				terrain_movement_info movement_info =
714 				{
715 					info.name(),
716 					info.id(),
717 					100 - movement_type.defense_modifier(terrain),
718 					moves,
719 					movement_type.vision_cost(terrain),
720 					movement_type.jamming_cost(terrain),
721 					movement_type.get_defense().capped(terrain)
722 				};
723 
724 				terrain_moves.insert(movement_info);
725 			}
726 		}
727 
728 		for(const terrain_movement_info &m : terrain_moves)
729 		{
730 			std::vector<item> row;
731 
732 			bool high_res = false;
733 			const std::string tc_base = high_res ? "images/buttons/icon-base-32.png" : "images/buttons/icon-base-16.png";
734 			const std::string terrain_image = "icons/terrain/terrain_type_" + m.id + (high_res ? "_30.png" : ".png");
735 
736 			const std::string final_image = tc_base + "~RC(magenta>" + m.id + ")~BLIT(" + terrain_image + ")";
737 
738 			row.emplace_back("<img>src='" + final_image + "'</img> " +
739 					make_link(m.name, "..terrain_" + m.id),
740 				font::line_width(m.name, normal_font_size) + (high_res ? 32 : 16) );
741 
742 			//defense  -  range: +10 % .. +70 %
743 			// passing false to select the more saturated red-to-green scale
744 			std::string color = game_config::red_to_green(m.defense, false).to_hex_string();
745 
746 			std::stringstream str;
747 			str << "<format>color='" << color << "' text='"<< m.defense << "%'</format>";
748 			std::string markup = str.str();
749 			str.str(clear_stringstream);
750 			str << m.defense << "%";
751 			row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
752 
753 			//movement  -  range: 1 .. 5, movetype::UNREACHABLE=impassable
754 			str.str(clear_stringstream);
755 			bool cannot_move = m.movement_cost > type_.movement();
756 			if (cannot_move) {		// cannot move in this terrain
757 				color = "red";
758 			} else if (m.movement_cost > 1) {
759 				color = "yellow";
760 			} else {
761 				color = "white";
762 			}
763 			str << "<format>color=" << color << " text='";
764 			// A 5 MP margin; if the movement costs go above
765 			// the unit's max moves + 5, we replace it with dashes.
766 			if(cannot_move && (m.movement_cost > type_.movement() + 5)) {
767 				str << font::unicode_figure_dash;
768 			} else {
769 				str << m.movement_cost;
770 			}
771 			str << "'</format>";
772 			markup = str.str();
773 			str.str(clear_stringstream);
774 			str << m.movement_cost;
775 			row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
776 
777 			//defense cap
778 			if (has_terrain_defense_caps) {
779 				str.str(clear_stringstream);
780 				if (m.defense_cap) {
781 					str << "<format>color='"<< color <<"' text='" << m.defense << "%'</format>";
782 				} else {
783 					str << "<format>color=white text='" << font::unicode_figure_dash << "'</format>";
784 				}
785 				markup = str.str();
786 				str.str(clear_stringstream);
787 				if (m.defense_cap) {
788 					str << m.defense << '%';
789 				} else {
790 					str << font::unicode_figure_dash;
791 				}
792 				row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
793 			}
794 
795 			//vision
796 			if (has_vision) {
797 				str.str(clear_stringstream);
798 				const bool cannot_view = m.vision_cost > type_.vision();
799 				if (cannot_view) {		// cannot view in this terrain
800 					color = "red";
801 				} else if (m.vision_cost > m.movement_cost) {
802 					color = "yellow";
803 				} else if (m.vision_cost == m.movement_cost) {
804 					color = "white";
805 				} else {
806 					color = "green";
807 				}
808 				str << "<format>color=" << color << " text='";
809 				// A 5 MP margin; if the vision costs go above
810 				// the unit's vision + 5, we replace it with dashes.
811 				if(cannot_view && (m.vision_cost > type_.vision() + 5)) {
812 					str << font::unicode_figure_dash;
813 				} else {
814 					str << m.vision_cost;
815 				}
816 				str << "'</format>";
817 				markup = str.str();
818 				str.str(clear_stringstream);
819 				str << m.vision_cost;
820 				row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
821 			}
822 
823 			//jamming
824 			if (has_jamming) {
825 				str.str(clear_stringstream);
826 				const bool cannot_jam = m.jamming_cost > type_.jamming();
827 				if (cannot_jam) {		// cannot jamm in this terrain
828 					color = "red";
829 				} else if (m.jamming_cost > m.vision_cost) {
830 					color = "yellow";
831 				} else if (m.jamming_cost == m.vision_cost) {
832 					color = "white";
833 				} else {
834 					color = "green";
835 				}
836 				str << "<format>color=" << color << " text='";
837 				// A 5 MP margin; if the jamming costs go above
838 				// the unit's jamming + 5, we replace it with dashes.
839 				if (cannot_jam && m.jamming_cost > type_.jamming() + 5) {
840 					str << font::unicode_figure_dash;
841 				} else {
842 					str << m.jamming_cost;
843 				}
844 				str << "'</format>";
845 
846 				push_tab_pair(row, str.str());
847 			}
848 
849 			table.push_back(row);
850 		}
851 
852 		ss << generate_table(table);
853 	} else {
854 		WRN_HP << "When building unit help topics, the display object was null and we couldn't get the terrain info we need.\n";
855 	}
856 	return ss.str();
857 }
858 
859 
860 
push_header(std::vector<item> & row,const std::string & name) const861 void unit_topic_generator::push_header(std::vector< item > &row,  const std::string& name) const {
862 	row.emplace_back(bold(name), font::line_width(name, normal_font_size, TTF_STYLE_BOLD));
863 }
864 
865 } // end namespace help
866