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