The Battle for Wesnoth  1.17.10+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 "video.hpp" // for game_canvas_size
36 
37 #include <map> // for map, etc
38 #include <optional>
39 #include <set>
40 
41 static lg::log_domain log_help("help");
42 #define WRN_HP LOG_STREAM(warn, log_help)
43 #define DBG_HP LOG_STREAM(debug, log_help)
44 
45 namespace help {
46 
48 {
49  const t_string name;
50  const t_string id;
51  const int defense;
52  const int movement_cost;
53  const int vision_cost;
54  const int jamming_cost;
55  const bool defense_cap;
56 
57  bool operator<(const terrain_movement_info& other) const
58  {
59  return translation::icompare(name, other.name) < 0;
60  }
61 };
62 
63 static std::string best_str(bool best) {
64  std::string lang_policy = (best ? _("Best of") : _("Worst of"));
65  std::string color_policy = (best ? "green": "red");
66 
67  return "<format>color='" + color_policy + "' text='" + lang_policy + "'</format>";
68 }
69 
70 typedef t_translation::ter_list::const_iterator ter_iter;
71 // Gets an english description of a terrain ter_list alias behavior: "Best of cave, hills", "Worst of Swamp, Forest" etc.
72 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)
73 {
74 
75  if (start == end) return "";
76  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.
77 
78  std::optional<ter_iter> last_change_pos;
79 
80  bool best = begin_best;
81  for (ter_iter i = start; i != end; ++i) {
82  if ((best && *i == t_translation::MINUS) || (!best && *i == t_translation::PLUS)) {
83  best = !best;
84  last_change_pos = i;
85  }
86  }
87 
88  std::stringstream ss;
89 
90  if (!last_change_pos) {
91  std::vector<std::string> names;
92  for (ter_iter i = start; i != end; ++i) {
93  const terrain_type tt = tdata->get_terrain_info(*i);
94  if (!tt.editor_name().empty())
95  names.push_back(tt.editor_name());
96  }
97 
98  if (names.empty()) return "";
99  if (names.size() == 1) return names.at(0);
100 
101  ss << best_str(best) << " ";
102  if (!first_level) ss << "( ";
103  ss << names.at(0);
104 
105  for (std::size_t i = 1; i < names.size(); i++) {
106  ss << ", " << names.at(i);
107  }
108 
109  if (!first_level) ss << " )";
110  } else {
111  std::vector<std::string> names;
112  for (ter_iter i = *last_change_pos+1; i != end; ++i) {
113  const terrain_type tt = tdata->get_terrain_info(*i);
114  if (!tt.editor_name().empty())
115  names.push_back(tt.editor_name());
116  }
117 
118  if (names.empty()) { //This alias list is apparently padded with junk at the end, so truncate it without adding more parens
119  return print_behavior_description(start, *last_change_pos, tdata, first_level, begin_best);
120  }
121 
122  ss << best_str(best) << " ";
123  if (!first_level) ss << "( ";
124  ss << print_behavior_description(start, *last_change_pos-1, tdata, false, begin_best);
125  // Printed the (parenthesized) leading part from before the change, now print the remaining names in this group.
126  for (const std::string & s : names) {
127  ss << ", " << s;
128  }
129  if (!first_level) ss << " )";
130  }
131  return ss.str();
132 }
133 
135  std::stringstream ss;
136 
137  if (!type_.icon_image().empty())
138  ss << "<img>src='images/buttons/icon-base-32.png~RC(magenta>" << type_.id()
139  << ")~BLIT("<< "terrain/" << type_.icon_image() << "_30.png)" << "'</img> ";
140 
141  if (!type_.editor_image().empty())
142  ss << "<img>src='" << type_.editor_image() << "'</img> ";
143 
144  if (!type_.help_topic_text().empty())
145  ss << "\n\n" << type_.help_topic_text().str() << "\n";
146  else
147  ss << "\n";
148 
149  std::shared_ptr<terrain_type_data> tdata = load_terrain_types_data();
150 
151  if (!tdata) {
152  WRN_HP << "When building terrain help topics, we couldn't acquire any terrain types data";
153  return ss.str();
154  }
155 
156  // Special notes are generated from the terrain's properties - at the moment there's no way for WML authors
157  // to add their own via a [special_note] tag.
158  std::vector<std::string> special_notes;
159 
160  if(type_.is_village()) {
161  special_notes.push_back(_("Villages allow any unit stationed therein to heal, or to be cured of poison."));
162  } else if(type_.gives_healing() > 0) {
163  auto symbols = utils::string_map{{"amount", std::to_string(type_.gives_healing())}};
164  // TRANSLATORS: special note for terrains such as the oasis; the only terrain in core with this property heals 8 hp just like a village.
165  // 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.
166  auto message = VNGETTEXT("This terrain allows units to be cured of poison, or to heal a single hitpoint.",
167  "This terrain allows units to heal $amount hitpoints, or to be cured of poison, as if stationed in a village.",
168  type_.gives_healing(), symbols);
169  special_notes.push_back(std::move(message));
170  }
171 
172  if(type_.is_castle()) {
173  special_notes.push_back(_("This terrain is a castle — units can be recruited onto it from a connected keep."));
174  }
175  if(type_.is_keep() && type_.is_castle()) {
176  // TRANSLATORS: The "this terrain is a castle" note will also be shown directly above this one.
177  special_notes.push_back(_("This terrain is a keep — a leader can recruit from this hex onto connected castle hexes."));
178  } else if(type_.is_keep() && !type_.is_castle()) {
179  // TRANSLATORS: Special note for a terrain, but none of the terrains in mainline do this.
180  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."));
181  }
182 
183  if(!special_notes.empty()) {
184  ss << "\n" << _("Special Notes:") << '\n';
185  for(const auto& note : special_notes) {
186  ss << font::unicode_bullet << " " << note << '\n';
187  }
188  }
189 
190  // Almost all terrains will show the data in this conditional block. The ones that don't are the
191  // archetypes used in [movetype]'s subtags such as [movement_costs].
192  if (!type_.is_indivisible()) {
193  ss << "\n" << _("Base Terrain: ");
194 
195  bool first = true;
196  for (const auto& underlying_terrain : type_.union_type()) {
197  const terrain_type& base = tdata->get_terrain_info(underlying_terrain);
198 
199  if (base.editor_name().empty()) continue;
200 
201  if (!first) {
202  ss << ", ";
203  } else {
204  first = false;
205  }
206 
207  ss << make_link(base.editor_name(), ".." + terrain_prefix + base.id());
208  }
209 
210  ss << "\n";
211 
212  const t_translation::ter_list& underlying_mvt_terrains = type_.mvt_type();
213  ss << "\n" << _("Movement properties: ");
214  ss << print_behavior_description(underlying_mvt_terrains.begin(), underlying_mvt_terrains.end(), tdata) << "\n";
215 
216  const t_translation::ter_list& underlying_def_terrains = type_.def_type();
217  ss << "\n" << _("Defense properties: ");
218  ss << print_behavior_description(underlying_def_terrains.begin(), underlying_def_terrains.end(), tdata) << "\n";
219  }
220 
221  if (game_config::debug) {
222 
223  ss << "\n";
224  ss << "ID: " << type_.id() << "\n";
225 
226  ss << "Village: " << (type_.is_village() ? "Yes" : "No") << "\n";
227  ss << "Gives Healing: " << type_.gives_healing() << "\n";
228 
229  ss << "Keep: " << (type_.is_keep() ? "Yes" : "No") << "\n";
230  ss << "Castle: " << (type_.is_castle() ? "Yes" : "No") << "\n";
231 
232  ss << "Overlay: " << (type_.is_overlay() ? "Yes" : "No") << "\n";
233  ss << "Combined: " << (type_.is_combined() ? "Yes" : "No") << "\n";
234  ss << "Nonnull: " << (type_.is_nonnull() ? "Yes" : "No") << "\n";
235 
236  ss << "Terrain string: " << type_.number() << "\n";
237 
238  ss << "Hide in Editor: " << (type_.hide_in_editor() ? "Yes" : "No") << "\n";
239  ss << "Editor Group: " << type_.editor_group() << "\n";
240 
241  ss << "Light Bonus: " << type_.light_bonus(0) << "\n";
242 
243  ss << type_.income_description();
244 
245  if (type_.editor_image().empty()) { // Note: this is purely temporary to help make a different help entry
246  ss << "\nEditor Image: Empty\n";
247  } else {
248  ss << "\nEditor Image: " << type_.editor_image() << "\n";
249  }
250 
251  const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number());
252  ss << "\nDebug Mvt Description String:";
253  for (const t_translation::terrain_code & t : underlying_mvt_terrains) {
254  ss << " " << t;
255  }
256 
257  const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number());
258  ss << "\nDebug Def Description String:";
259  for (const t_translation::terrain_code & t : underlying_def_terrains) {
260  ss << " " << t;
261  }
262 
263  }
264 
265  return ss.str();
266 }
267 
268 
269 //Typedef to help with formatting list of traits
270 // Maps localized trait name to trait help topic ID
271 typedef std::pair<std::string, std::string> trait_data;
272 
273 //Helper function for printing a list of trait data
274 static void print_trait_list(std::stringstream & ss, const std::vector<trait_data> & l)
275 {
276  std::size_t i = 0;
277  ss << make_link(l[i].first, l[i].second);
278 
279  // This doesn't skip traits with empty names
280  for(i++; i < l.size(); i++) {
281  ss << ", " << make_link(l[i].first,l[i].second);
282  }
283 }
284 
285 std::string unit_topic_generator::operator()() const {
286  // Force the lazy loading to build this unit.
288 
289  std::stringstream ss;
290  std::string clear_stringstream;
291  const std::string detailed_description = type_.unit_description();
292  const unit_type& female_type = type_.get_gender_unit_type(unit_race::FEMALE);
293  const unit_type& male_type = type_.get_gender_unit_type(unit_race::MALE);
294 
295  const int screen_width = video::game_canvas_size().x;
296 
297  ss << _("Level") << " " << type_.level();
298  ss << "\n\n";
299 
300  ss << "<img>src='" << male_type.image();
301  ss << "~RC(" << male_type.flag_rgb() << ">red)";
302  if (screen_width >= 1200) ss << "~XBRZ(2)";
303  ss << "' box='no'</img> ";
304 
305 
306  if (&female_type != &male_type) {
307  ss << "<img>src='" << female_type.image();
308  ss << "~RC(" << female_type.flag_rgb() << ">red)";
309  if (screen_width >= 1200) ss << "~XBRZ(2)";
310  ss << "' box='no'</img> ";
311  }
312 
313  const std::string &male_portrait = male_type.small_profile().empty() ?
314  male_type.big_profile() : male_type.small_profile();
315  const std::string &female_portrait = female_type.small_profile().empty() ?
316  female_type.big_profile() : female_type.small_profile();
317 
318  const bool has_male_portrait = !male_portrait.empty() && male_portrait != male_type.image() && male_portrait != "unit_image";
319  const bool has_female_portrait = !female_portrait.empty() && female_portrait != male_portrait && female_portrait != female_type.image() && female_portrait != "unit_image";
320 
321  int sz = (has_male_portrait && has_female_portrait ? 300 : 400);
322  if (screen_width <= 1366) {
323  sz = (has_male_portrait && has_female_portrait ? 200 : 300);
324  } else if (screen_width >= 1920) {
325  sz = 400;
326  }
327 
328  // TODO: figure out why the second checks don't match but the last does
329  if (has_male_portrait) {
330  ss << "<img>src='" << male_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz << ")' box='no' align='right' float='yes'</img> ";
331  }
332 
333 
334  if (has_female_portrait) {
335  ss << "<img>src='" << female_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz << ")' box='no' align='right' float='yes'</img> ";
336  }
337 
338  ss << "\n\n\n";
339 
340  // Print cross-references to units that this unit advances from/to.
341  // Cross reference to the topics containing information about those units.
342  const bool first_reverse_value = true;
343  bool reverse = first_reverse_value;
344  if (variation_.empty()) {
345  do {
346  std::vector<std::string> adv_units =
347  reverse ? type_.advances_from() : type_.advances_to();
348  bool first = true;
349 
350  for (const std::string &adv : adv_units) {
352  if (!type || type->hide_help()) {
353  continue;
354  }
355 
356  if (first) {
357  if (reverse) {
358  ss << _("Advances from: ");
359  } else {
360  ss << _("Advances to: ");
361  }
362  first = false;
363  } else {
364  ss << ", ";
365  }
366 
367  std::string lang_unit = type->type_name();
368  std::string ref_id;
369  if (description_type(*type) == FULL_DESCRIPTION) {
370  const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
371  ref_id = section_prefix + unit_prefix + type->id();
372  } else {
373  ref_id = unknown_unit_topic;
374  lang_unit += " (?)";
375  }
376  ss << make_link(lang_unit, ref_id);
377  }
378  if (!first) {
379  ss << "\n";
380  }
381 
382  reverse = !reverse; //switch direction
383  } while(reverse != first_reverse_value); // don't restart
384  }
385 
386  const unit_type* parent = variation_.empty() ? &type_ :
388  if (!variation_.empty()) {
389  ss << _("Base unit: ") << make_link(parent->type_name(), ".." + unit_prefix + type_.id()) << "\n";
390  } else {
391  bool first = true;
392  for (const std::string& base_id : utils::split(type_.get_cfg()["base_ids"])) {
393  if (first) {
394  ss << _("Base units: ");
395  first = false;
396  }
397  const unit_type* base_type = unit_types.find(base_id, unit_type::HELP_INDEXED);
398  const std::string section_prefix = base_type->show_variations_in_help() ? ".." : "";
399  ss << make_link(base_type->type_name(), section_prefix + unit_prefix + base_id) << "\n";
400  }
401  }
402 
403  bool first = true;
404  for (const std::string &var_id : parent->variations()) {
405  const unit_type &type = parent->get_variation(var_id);
406 
407  if(type.hide_help()) {
408  continue;
409  }
410 
411  if (first) {
412  ss << _("Variations: ");
413  first = false;
414  } else {
415  ss << ", ";
416  }
417 
418  std::string ref_id;
419 
420  std::string var_name = type.variation_name();
421  if (description_type(type) == FULL_DESCRIPTION) {
422  ref_id = variation_prefix + type.id() + "_" + var_id;
423  } else {
424  ref_id = unknown_unit_topic;
425  var_name += " (?)";
426  }
427 
428  ss << make_link(var_name, ref_id);
429  }
430  ss << "\n"; //added even if empty, to avoid shifting
431 
432  // Print the race of the unit, cross-reference it to the respective topic.
433  const std::string race_id = type_.race_id();
434  std::string race_name = type_.race()->plural_name();
435  if (race_name.empty()) {
436  race_name = _ ("race^Miscellaneous");
437  }
438  ss << _("Race: ");
439  ss << make_link(race_name, "..race_" + race_id);
440  ss << "\n\n";
441 
442  // Print the possible traits of the unit, cross-reference them
443  // to their respective topics.
444  if (config::const_child_itors traits = type_.possible_traits()) {
445  std::vector<trait_data> must_have_traits;
446  std::vector<trait_data> random_traits;
447  int must_have_nameless_traits = 0;
448 
449  for(const config& trait : traits) {
450  const std::string& male_name = trait["male_name"].str();
451  const std::string& female_name = trait["female_name"].str();
452  std::string trait_name;
453  if (type_.has_gender_variation(unit_race::MALE) && ! male_name.empty())
454  trait_name = male_name;
455  else if (type_.has_gender_variation(unit_race::FEMALE) && ! female_name.empty())
456  trait_name = female_name;
457  else if (! trait["name"].str().empty())
458  trait_name = trait["name"].str();
459  else
460  continue; // Hidden trait
461 
462  std::string lang_trait_name = translation::gettext(trait_name.c_str());
463  if (lang_trait_name.empty() && trait["availability"].str() == "musthave") {
464  ++must_have_nameless_traits;
465  continue;
466  }
467  const std::string ref_id = "traits_"+trait["id"].str();
468  ((trait["availability"].str() == "musthave") ? must_have_traits : random_traits).emplace_back(lang_trait_name, ref_id);
469  }
470 
471  bool line1 = !must_have_traits.empty();
472  bool line2 = !random_traits.empty() && type_.num_traits() > must_have_traits.size();
473 
474  if (line1) {
475  std::string traits_label = _("Traits");
476  ss << traits_label;
477  if (line2) {
478  std::stringstream must_have_count;
479  must_have_count << " (" << must_have_traits.size() << ") : ";
480  std::stringstream random_count;
481  random_count << " (" << (type_.num_traits() - must_have_traits.size() - must_have_nameless_traits) << ") : ";
482 
483  int second_line_whitespace = font::pango_line_width(traits_label+must_have_count.str(), normal_font_size)
484  - font::pango_line_width(random_count.str(), normal_font_size);
485  // This ensures that the second line is justified so that the ':' characters are aligned.
486 
487  ss << must_have_count.str();
488  print_trait_list(ss, must_have_traits);
489  ss << "\n" << jump(second_line_whitespace) << random_count.str();
490  print_trait_list(ss, random_traits);
491  } else {
492  ss << ": ";
493  print_trait_list(ss, must_have_traits);
494  }
495  ss << "\n\n";
496  } else {
497  if (line2) {
498  ss << _("Traits") << " (" << (type_.num_traits() - must_have_nameless_traits) << ") : ";
499  print_trait_list(ss, random_traits);
500  ss << "\n\n";
501  }
502  }
503  }
504 
505  // Print the abilities the units has, cross-reference them
506  // to their respective topics. TODO: Update this according to musthave trait effects, similar to movetype below
507  if(!type_.abilities_metadata().empty()) {
508  ss << _("Abilities: ");
509 
510  bool start = true;
511 
512  for(auto iter = type_.abilities_metadata().begin(); iter != type_.abilities_metadata().end(); ++iter) {
513  const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
514 
515  if(iter->name.empty()) {
516  continue;
517  }
518 
519  if(!start) {
520  ss << ", ";
521  } else {
522  start = false;
523  }
524 
525  std::string lang_ability = translation::gettext(iter->name.c_str());
526  ss << make_link(lang_ability, ref_id);
527  }
528 
529  ss << "\n\n";
530  }
531 
532  // Print the extra AMLA upgrade abilities, cross-reference them to their respective topics.
533  if(!type_.adv_abilities_metadata().empty()) {
534  ss << _("Ability Upgrades: ");
535 
536  bool start = true;
537 
538  for(auto iter = type_.adv_abilities_metadata().begin(); iter != type_.adv_abilities_metadata().end(); ++iter) {
539  const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
540 
541  if(iter->name.empty()) {
542  continue;
543  }
544 
545  if(!start) {
546  ss << ", ";
547  } else {
548  start = false;
549  }
550 
551  std::string lang_ability = translation::gettext(iter->name.c_str());
552  ss << make_link(lang_ability, ref_id);
553  }
554 
555  ss << "\n\n";
556  }
557 
558  // Print some basic information such as HP and movement points.
559  // TODO: Make this info update according to musthave traits, similar to movetype below.
560 
561  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
562  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
563  // unpleasant line breaks (issue #3256).
564  ss << _("HP:") << font::nbsp << type_.hitpoints() << jump(30)
565  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
566  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
567  // unpleasant line breaks (issue #3256).
568  << _("Moves:") << font::nbsp << type_.movement() << jump(30);
569  if (type_.vision() != type_.movement()) {
570  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
571  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
572  // unpleasant line breaks (issue #3256).
573  ss << _("Vision:") << font::nbsp << type_.vision() << jump(30);
574  }
575  if (type_.jamming() > 0) {
576  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
577  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
578  // unpleasant line breaks (issue #3256).
579  ss << _("Jamming:") << font::nbsp << type_.jamming() << jump(30);
580  }
581  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
582  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
583  // unpleasant line breaks (issue #3256).
584  ss << _("Cost:") << font::nbsp << type_.cost() << jump(30)
585  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
586  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
587  // unpleasant line breaks (issue #3256).
588  << _("Alignment:") << font::nbsp
589  << make_link(type_.alignment_description(type_.alignment(), type_.genders().front()), "time_of_day")
590  << jump(30);
591  if (type_.can_advance() || type_.modification_advancements()) {
592  // TRANSLATORS: This string is used in the help page of a single unit. It uses
593  // non-breaking spaces to prevent unpleasant line breaks (issue #3256). In the
594  // translation use non-breaking spaces as appropriate for the target language.
595  ss << _("Required\u00a0XP:") << font::nbsp << type_.experience_needed();
596  }
597 
598  // Print the detailed description about the unit.
599  ss << "\n\n" << detailed_description;
600  if(const auto notes = type_.special_notes(); !notes.empty()) {
601  ss << "\n\n" << _("Special Notes:") << '\n';
602  for(const auto& note : notes) {
603  ss << font::unicode_bullet << " " << note << '\n';
604  }
605  }
606 
607  // Padding for range and damage type icons
608  const auto padding = 4; // matches the alignment of the terrain rows
609 
610  // Print the different attacks a unit has, if it has any.
611  if (!type_.attacks().empty()) {
612  // Print headers for the table.
613  ss << "\n\n<header>text='" << escape(_("unit help^Attacks"))
614  << "'</header>\n\n";
615  table_spec table;
616 
617  std::vector<item> first_row;
618  // Dummy element, icons are below.
619  first_row.push_back(item("", 0));
620  push_header(first_row, _("unit help^Name"));
621  push_header(first_row, _("Strikes"));
622  push_header(first_row, _("Range"));
623  push_header(first_row, _("Type"));
624  push_header(first_row, _("Special"));
625  table.push_back(first_row);
626  // Print information about every attack.
627  for(const attack_type& attack : type_.attacks()) {
628  std::string lang_weapon = attack.name();
629  std::string lang_type = string_table["type_" + attack.type()];
630  std::vector<item> row;
631  std::stringstream attack_ss;
632 
633  // Attack icon
634  attack_ss << "<img>src='" << attack.icon() << "'</img>";
635  row.emplace_back(attack_ss.str(),image_width(attack.icon()));
636  attack_ss.str(clear_stringstream);
637 
638  // Attack name
639  push_tab_pair(row, lang_weapon);
640 
641  // damage x strikes
642  attack_ss << attack.damage() << font::weapon_numbers_sep << attack.num_attacks()
643  << " " << attack.accuracy_parry_description();
644  push_tab_pair(row, attack_ss.str());
645  attack_ss.str(clear_stringstream);
646 
647  // Range, with icon
648  const std::string range_icon = "icons/profiles/" + attack.range() + "_attack.png~SCALE_INTO(16,16)";
649  if (attack.min_range() > 1 || attack.max_range() > 1) {
650  attack_ss << attack.min_range() << "-" << attack.max_range() << ' ';
651  }
652  attack_ss << string_table["range_" + attack.range()];
653  push_tab_pair(row, attack_ss.str(), range_icon, padding);
654  attack_ss.str(clear_stringstream);
655 
656  // Damage type, with icon
657  const std::string type_icon = "icons/profiles/" + attack.type() + ".png~SCALE_INTO(16,16)";
658  push_tab_pair(row, lang_type, type_icon, padding);
659 
660  // Show this attack's special, if it has any. Cross
661  // reference it to the section describing the special.
662  std::vector<std::pair<t_string, t_string>> specials = attack.special_tooltips();
663  if (!specials.empty()) {
664  std::string lang_special = "";
665  const std::size_t specials_size = specials.size();
666  for (std::size_t i = 0; i != specials_size; ++i) {
667  const std::string ref_id = std::string("weaponspecial_")
668  + specials[i].first.base_str();
669  lang_special = (specials[i].first);
670  attack_ss << make_link(lang_special, ref_id);
671  if (i+1 != specials_size) {
672  attack_ss << ", "; //comma placed before next special
673  }
674  }
675  row.emplace_back(attack_ss.str(), font::pango_line_width(lang_special, normal_font_size));
676  }
677  table.push_back(row);
678  }
679  ss << generate_table(table);
680  }
681 
682  // Generate the movement type of the unit, with resistance, defense, movement, jamming and vision data updated according to any 'musthave' traits which always apply
683  movetype movement_type = type_.movement_type();
684  config::const_child_itors traits = type_.possible_traits();
685  if (!traits.empty() && type_.num_traits() > 0) {
686  for (const config & t : traits) {
687  if (t["availability"].str() == "musthave") {
688  for (const config & effect : t.child_range("effect")) {
689  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.
690  && movetype::effects.find(effect["apply_to"].str()) != movetype::effects.end()) {
691  movement_type.merge(effect, effect["replace"].to_bool());
692  }
693  }
694  }
695  }
696  }
697 
698  // Print the resistance table of the unit.
699  ss << "\n\n<header>text='" << escape(_("Resistances"))
700  << "'</header>\n\n";
701  table_spec resistance_table;
702  std::vector<item> first_res_row;
703  push_header(first_res_row, _("Attack Type"));
704  push_header(first_res_row, _("Resistance"));
705  resistance_table.push_back(first_res_row);
706  utils::string_map_res dam_tab = movement_type.damage_table();
707  for(std::pair<std::string, std::string> dam_it : dam_tab) {
708  std::vector<item> row;
709  int resistance = 100;
710  try {
711  resistance -= std::stoi(dam_it.second);
712  } catch(std::invalid_argument&) {}
713  std::string resist = std::to_string(resistance) + '%';
714  const std::size_t pos = resist.find('-');
715  if (pos != std::string::npos) {
716  resist.replace(pos, 1, font::unicode_minus);
717  }
718  std::string color = unit_helper::resistance_color(resistance);
719  const std::string lang_type = string_table["type_" + dam_it.first];
720  const std::string type_icon = "icons/profiles/" + dam_it.first + ".png~SCALE_INTO(16,16)";
721  push_tab_pair(row, lang_type, type_icon, padding);
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  std::stringstream str_unformatted;
807  str << "<format>color='" << color << "' text='"<< m.defense << "%'</format>";
808  str_unformatted << m.defense << "%";
809  row.emplace_back(str.str(), font::pango_line_width(str_unformatted.str(), normal_font_size));
810 
811  //movement - range: 1 .. 5, movetype::UNREACHABLE=impassable
812  str.str(clear_stringstream);
813  str_unformatted.str(clear_stringstream);
814  const bool cannot_move = m.movement_cost > type_.movement(); // cannot move in this terrain
815  double movement_red_to_green = 100.0 - 25.0 * m.movement_cost;
816 
817  // passing true to select the less saturated red-to-green scale
818  std::string movement_color = game_config::red_to_green(movement_red_to_green, true).to_hex_string();
819  str << "<format>color='" << movement_color << "' text='";
820  // A 5 MP margin; if the movement costs go above
821  // the unit's max moves + 5, we replace it with dashes.
822  if(cannot_move && (m.movement_cost > type_.movement() + 5)) {
823  str_unformatted << font::unicode_figure_dash;
824  } else if(cannot_move) {
825  str_unformatted << "(" << m.movement_cost << ")";
826  } else {
827  str_unformatted << m.movement_cost;
828  }
829  if(m.movement_cost != 0) {
830  const int movement_hexes_per_turn = type_.movement() / m.movement_cost;
831  str_unformatted << " ";
832  for(int i = 0; i < movement_hexes_per_turn; ++i) {
833  // Unicode horizontal black hexagon and Unicode zero width space (to allow a line break)
834  str_unformatted << "\u2b23\u200b";
835  }
836  }
837  str << str_unformatted.str() << "'</format>";
838  row.emplace_back(str.str(), font::pango_line_width(str_unformatted.str(), normal_font_size));
839 
840  //defense cap
841  if (has_terrain_defense_caps) {
842  str.str(clear_stringstream);
843  str_unformatted.str(clear_stringstream);
844  if (m.defense_cap) {
845  str << "<format>color='"<< color <<"' text='" << m.defense << "%'</format>";
846  str_unformatted << m.defense << "%";
847  } else {
848  str << "<format>color=white text='" << font::unicode_figure_dash << "'</format>";
849  str_unformatted << font::unicode_figure_dash;
850  }
851  row.emplace_back(str.str(), font::pango_line_width(str_unformatted.str(), normal_font_size));
852  }
853 
854  //vision
855  if (has_vision) {
856  str.str(clear_stringstream);
857  str_unformatted.str(clear_stringstream);
858  const bool cannot_view = m.vision_cost > type_.vision(); // cannot view in this terrain
859  double vision_red_to_green = 100.0 - 25.0 * m.vision_cost;
860 
861  // passing true to select the less saturated red-to-green scale
862  std::string vision_color = game_config::red_to_green(vision_red_to_green, true).to_hex_string();
863  str << "<format>color='" << vision_color << "' text='";
864  // A 5 MP margin; if the vision costs go above
865  // the unit's vision + 5, we replace it with dashes.
866  if(cannot_view && (m.vision_cost > type_.vision() + 5)) {
867  str_unformatted << font::unicode_figure_dash;
868  } else if(cannot_view) {
869  str_unformatted << "(" << m.vision_cost << ")";
870  } else {
871  str_unformatted << m.vision_cost;
872  }
873  if(m.vision_cost != 0) {
874  const int vision_hexes_per_turn = type_.vision() / m.vision_cost;
875  str_unformatted << " ";
876  for(int i = 0; i < vision_hexes_per_turn; ++i) {
877  // Unicode horizontal black hexagon and Unicode zero width space (to allow a line break)
878  str_unformatted << "\u2b23\u200b";
879  }
880  }
881  str << str_unformatted.str() << "'</format>";
882  row.emplace_back(str.str(), font::pango_line_width(str_unformatted.str(), normal_font_size));
883  }
884 
885  //jamming
886  if (has_jamming) {
887  str.str(clear_stringstream);
888  str_unformatted.str(clear_stringstream);
889  const bool cannot_jam = m.jamming_cost > type_.jamming(); // cannot jam in this terrain
890  double jamming_red_to_green = 100.0 - 25.0 * m.jamming_cost;
891 
892  // passing true to select the less saturated red-to-green scale
893  std::string jamming_color = game_config::red_to_green(jamming_red_to_green, true).to_hex_string();
894  str << "<format>color='" << jamming_color << "' text='";
895  // A 5 MP margin; if the jamming costs go above
896  // the unit's jamming + 5, we replace it with dashes.
897  if (cannot_jam && m.jamming_cost > type_.jamming() + 5) {
898  str_unformatted << font::unicode_figure_dash;
899  } else if(cannot_jam) {
900  str_unformatted << "(" << m.jamming_cost << ")";
901  } else {
902  str_unformatted << m.jamming_cost;
903  }
904  if(m.jamming_cost != 0) {
905  const int jamming_hexes_per_turn = type_.jamming() / m.jamming_cost;
906  str_unformatted << " ";
907  for(int i = 0; i < jamming_hexes_per_turn; ++i) {
908  // Unicode horizontal black hexagon and Unicode zero width space (to allow a line break)
909  str_unformatted << "\u2b23\u200b";
910  }
911  }
912  str << str_unformatted.str() << "'</format>";
913  row.emplace_back(str.str(), font::pango_line_width(str_unformatted.str(), normal_font_size));
914  }
915 
916  table.push_back(row);
917  }
918 
919  ss << generate_table(table);
920  } else {
921  WRN_HP << "When building unit help topics, the display object was null and we couldn't get the terrain info we need.";
922  }
923  return ss.str();
924 }
925 
926 void unit_topic_generator::push_header(std::vector< item > &row, const std::string& name) const {
928 }
929 
930 } // 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:94
bool operator<(const terrain_movement_info &other) const
const std::string unit_prefix
Definition: help_impl.cpp:88
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:1556
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:1246
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:87
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:182
const std::string & flag_rgb() const
Definition: types.cpp:720
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:1465
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
#define VNGETTEXT(msgid, msgid_plural, count,...)
const int normal_font_size
Definition: help_impl.cpp:80
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:871
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:1546
static std::string _(const char *str)
Definition: gettext.hpp:93
const std::string terrain_prefix
Definition: help_impl.cpp:89
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:406
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:1238
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:1570
std::vector< std::vector< help::item > > table_spec
Definition: help_impl.hpp:416
bool hide_help() const
Definition: types.cpp:624
std::shared_ptr< terrain_type_data > load_terrain_types_data()
Load the appropriate terrain types data to use.
Definition: help_impl.cpp:1619
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:347
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:922
t_translation::ter_list::const_iterator ter_iter
std::map< std::string, t_string, res_compare > string_map_res
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:93
std::vector< std::string > variations() const
Definition: types.cpp:745
std::string resistance_color(const int resistance)
Maps resistance <= -60 (resistance value <= -60%) to intense red.
Definition: helper.cpp:50
std::string make_link(const std::string &text, const std::string &dst)
Definition: help_impl.hpp:386
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:762
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:78
point game_canvas_size()
The size of the game canvas, in drawing coordinates / game pixels.
Definition: video.cpp:422
unsigned screen_width
The screen resolution and pixel pitch should be available for all widgets since their drawing method ...
Definition: settings.cpp:27
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:1613
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:399
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:474
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:526
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
utils::string_map_res damage_table() const
Returns a map from attack types to resistances.
Definition: movetype.hpp:302
const unit_type & get_gender_unit_type(std::string gender) const
Returns a gendered variant of this unit_type.
Definition: types.cpp:453
color_t red_to_green(double val, bool for_text)
Return a color corresponding to the value val red for val=0.0 to green for val=100.0, passing by yellow.
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:1104
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:414
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.