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