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