The Battle for Wesnoth  1.19.7+dev
help_topic_generators.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2024
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 "formula/string_utils.hpp" // for VNGETTEXT
21 #include "game_config.hpp" // for debug, menu_contract, etc
22 #include "gettext.hpp" // for _, gettext, N_
23 #include "language.hpp" // for string_table, symbol_table
24 #include "log.hpp" // for LOG_STREAM, logger, etc
25 #include "movetype.hpp" // for movetype, movetype::effects, etc
26 #include "preferences/preferences.hpp" // for encountered_terrains, etc
27 #include "units/race.hpp" // for unit_race, etc
28 #include "serialization/markup.hpp" // for markup related utilities
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 "utils/optional_fwd.hpp"
36 #include "video.hpp" // fore current_resolution
37 
38 #include <set>
39 
40 static lg::log_domain log_help("help");
41 #define WRN_HP LOG_STREAM(warn, log_help)
42 #define DBG_HP LOG_STREAM(debug, log_help)
43 
44 namespace help {
45 
47 {
48  const t_string name;
49  const std::string id;
50  const int defense;
51  const int movement_cost;
52  const int vision_cost;
53  const int jamming_cost;
54  const bool defense_cap;
55 
56  bool operator<(const terrain_movement_info& other) const
57  {
58  return translation::icompare(name, other.name) < 0;
59  }
60 };
61 
62 static std::string best_str(bool best) {
63  std::string lang_policy = (best ? _("Best of") : _("Worst of"));
64  std::string color_policy = (best ? "green": "red");
65 
66  return markup::span_color(color_policy, lang_policy);
67 }
68 
69 static std::string format_mp_entry(const int cost, const int max_cost) {
70  std::stringstream str_unformatted;
71  const bool cannot = cost < max_cost;
72 
73  // passing true to select the less saturated red-to-green scale
74  color_t color = game_config::red_to_green(100.0 - 25.0 * max_cost, true);
75 
76  // A 5 point margin; if the costs go above
77  // the unit's mp cost + 5, we replace it with dashes.
78  if (cannot && max_cost > cost + 5) {
79  str_unformatted << font::unicode_figure_dash;
80  } else if(cannot) {
81  str_unformatted << "(" << max_cost << ")";
82  } else {
83  str_unformatted << max_cost;
84  }
85  if(max_cost != 0) {
86  const int hexes_per_turn = cost / max_cost;
87  str_unformatted << " ";
88  for(int i = 0; i < hexes_per_turn; ++i) {
89  // Unicode horizontal black hexagon and Unicode zero width space (to allow a line break)
90  str_unformatted << "\u2b23\u200b";
91  }
92  }
93 
94  return markup::span_color(color, str_unformatted.str());
95 }
96 
97 typedef t_translation::ter_list::const_iterator ter_iter;
98 // Gets an english description of a terrain ter_list alias behavior: "Best of cave, hills", "Worst of Swamp, Forest" etc.
99 static std::string print_behavior_description(const ter_iter& start, const ter_iter& end, const std::shared_ptr<terrain_type_data> & tdata, bool first_level = true, bool begin_best = true)
100 {
101 
102  if (start == end) return "";
103  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.
104 
105  utils::optional<ter_iter> last_change_pos;
106 
107  bool best = begin_best;
108  for (ter_iter i = start; i != end; ++i) {
109  if ((best && *i == t_translation::MINUS) || (!best && *i == t_translation::PLUS)) {
110  best = !best;
111  last_change_pos = i;
112  }
113  }
114 
115  std::stringstream ss;
116 
117  if (!last_change_pos) {
118  std::vector<std::string> names;
119  for (ter_iter i = start; i != end; ++i) {
120  if (*i == t_translation::BASE) {
121  // TRANSLATORS: in a description of an overlay terrain, the terrain that it's placed on
122  names.push_back(_("base terrain"));
123  } else {
124  const terrain_type tt = tdata->get_terrain_info(*i);
125  if (!tt.editor_name().empty())
126  names.push_back(tt.editor_name());
127  }
128  }
129 
130  if (names.empty()) return "";
131  if (names.size() == 1) return names.at(0);
132 
133  ss << best_str(best) << " ";
134  if (!first_level) ss << "( ";
135  ss << names.at(0);
136 
137  for (std::size_t i = 1; i < names.size(); i++) {
138  ss << ", " << names.at(i);
139  }
140 
141  if (!first_level) ss << " )";
142  } else {
143  std::vector<std::string> names;
144  for (ter_iter i = *last_change_pos+1; i != end; ++i) {
145  const terrain_type tt = tdata->get_terrain_info(*i);
146  if (!tt.editor_name().empty())
147  names.push_back(tt.editor_name());
148  }
149 
150  if (names.empty()) { //This alias list is apparently padded with junk at the end, so truncate it without adding more parens
151  return print_behavior_description(start, *last_change_pos, tdata, first_level, begin_best);
152  }
153 
154  ss << best_str(best) << " ";
155  if (!first_level) ss << "( ";
156  ss << print_behavior_description(start, *last_change_pos-1, tdata, false, begin_best);
157  // Printed the (parenthesized) leading part from before the change, now print the remaining names in this group.
158  for (const std::string & s : names) {
159  ss << ", " << s;
160  }
161  if (!first_level) ss << " )";
162  }
163  return ss.str();
164 }
165 
167  std::stringstream ss;
168 
169  if (!type_.icon_image().empty()) {
170  ss << markup::img(formatter()
171  << "images/buttons/icon-base-32.png~RC(magenta>" << type_.id()
172  << ")~BLIT(" << "terrain/" << type_.icon_image() << "_30.png)");
173  }
174 
175  if (!type_.editor_image().empty()) {
177  }
178 
179  if (!type_.help_topic_text().empty()) {
180  ss << "\n" << type_.help_topic_text().str() << "\n";
181  } else {
182  ss << "\n";
183  }
184 
185  std::shared_ptr<terrain_type_data> tdata = load_terrain_types_data();
186 
187  if (!tdata) {
188  WRN_HP << "When building terrain help topics, we couldn't acquire any terrain types data";
189  return ss.str();
190  }
191 
192  // Special notes are generated from the terrain's properties - at the moment there's no way for WML authors
193  // to add their own via a [special_note] tag.
194  std::vector<std::string> special_notes;
195 
196  if(type_.is_village()) {
197  special_notes.push_back(_("Villages allow any unit stationed therein to heal, or to be cured of poison."));
198  } else if(type_.gives_healing() > 0) {
199  auto symbols = utils::string_map{{"amount", std::to_string(type_.gives_healing())}};
200  // TRANSLATORS: special note for terrains such as the oasis; the only terrain in core with this property heals 8 hp just like a village.
201  // 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.
202  auto message = VNGETTEXT("This terrain allows units to be cured of poison, or to heal a single hitpoint.",
203  "This terrain allows units to heal $amount hitpoints, or to be cured of poison, as if stationed in a village.",
204  type_.gives_healing(), symbols);
205  special_notes.push_back(std::move(message));
206  }
207 
208  if(type_.is_castle()) {
209  special_notes.push_back(_("This terrain is a castle — units can be recruited onto it from a connected keep."));
210  }
211  if(type_.is_keep() && type_.is_castle()) {
212  // TRANSLATORS: The "this terrain is a castle" note will also be shown directly above this one.
213  special_notes.push_back(_("This terrain is a keep — a leader can recruit from this hex onto connected castle hexes."));
214  } else if(type_.is_keep() && !type_.is_castle()) {
215  // TRANSLATORS: Special note for a terrain, but none of the terrains in mainline do this.
216  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."));
217  }
218 
219  if(!special_notes.empty()) {
220  ss << "\n\n" << markup::tag("header", _("Special Notes")) << "\n\n";
221  for(const auto& note : special_notes) {
222  ss << font::unicode_bullet << " " << note << '\n';
223  }
224  }
225 
226  // Almost all terrains will show the data in this conditional block. The ones that don't are the
227  // archetypes used in [movetype]'s subtags such as [movement_costs].
228  if (!type_.is_indivisible()) {
229  std::vector<t_string> underlying;
230  for (const auto& underlying_terrain : type_.union_type()) {
231  const terrain_type& base = tdata->get_terrain_info(underlying_terrain);
232  if (!base.editor_name().empty()) {
233  underlying.push_back(markup::make_link(base.editor_name(), ".." + terrain_prefix + base.id()));
234  }
235  }
236  utils::string_map symbols;
237  symbols["types"] = utils::format_conjunct_list("", underlying);
238  // TRANSLATORS: $types is a conjunct list, typical values will be "Castle" or "Flat and Shallow Water".
239  // The terrain names will be hypertext links to the help page of the corresponding terrain type.
240  // There will always be at least 1 item in the list, but unlikely to be more than 3.
241  ss << "\n" << VNGETTEXT("Basic terrain type: $types", "Basic terrain types: $types", underlying.size(), symbols);
242 
243  if (type_.has_default_base()) {
244  const terrain_type& base = tdata->get_terrain_info(type_.default_base());
245 
246  symbols.clear();
247  if (base.is_indivisible()) {
248  symbols["type"] = markup::make_link(base.editor_name(), ".." + terrain_prefix + base.id());
249  } else {
250  symbols["type"] = markup::make_link(base.editor_name(), terrain_prefix + base.id());
251  }
252  // TRANSLATORS: In the help for a terrain type, for example Dwarven Village is often placed on Cave Floor
253  ss << "\n" << VGETTEXT("Typical base terrain: $type", symbols);
254  }
255 
256  ss << "\n";
257 
258  const t_translation::ter_list& underlying_mvt_terrains = type_.mvt_type();
259  ss << "\n" << _("Movement properties: ");
260  ss << print_behavior_description(underlying_mvt_terrains.begin(), underlying_mvt_terrains.end(), tdata) << "\n";
261 
262  const t_translation::ter_list& underlying_def_terrains = type_.def_type();
263  ss << "\n" << _("Defense properties: ");
264  ss << print_behavior_description(underlying_def_terrains.begin(), underlying_def_terrains.end(), tdata) << "\n";
265  }
266 
267  if (game_config::debug) {
268 
269  ss << "\n";
270  ss << "ID: " << type_.id() << "\n";
271 
272  ss << "Village: " << (type_.is_village() ? "Yes" : "No") << "\n";
273  ss << "Gives Healing: " << type_.gives_healing() << "\n";
274 
275  ss << "Keep: " << (type_.is_keep() ? "Yes" : "No") << "\n";
276  ss << "Castle: " << (type_.is_castle() ? "Yes" : "No") << "\n";
277 
278  ss << "Overlay: " << (type_.is_overlay() ? "Yes" : "No") << "\n";
279  ss << "Combined: " << (type_.is_combined() ? "Yes" : "No") << "\n";
280  ss << "Nonnull: " << (type_.is_nonnull() ? "Yes" : "No") << "\n";
281 
282  ss << "Terrain string: " << type_.number() << "\n";
283 
284  ss << "Hide in Editor: " << (type_.hide_in_editor() ? "Yes" : "No") << "\n";
285  ss << "Editor Group: " << type_.editor_group() << "\n";
286 
287  ss << "Light Bonus: " << type_.light_bonus(0) << "\n";
288 
289  ss << type_.income_description();
290 
291  if (type_.editor_image().empty()) { // Note: this is purely temporary to help make a different help entry
292  ss << "\nEditor Image: Empty\n";
293  } else {
294  ss << "\nEditor Image: " << type_.editor_image() << "\n";
295  }
296 
297  const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number());
298  ss << "\nDebug Mvt Description String:";
299  for (const t_translation::terrain_code & t : underlying_mvt_terrains) {
300  ss << " " << t;
301  }
302 
303  const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number());
304  ss << "\nDebug Def Description String:";
305  for (const t_translation::terrain_code & t : underlying_def_terrains) {
306  ss << " " << t;
307  }
308 
309  }
310 
311  return ss.str();
312 }
313 
314 //Typedef to help with formatting list of traits
315 // Maps localized trait name to trait help topic ID
316 typedef std::pair<std::string, std::string> trait_data;
317 
318 //Helper function for printing a list of trait data
319 static void print_trait_list(std::stringstream & ss, const std::vector<trait_data> & l)
320 {
321  std::size_t i = 0;
322  ss << markup::make_link(l[i].first, l[i].second);
323 
324  // This doesn't skip traits with empty names
325  for(i++; i < l.size(); i++) {
326  ss << ", " << markup::make_link(l[i].first,l[i].second);
327  }
328 }
329 
330 std::string unit_topic_generator::operator()() const {
331  // Force the lazy loading to build this unit.
333 
334  std::stringstream ss;
335  const std::string detailed_description = type_.unit_description();
338 
339  const int screen_width = video::game_canvas_size().x;
340 
341  ss << _("Level") << " " << type_.level();
342 
343  // Portraits
344  const std::string &male_portrait = male_type.small_profile().empty() ?
345  male_type.big_profile() : male_type.small_profile();
346  const std::string &female_portrait = female_type.small_profile().empty() ?
347  female_type.big_profile() : female_type.small_profile();
348 
349  const bool has_male_portrait = !male_portrait.empty() && male_portrait != male_type.image() && male_portrait != "unit_image";
350  const bool has_female_portrait = !female_portrait.empty() && female_portrait != male_portrait && female_portrait != female_type.image() && female_portrait != "unit_image";
351 
352  // TODO: figure out why the second checks don't match but the last does
353  if (has_male_portrait) {
354  ss << markup::img(male_portrait + "~FL(horiz)", "right", true);
355  }
356 
357  if (has_female_portrait) {
358  ss << markup::img(female_portrait + "~FL(horiz)", "right", true);
359  }
360 
361  // Unit Images
362  ss << markup::img(formatter()
363  << male_type.image() << "~RC(" << male_type.flag_rgb() << ">red)"
364  << (screen_width >= 1200 ? "~SCALE_SHARP(200%,200%)" : ""));
365 
366  if (female_type.image() != male_type.image()) {
367  ss << markup::img(formatter()
368  << female_type.image() << "~RC(" << female_type.flag_rgb() << ">red)"
369  << (screen_width >= 1200 ? "~SCALE_SHARP(200%,200%)" : ""));
370  }
371 
372  ss << "\n";
373 
374  // Print cross-references to units that this unit advances from/to.
375  // Cross reference to the topics containing information about those units.
376  const bool first_reverse_value = true;
377  bool reverse = first_reverse_value;
378  if (variation_.empty()) {
379  do {
380  std::vector<std::string> adv_units =
382  bool first = true;
383 
384  for (const std::string &adv : adv_units) {
386  if (!type || type->hide_help()) {
387  continue;
388  }
389 
390  if (first) {
391  if (reverse) {
392  ss << _("Advances from: ");
393  } else {
394  ss << _("Advances to: ");
395  }
396  first = false;
397  } else {
398  ss << ", ";
399  }
400 
401  std::string lang_unit = type->type_name();
402  std::string ref_id;
404  const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
405  ref_id = section_prefix + unit_prefix + type->id();
406  } else {
407  ref_id = unknown_unit_topic;
408  lang_unit += " (?)";
409  }
410  ss << markup::make_link(lang_unit, ref_id);
411  }
412  if (!first) {
413  ss << "\n";
414  }
415 
416  reverse = !reverse; //switch direction
417  } while(reverse != first_reverse_value); // don't restart
418  }
419 
420  const unit_type* parent = variation_.empty() ? &type_ :
422  if (!variation_.empty()) {
423  ss << _("Base unit: ") << markup::make_link(parent->type_name(), ".." + unit_prefix + type_.id()) << "\n";
424  } else {
425  bool first = true;
426  for (const std::string& base_id : utils::split(type_.get_cfg()["base_ids"])) {
427  if (first) {
428  ss << _("Base units: ");
429  first = false;
430  }
431  const unit_type* base_type = unit_types.find(base_id, unit_type::HELP_INDEXED);
432  const std::string section_prefix = base_type->show_variations_in_help() ? ".." : "";
433  ss << markup::make_link(base_type->type_name(), section_prefix + unit_prefix + base_id) << "\n";
434  }
435  }
436 
437  bool first = true;
438  for (const std::string &var_id : parent->variations()) {
439  const unit_type &type = parent->get_variation(var_id);
440 
441  if(type.hide_help()) {
442  continue;
443  }
444 
445  if (first) {
446  ss << _("Variations: ");
447  first = false;
448  } else {
449  ss << ", ";
450  }
451 
452  std::string ref_id;
453 
454  std::string var_name = type.variation_name();
456  ref_id = variation_prefix + type.id() + "_" + var_id;
457  } else {
458  ref_id = unknown_unit_topic;
459  var_name += " (?)";
460  }
461 
462  ss << markup::make_link(var_name, ref_id);
463  }
464 
465  // Print the race of the unit, cross-reference it to the respective topic.
466  const std::string race_id = type_.race_id();
467  std::string race_name = type_.race()->plural_name();
468  if (race_name.empty()) {
469  race_name = _ ("race^Miscellaneous");
470  }
471  ss << _("Race: ");
472  ss << markup::make_link(race_name, "..race_" + race_id);
473  ss << "\n";
474 
475  // Print the possible traits of the unit, cross-reference them
476  // to their respective topics.
478  std::vector<trait_data> must_have_traits;
479  std::vector<trait_data> random_traits;
480  int must_have_nameless_traits = 0;
481 
482  for(const config& trait : traits) {
483  const std::string& male_name = trait["male_name"].str();
484  const std::string& female_name = trait["female_name"].str();
485  std::string trait_name;
486  if (type_.has_gender_variation(unit_race::MALE) && ! male_name.empty())
487  trait_name = male_name;
488  else if (type_.has_gender_variation(unit_race::FEMALE) && ! female_name.empty())
489  trait_name = female_name;
490  else if (! trait["name"].str().empty())
491  trait_name = trait["name"].str();
492  else
493  continue; // Hidden trait
494 
495  std::string lang_trait_name = translation::gettext(trait_name.c_str());
496  if (lang_trait_name.empty() && trait["availability"].str() == "musthave") {
497  ++must_have_nameless_traits;
498  continue;
499  }
500  const std::string ref_id = "traits_"+trait["id"].str();
501  ((trait["availability"].str() == "musthave") ? must_have_traits : random_traits).emplace_back(lang_trait_name, ref_id);
502  }
503 
504  bool line1 = !must_have_traits.empty();
505  bool line2 = !random_traits.empty() && type_.num_traits() > must_have_traits.size();
506 
507  if (line1) {
508  std::string traits_label = _("Traits");
509  ss << traits_label;
510  if (line2) {
511  std::stringstream must_have_count;
512  must_have_count << "\n (" << must_have_traits.size() << ") : ";
513  std::stringstream random_count;
514  random_count << " (" << (type_.num_traits() - must_have_traits.size() - must_have_nameless_traits) << ") : ";
515  ss << must_have_count.str();
516  print_trait_list(ss, must_have_traits);
517  ss << "\n" << random_count.str();
518  print_trait_list(ss, random_traits);
519  } else {
520  ss << ": ";
521  print_trait_list(ss, must_have_traits);
522  }
523  ss << "\n";
524  } else {
525  if (line2) {
526  ss << _("Traits") << " (" << (type_.num_traits() - must_have_nameless_traits) << ") : ";
527  print_trait_list(ss, random_traits);
528  ss << "\n";
529  }
530  }
531  }
532 
533  // Print the abilities the units has, cross-reference them
534  // to their respective topics. TODO: Update this according to musthave trait effects, similar to movetype below
535  if(!type_.abilities_metadata().empty()) {
536  ss << _("Abilities: ");
537 
538  bool start = true;
539 
540  for(auto iter = type_.abilities_metadata().begin(); iter != type_.abilities_metadata().end(); ++iter) {
541  const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
542 
543  if(iter->name.empty()) {
544  continue;
545  }
546 
547  if(!start) {
548  ss << ", ";
549  } else {
550  start = false;
551  }
552 
553  std::string lang_ability = translation::gettext(iter->name.c_str());
554  ss << markup::make_link(lang_ability, ref_id);
555  }
556 
557  ss << "\n\n";
558  }
559 
560  // Print the extra AMLA upgrade abilities, cross-reference them to their respective topics.
561  if(!type_.adv_abilities_metadata().empty()) {
562  ss << _("Ability Upgrades: ");
563 
564  bool start = true;
565 
566  for(auto iter = type_.adv_abilities_metadata().begin(); iter != type_.adv_abilities_metadata().end(); ++iter) {
567  const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
568 
569  if(iter->name.empty()) {
570  continue;
571  }
572 
573  if(!start) {
574  ss << ", ";
575  } else {
576  start = false;
577  }
578 
579  std::string lang_ability = translation::gettext(iter->name.c_str());
580  ss << markup::make_link(lang_ability, ref_id);
581  }
582 
583  ss << "\n\n";
584  }
585 
586  // Print some basic information such as HP and movement points.
587  // TODO: Make this info update according to musthave traits, similar to movetype below.
588 
589  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
590  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
591  // unpleasant line breaks (issue #3256).
592  ss << _("HP:") << font::nbsp << type_.hitpoints() << " "
593  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
594  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
595  // unpleasant line breaks (issue #3256).
596  << _("Moves:") << font::nbsp << type_.movement() << " ";
597  if (type_.vision() != type_.movement()) {
598  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
599  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
600  // unpleasant line breaks (issue #3256).
601  ss << _("Vision:") << font::nbsp << type_.vision() << " ";
602  }
603  if (type_.jamming() > 0) {
604  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
605  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
606  // unpleasant line breaks (issue #3256).
607  ss << _("Jamming:") << font::nbsp << type_.jamming() << " ";
608  }
609  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
610  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
611  // unpleasant line breaks (issue #3256).
612  ss << _("Cost:") << font::nbsp << type_.cost() << " "
613  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
614  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
615  // unpleasant line breaks (issue #3256).
616  << _("Alignment:") << font::nbsp
617  << markup::make_link(type_.alignment_description(type_.alignment(), type_.genders().front()), "time_of_day")
618  << " ";
620  // TRANSLATORS: This string is used in the help page of a single unit. It uses
621  // non-breaking spaces to prevent unpleasant line breaks (issue #3256). In the
622  // translation use non-breaking spaces as appropriate for the target language.
623  ss << _("Required\u00a0XP:") << font::nbsp << type_.experience_needed();
624  }
625 
626  // Print the detailed description about the unit.
627  ss << "\n" << detailed_description;
628 
629  if(const auto notes = type_.special_notes(); !notes.empty()) {
630  ss << "\n" << markup::tag("header", _("Special Notes")) << "\n";
631  for(const auto& note : notes) {
632  ss << font::unicode_bullet << " " << markup::italic(note) << '\n';
633  }
634  }
635 
636  std::stringstream table_ss;
637 
638  //
639  // Attacks table
640  //
641  ss << "\n" << markup::tag("header", _("Attacks"));
642 
643  if (!type_.attacks().empty()) {
644  // Print headers for the table.
645  table_ss << markup::tag("row",
646  markup::tag("col", markup::bold(_("Icon"))),
647  markup::tag("col", markup::bold(_("Name"))),
648  markup::tag("col", markup::bold(_("Strikes"))),
649  markup::tag("col", markup::bold(_("Range"))),
650  markup::tag("col", markup::bold(_("Type"))),
651  markup::tag("col", markup::bold(_("Special"))));
652 
653  // Print information about every attack.
654  for(const attack_type& attack : type_.attacks()) {
655  std::stringstream attack_ss;
656 
657  std::string lang_weapon = attack.name();
658  std::string lang_type = string_table["type_" + attack.type()];
659 
660  // Attack icon
661  attack_ss << markup::tag("col", markup::img(attack.icon()));
662 
663  // attack name
664  attack_ss << markup::tag("col", lang_weapon);
665 
666  // damage x strikes
667  attack_ss << markup::tag("col",
668  attack.damage(), font::weapon_numbers_sep, attack.num_attacks(),
669  " ", attack.accuracy_parry_description());
670 
671  // range
672  const std::string range_icon = "icons/profiles/" + attack.range() + "_attack.png~SCALE_INTO(16,16)";
673  if (attack.min_range() > 1 || attack.max_range() > 1) {
674  attack_ss << markup::tag("col",
675  markup::img(range_icon),
676  attack.min_range(), "-", attack.max_range(), ' ',
677  string_table["range_" + attack.range()]);
678  } else {
679  attack_ss << markup::tag("col",
680  markup::img(range_icon),
681  string_table["range_" + attack.range()]);
682  }
683 
684  // type
685  const std::string type_icon = "icons/profiles/" + attack.type() + ".png~SCALE_INTO(16,16)";
686  attack_ss << markup::tag("col", markup::img(type_icon), lang_type);
687 
688  // special
689  std::vector<std::pair<t_string, t_string>> specials = attack.special_tooltips();
690  if (!specials.empty()) {
691  std::stringstream specials_ss;
692  std::string lang_special = "";
693  const std::size_t specials_size = specials.size();
694  for (std::size_t i = 0; i != specials_size; ++i) {
695  const std::string ref_id = std::string("weaponspecial_")
696  + specials[i].first.base_str();
697  lang_special = (specials[i].first);
698  specials_ss << markup::make_link(lang_special, ref_id);
699  if (i+1 != specials_size) {
700  specials_ss << ", "; //comma placed before next special
701  }
702  }
703  attack_ss << markup::tag("col", specials_ss.str());
704  } else {
705  attack_ss << markup::tag("col", "none");
706  }
707 
708  table_ss << markup::tag("row", attack_ss.str());
709  }
710 
711  ss << markup::tag("table", table_ss.str());
712  }
713 
714  // Generate the movement type of the unit,
715  // with resistance, defense, movement, jamming and vision data
716  // updated according to any 'musthave' traits which always apply.
717  movetype movement_type = type_.movement_type();
719  if (!traits.empty() && type_.num_traits() > 0) {
720  for (const config & t : traits) {
721  if (t["availability"].str() == "musthave") {
722  for (const config & effect : t.child_range("effect")) {
723  if (!effect.has_child("filter") // If this is musthave but has a unit filter, it might not always apply, so don't apply it in the help.
724  && movetype::effects.find(effect["apply_to"].str()) != movetype::effects.end()) {
725  movement_type.merge(effect, effect["replace"].to_bool());
726  }
727  }
728  }
729  }
730  }
731 
732  const bool has_terrain_defense_caps = movement_type.has_terrain_defense_caps(prefs::get().encountered_terrains());
733  const bool has_vision = type_.movement_type().has_vision_data();
734  const bool has_jamming = type_.movement_type().has_jamming_data();
735 
736  //
737  // Resistances table
738  //
739  ss << "\n" << markup::tag("header", _("Resistances"));
740 
741  std::stringstream().swap(table_ss);
742  table_ss << markup::tag("row",
743  markup::tag("col", markup::bold(_("Attack Type"))),
744  markup::tag("col", markup::bold(_("Resistance"))));
745 
746  utils::string_map_res dam_tab = movement_type.damage_table();
747  for(std::pair<std::string, std::string> dam_it : dam_tab) {
748  int resistance = 100;
749  try {
750  resistance -= std::stoi(dam_it.second);
751  } catch(std::invalid_argument&) {}
752  std::string resist = std::to_string(resistance) + '%';
753  const std::size_t pos = resist.find('-');
754  if (pos != std::string::npos) {
755  resist.replace(pos, 1, font::unicode_minus);
756  }
757  std::string color = unit_helper::resistance_color(resistance);
758  const std::string lang_type = string_table["type_" + dam_it.first];
759  const std::string type_icon = "icons/profiles/" + dam_it.first + ".png~SCALE_INTO(16,16)";
760  table_ss << markup::tag("row",
761  markup::tag("col", markup::img(type_icon), lang_type),
762  markup::tag("col", markup::span_color(color, resist)));
763  }
764  ss << markup::tag("table", table_ss.str());
765 
766  //
767  // Terrain Modifiers table
768  //
769  std::stringstream().swap(table_ss);
770  if (std::shared_ptr<terrain_type_data> tdata = load_terrain_types_data()) {
771  // Print the terrain modifier table of the unit.
772  ss << "\n" << markup::tag("header", _("Terrain Modifiers"));
773 
774  // Header row
775  std::stringstream row_ss;
776  row_ss << markup::tag("col", markup::bold(_("Terrain")));
777  row_ss << markup::tag("col", markup::bold(_("Defense")));
778  row_ss << markup::tag("col", markup::bold(_("Movement Cost")));
779  if (has_terrain_defense_caps) { row_ss << markup::tag("col", markup::bold(_("Defense Cap"))); }
780  if (has_vision) { row_ss << markup::tag("col", markup::bold(_("Vision Cost"))); }
781  if (has_jamming) { row_ss << markup::tag("col", markup::bold(_("Jamming Cost"))); }
782  table_ss << markup::tag("row", row_ss.str());
783 
784  // Organize terrain movetype data
785  std::set<terrain_movement_info> terrain_moves;
786  for (t_translation::terrain_code terrain : prefs::get().encountered_terrains()) {
788  continue;
789  }
790  const terrain_type& info = tdata->get_terrain_info(terrain);
791  const int moves = movement_type.movement_cost(terrain);
792  const bool cannot_move = moves > type_.movement();
793  if (cannot_move && info.hide_if_impassable()) {
794  continue;
795  }
796 
797  if (info.is_indivisible() && info.is_nonnull()) {
798  terrain_movement_info movement_info =
799  {
800  info.name(),
801  info.id(),
802  100 - movement_type.defense_modifier(terrain),
803  moves,
804  movement_type.vision_cost(terrain),
805  movement_type.jamming_cost(terrain),
806  movement_type.get_defense().capped(terrain)
807  };
808 
809  terrain_moves.insert(movement_info);
810  }
811  }
812 
813  // Add movement table rows
814  for(const terrain_movement_info &m : terrain_moves)
815  {
816  std::stringstream().swap(row_ss);
817  bool high_res = false;
818  const std::string tc_base = high_res ? "images/buttons/icon-base-32.png" : "images/buttons/icon-base-16.png";
819  const std::string terrain_image = "icons/terrain/terrain_type_" + m.id + (high_res ? "_30.png" : ".png");
820  const std::string final_image = tc_base + "~RC(magenta>" + m.id + ")~BLIT(" + terrain_image + ")";
821 
822  row_ss << markup::tag("col", markup::img(final_image), markup::make_link(m.name, "..terrain_" + m.id));
823 
824  // Defense - range: +10 % .. +70 %
825  // passing false to select the more saturated red-to-green scale
826  color_t def_color = game_config::red_to_green(m.defense, false);
827  row_ss << markup::tag("col", markup::span_color(def_color, m.defense, "%"));
828 
829  // Movement - range: 1 .. 5, movetype::UNREACHABLE=impassable
830  row_ss << markup::tag("col", format_mp_entry(type_.movement(), m.movement_cost));
831 
832  // Defense cap
833  if (has_terrain_defense_caps) {
834  if (m.defense_cap) {
835  row_ss << markup::tag("col", markup::span_color(def_color, m.defense, "%"));
836  } else {
837  row_ss << markup::tag("col", markup::span_color("white", font::unicode_figure_dash));
838  }
839  }
840 
841  // Vision
842  // uses same formatting as MP
843  if (has_vision) {
844  row_ss << markup::tag("col", format_mp_entry(type_.vision(), m.vision_cost));
845  }
846 
847  // Jamming
848  // uses same formatting as MP
849  if (has_jamming) {
850  row_ss << markup::tag("col", format_mp_entry(type_.jamming(), m.jamming_cost));
851  }
852 
853  table_ss << markup::tag("row", row_ss.str());
854  }
855 
856  ss << markup::tag("table", table_ss.str());
857 
858  } else {
859  WRN_HP << "When building unit help topics, we couldn't get the terrain info we need.";
860  }
861 
862  return ss.str();
863 }
864 
865 } // end namespace help
double t
Definition: astarsearch.cpp:63
std::vector< std::string > names
Definition: build_info.cpp:67
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:282
std::ostringstream wrapper.
Definition: formatter.hpp:40
virtual std::string operator()() const
virtual std::string operator()() const
bool capped(const t_translation::terrain_code &terrain) const
Returns whether there is a defense cap associated to this terrain.
Definition: movetype.hpp:196
The basic "size" of the unit - flying, small land, large land, etc.
Definition: movetype.hpp:44
static const std::set< std::string > effects
The set of applicable effects for movement types.
Definition: movetype.hpp:340
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:858
bool has_vision_data() const
Returns whether or not there are any vision-specific costs.
Definition: movetype.hpp:301
bool has_jamming_data() const
Returns whether or not there are any jamming-specific costs.
Definition: movetype.hpp:303
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:850
int defense_modifier(const t_translation::terrain_code &terrain) const
Returns the defensive value of the indicated terrain.
Definition: movetype.hpp:288
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:284
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:281
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:278
utils::string_map_res damage_table() const
Returns a map from damage types to resistances.
Definition: movetype.hpp:295
terrain_defense & get_defense()
Definition: movetype.hpp:263
static prefs & get()
bool empty() const
Definition: tstring.hpp:194
const std::string & str() const
Definition: tstring.hpp:198
const std::string & editor_group() const
Definition: terrain.hpp:152
bool has_default_base() const
Definition: terrain.hpp:177
const std::string & icon_image() const
Definition: terrain.hpp:44
const t_string & income_description() const
Definition: terrain.hpp:147
bool is_combined() const
True for instances created by the terrain_code(base, overlay) constructor.
Definition: terrain.hpp:167
const std::string & editor_image() const
Definition: terrain.hpp:47
bool is_nonnull() const
True if this object represents some sentinel values.
Definition: terrain.hpp:129
bool is_keep() const
Definition: terrain.hpp:143
bool is_castle() const
Definition: terrain.hpp:142
const std::string & id() const
Definition: terrain.hpp:52
const t_string & help_topic_text() const
Definition: terrain.hpp:51
bool is_village() const
Definition: terrain.hpp:141
const t_translation::ter_list & def_type() const
Definition: terrain.hpp:76
const t_translation::ter_list & mvt_type() const
The underlying type of the terrain.
Definition: terrain.hpp:75
int light_bonus(int base) const
Returns the light (lawful) bonus for this terrain when the time of day gives a base bonus.
Definition: terrain.hpp:132
const t_translation::ter_list & union_type() const
Definition: terrain.hpp:78
bool is_overlay() const
Definition: terrain.hpp:155
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
const t_string & editor_name() const
Definition: terrain.hpp:49
int gives_healing() const
Definition: terrain.hpp:140
t_translation::terrain_code default_base() const
Overlay terrains defined by a [terrain_type] can declare a fallback base terrain, for use when the ov...
Definition: terrain.hpp:176
bool hide_in_editor() const
Definition: terrain.hpp:62
t_translation::terrain_code number() const
Definition: terrain.hpp:66
const t_string & plural_name() const
Definition: race.hpp:39
@ FEMALE
Definition: race.hpp:28
@ MALE
Definition: race.hpp:28
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:1265
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:1257
A single unit type that the player may recruit.
Definition: types.hpp:43
bool can_advance() const
Definition: types.hpp:222
const std::vector< std::string > advances_from() const
A vector of unit_type ids that can advance to this unit_type.
Definition: types.cpp:654
std::string race_id() const
Returns the ID of this type's race without the need to build the type.
Definition: types.hpp:272
static std::string alignment_description(unit_alignments::type align, unit_race::GENDER gender=unit_race::MALE)
Implementation detail of unit_type::alignment_description.
Definition: types.cpp:841
const unit_type & get_gender_unit_type(const std::string &gender) const
Returns a gendered variant of this unit_type.
Definition: types.cpp:455
const std::string & image() const
Definition: types.hpp:176
const std::string & id() const
The id for this unit_type.
Definition: types.hpp:141
const std::vector< ability_metadata > & adv_abilities_metadata() const
Some extra abilities that may be gained through AMLA advancements.
Definition: types.hpp:220
const movetype & movement_type() const
Definition: types.hpp:189
bool show_variations_in_help() const
Whether the unit type has at least one help-visible variation.
Definition: types.cpp:764
const unit_race * race() const
Never returns nullptr, but may point to the null race.
Definition: types.hpp:277
int hitpoints() const
Definition: types.hpp:161
const unit_type & get_variation(const std::string &id) const
Definition: types.cpp:476
const_attack_itors attacks() const
Definition: types.cpp:545
const std::vector< std::string > & advances_to() const
A vector of unit_type ids that this unit_type can advance to.
Definition: types.hpp:115
bool has_gender_variation(const unit_race::GENDER gender) const
Definition: types.hpp:250
int movement() const
Definition: types.hpp:166
t_string unit_description() const
Definition: types.cpp:486
@ HELP_INDEXED
Definition: types.hpp:74
@ FULL
Definition: types.hpp:74
const std::vector< unit_race::GENDER > & genders() const
The returned vector will not be empty, provided this has been built to the HELP_INDEXED status.
Definition: types.hpp:249
std::vector< std::string > variations() const
Definition: types.cpp:747
const std::string & flag_rgb() const
Definition: types.cpp:722
int vision() const
Definition: types.hpp:167
std::vector< t_string > special_notes() const
Returns all notes that should be displayed in the help page for this type, including those found in a...
Definition: types.cpp:495
config::const_child_itors modification_advancements() const
Returns two iterators pointing to a range of AMLA configs.
Definition: types.hpp:120
int cost() const
Definition: types.hpp:172
int experience_needed(bool with_acceleration=true) const
Definition: types.cpp:579
const std::string & big_profile() const
Definition: types.hpp:179
const t_string & type_name() const
The name of the unit in the current language setting.
Definition: types.hpp:138
config::const_child_itors possible_traits() const
Definition: types.hpp:231
int level() const
Definition: types.hpp:164
unit_alignments::type alignment() const
Definition: types.hpp:193
const std::string & small_profile() const
Definition: types.hpp:178
const std::vector< ability_metadata > & abilities_metadata() const
Definition: types.hpp:217
const config & get_cfg() const
Definition: types.hpp:281
unsigned int num_traits() const
Definition: types.hpp:135
int jamming() const
Definition: types.hpp:170
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
#define VNGETTEXT(msgid, msgid_plural, count,...)
std::size_t i
Definition: function.cpp:1029
static std::string _(const char *str)
Definition: gettext.hpp:93
#define WRN_HP
static lg::log_domain log_help("help")
symbol_table string_table
Definition: language.cpp:64
Standard logging facilities (interface).
EXIT_STATUS start(bool clear_id, const std::string &filename, bool take_screenshot, const std::string &screenshot_filename)
Main interface for launching the editor from the title screen.
const std::string nbsp
Definition: constants.cpp:40
const std::string unicode_bullet
Definition: constants.cpp:47
const std::string unicode_figure_dash
Definition: constants.cpp:45
const std::string weapon_numbers_sep
Definition: constants.cpp:49
const std::string unicode_minus
Definition: constants.cpp:42
const bool & debug
Definition: game_config.cpp:94
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....
unsigned screen_width
The screen resolution and pixel pitch should be available for all widgets since their drawing method ...
Definition: settings.cpp:27
@ FULL_DESCRIPTION
Definition: help_impl.hpp:245
t_translation::ter_list::const_iterator ter_iter
const std::string unit_prefix
Definition: help_impl.cpp:84
const std::string variation_prefix
Definition: help_impl.cpp:89
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:1186
const std::string ability_prefix
Definition: help_impl.cpp:90
static std::string print_behavior_description(const ter_iter &start, const ter_iter &end, const std::shared_ptr< terrain_type_data > &tdata, bool first_level=true, bool begin_best=true)
std::pair< std::string, std::string > trait_data
const std::string terrain_prefix
Definition: help_impl.cpp:85
const std::string unknown_unit_topic
Definition: help_impl.cpp:83
std::shared_ptr< terrain_type_data > load_terrain_types_data()
Load the appropriate terrain types data to use.
Definition: help_impl.cpp:1460
static std::string format_mp_entry(const int cost, const int max_cost)
static std::string best_str(bool best)
static void print_trait_list(std::stringstream &ss, const std::vector< trait_data > &l)
logger & info()
Definition: log.cpp:319
std::string italic(Args &&... data)
Definition: markup.hpp:140
static const std::string br
Definition: markup.hpp:33
std::string make_link(const std::string &text, const std::string &dst)
Definition: markup.cpp:23
std::string img(const std::string &src, const std::string &align, const bool floating)
Definition: markup.cpp:29
std::string tag(const std::string &tag_name, Args &&... contents)
Definition: markup.hpp:45
std::string bold(Args &&... data)
Definition: markup.hpp:128
std::string span_color(const color_t &color, Args &&... data)
Definition: markup.hpp:68
const terrain_code VOID_TERRAIN
VOID_TERRAIN is used for shrouded hexes.
const terrain_code MINUS
bool terrain_matches(const terrain_code &src, const terrain_code &dest)
Tests whether a specific terrain matches an expression, for matching rules see above.
std::vector< terrain_code > ter_list
Definition: translation.hpp:77
const terrain_code BASE
const ter_match ALL_OFF_MAP
const terrain_code PLUS
const terrain_code FOGGED
static std::string gettext(const char *str)
Definition: gettext.hpp:60
int icompare(const std::string &s1, const std::string &s2)
Case-insensitive lexicographical comparison.
Definition: gettext.cpp:519
std::string resistance_color(const int resistance)
Maps resistance <= -60 (resistance value <= -60%) to intense red.
Definition: helper.cpp:49
constexpr auto reverse
Definition: ranges.hpp:40
int stoi(std::string_view str)
Same interface as std::stoi and meant as a drop in replacement, except:
Definition: charconv.hpp:154
std::map< std::string, t_string, res_compare > string_map_res
std::string format_conjunct_list(const t_string &empty, const std::vector< t_string > &elems)
Format a conjunctive list.
std::map< std::string, t_string > string_map
std::vector< std::string > split(const config_attribute_value &val)
point game_canvas_size()
The size of the game canvas, in drawing coordinates / game pixels.
Definition: video.cpp:432
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:59
bool operator<(const terrain_movement_info &other) const
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
static map_location::direction s
unit_type_data unit_types
Definition: types.cpp:1504