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