The Battle for Wesnoth  1.19.17+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 std::shared_ptr<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  std::shared_ptr 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  const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number());
320  ss << "\nDebug Mvt Description String:";
321  for(const t_translation::terrain_code& t : underlying_mvt_terrains) {
322  ss << " " << t;
323  }
324 
325  const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number());
326  ss << "\nDebug Def Description String:";
327  for(const t_translation::terrain_code& t : underlying_def_terrains) {
328  ss << " " << t;
329  }
330 
331  }
332 
333  return ss.str();
334 }
335 
336 //Typedef to help with formatting list of traits
337 // Maps localized trait name to trait help topic ID
338 typedef std::pair<std::string, std::string> trait_data;
339 
340 //Helper function for printing a list of trait data
341 static void print_trait_list(std::stringstream & ss, const std::vector<trait_data> & l)
342 {
343  std::size_t i = 0;
344  ss << markup::make_link(l[i].first, l[i].second);
345 
346  // This doesn't skip traits with empty names
347  for(i++; i < l.size(); i++) {
348  ss << ", " << markup::make_link(l[i].first,l[i].second);
349  }
350 }
351 
352 std::string unit_topic_generator::operator()() const {
353  // Force the lazy loading to build this unit.
355 
356  std::stringstream ss;
357  const std::string detailed_description = type_.unit_description();
360 
361  const int screen_width = video::game_canvas_size().x;
362 
363  ss << _("Level") << " " << type_.level();
364 
365  // Portraits
366  const std::string &male_portrait = male_type.small_profile().empty() ?
367  male_type.big_profile() : male_type.small_profile();
368  const std::string &female_portrait = female_type.small_profile().empty() ?
369  female_type.big_profile() : female_type.small_profile();
370 
371  const bool has_male_portrait = !male_portrait.empty() && male_portrait != male_type.image() && male_portrait != "unit_image";
372  const bool has_female_portrait = !female_portrait.empty() && female_portrait != male_portrait && female_portrait != female_type.image() && female_portrait != "unit_image";
373 
374  // TODO: figure out why the second checks don't match but the last does
375  if(has_male_portrait) {
376  ss << markup::img(male_portrait + "~FL(horiz)", "right", true);
377  }
378 
379  if(has_female_portrait) {
380  ss << markup::img(female_portrait + "~FL(horiz)", "right", true);
381  }
382 
383  // Unit Images
384  if(!male_type.image().empty()) {
385  ss << markup::img(formatter()
386  << male_type.image() << "~RC(" << male_type.flag_rgb() << ">red)"
387  << (screen_width >= 1200 ? "~SCALE_SHARP(200%,200%)" : ""));
388 
389  if(!female_type.image().empty() && female_type.image() != male_type.image()) {
390  ss << markup::img(formatter()
391  << female_type.image() << "~RC(" << female_type.flag_rgb() << ">red)"
392  << (screen_width >= 1200 ? "~SCALE_SHARP(200%,200%)" : ""));
393  }
394  }
395 
396  ss << "\n";
397 
398  // Print cross-references to units that this unit advances from/to.
399  // Cross reference to the topics containing information about those units.
400  const bool first_reverse_value = true;
401  bool reverse = first_reverse_value;
402  if(variation_.empty()) {
403  do {
404  std::vector<std::string> adv_units =
406  bool first = true;
407 
408  for(const std::string &adv : adv_units) {
410  if(!type || type->hide_help()) {
411  continue;
412  }
413 
414  if(first) {
415  if(reverse) {
416  ss << _("Advances from:");
417  } else {
418  ss << _("Advances to:");
419  }
420  ss << font::nbsp;
421  first = false;
422  } else {
423  ss << ", ";
424  }
425 
426  std::string lang_unit = type->type_name();
427  std::string ref_id;
429  const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
430  ref_id = section_prefix + unit_prefix + type->id();
431  } else {
432  ref_id = unknown_unit_topic;
433  lang_unit += " (?)";
434  }
435  ss << markup::make_link(lang_unit, ref_id);
436  }
437  if(!first) {
438  ss << "\n";
439  }
440 
441  reverse = !reverse; //switch direction
442  } while(reverse != first_reverse_value); // don't restart
443  }
444 
445  const unit_type* parent = variation_.empty() ? &type_ :
447  if(!variation_.empty()) {
448  ss << _("Base unit:") << font::nbsp << markup::make_link(parent->type_name(), ".." + unit_prefix + type_.id()) << "\n";
449  } else {
450  bool first = true;
451  for(const std::string& base_id : utils::split(type_.get_cfg()["base_ids"])) {
452  if(first) {
453  ss << _("Base units:") << font::nbsp;
454  first = false;
455  }
456  const unit_type* base_type = unit_types.find(base_id, unit_type::HELP_INDEXED);
457  const std::string section_prefix = base_type->show_variations_in_help() ? ".." : "";
458  ss << markup::make_link(base_type->type_name(), section_prefix + unit_prefix + base_id) << "\n";
459  }
460  }
461 
462  bool first = true;
463  for(const std::string& var_id : parent->variations()) {
464  const unit_type& type = parent->get_variation(var_id);
465 
466  if(type.hide_help()) {
467  continue;
468  }
469 
470  if(first) {
471  ss << _("Variations:") << font::nbsp;
472  first = false;
473  } else {
474  ss << ", ";
475  }
476 
477  std::string ref_id;
478 
479  std::string var_name = type.variation_name();
481  ref_id = variation_prefix + type.id() + "_" + var_id;
482  } else {
483  ref_id = unknown_unit_topic;
484  var_name += " (?)";
485  }
486 
487  ss << markup::make_link(var_name, ref_id);
488  }
489 
490  if(!parent->variations().empty()) {
491  ss << "\n";
492  }
493 
494  // Print the race of the unit, cross-reference it to the respective topic.
495  const std::string race_id = type_.race_id();
496  std::string race_name = type_.race()->plural_name();
497  if(race_name.empty()) {
498  race_name = _ ("race^Miscellaneous");
499  }
500  ss << _("Race:") << font::nbsp;
501  ss << markup::make_link(race_name, "..race_" + race_id);
502  ss << "\n";
503 
504  // Print the possible traits of the unit, cross-reference them
505  // to their respective topics.
507  std::vector<trait_data> must_have_traits;
508  std::vector<trait_data> random_traits;
509  int must_have_nameless_traits = 0;
510 
511  for(const config& trait : traits) {
512  const std::string& male_name = trait["male_name"].str();
513  const std::string& female_name = trait["female_name"].str();
514  std::string trait_name;
515  if(type_.has_gender_variation(unit_race::MALE) && ! male_name.empty())
516  trait_name = male_name;
517  else if(type_.has_gender_variation(unit_race::FEMALE) && ! female_name.empty())
518  trait_name = female_name;
519  else if(! trait["name"].str().empty())
520  trait_name = trait["name"].str();
521  else
522  continue; // Hidden trait
523 
524  std::string lang_trait_name = translation::gettext(trait_name.c_str());
525  if(lang_trait_name.empty() && trait["availability"].str() == "musthave") {
526  ++must_have_nameless_traits;
527  continue;
528  }
529  const std::string ref_id = "traits_"+trait["id"].str();
530  ((trait["availability"].str() == "musthave") ? must_have_traits : random_traits).emplace_back(lang_trait_name, ref_id);
531  }
532 
533  int nr_random_traits = type_.num_traits() - must_have_traits.size() - must_have_nameless_traits;
534  if(must_have_traits.empty()) {
535  if(nr_random_traits > 0) {
536  ss << _("Traits") << " "<< VNGETTEXT("(1 of):", "(random $number of):", nr_random_traits, utils::string_map{{"number", std::to_string(nr_random_traits)}}) << font::nbsp;
537  print_trait_list(ss, random_traits);
538  ss << "\n";
539  }
540  } else {
541  ss << _("Traits");
542  if(nr_random_traits > 0) {
543  ss << "\n(" << must_have_traits.size() << "):" << font::nbsp;
544  print_trait_list(ss, must_have_traits);
545 
546  ss << "\n" << VNGETTEXT("(1 of):", "(random $number of):", nr_random_traits, utils::string_map{{"number", std::to_string(nr_random_traits)}}) << font::nbsp;
547  print_trait_list(ss, random_traits);
548  } else {
549  ss << ":" << font::nbsp;
550  print_trait_list(ss, must_have_traits);
551  }
552  ss << "\n";
553  }
554  }
555 
556  // Print the abilities the units has, cross-reference them
557  // to their respective topics. TODO: Update this according to musthave trait effects, similar to movetype below
558  if(!type_.abilities_metadata().empty()) {
559  ss << _("Abilities:") << font::nbsp;
560 
561  bool start = true;
562 
563  for(const auto& ability : type_.abilities_metadata()) {
564  const std::string ref_id = ability_prefix + ability.id + ability.name.base_str();
565 
566  if(ability.name.empty()) {
567  continue;
568  }
569 
570  if(!start) {
571  ss << ", ";
572  } else {
573  start = false;
574  }
575 
576  std::string lang_ability = translation::gettext(ability.name.c_str());
577  ss << markup::make_link(lang_ability, ref_id);
578  }
579 
580  ss << "\n\n";
581  }
582 
583  // Print the extra AMLA upgrade abilities, cross-reference them to their respective topics.
584  if(!type_.adv_abilities_metadata().empty()) {
585  ss << _("Ability Upgrades:") << font::nbsp;
586 
587  bool start = true;
588 
589  for(const auto& ability : type_.adv_abilities_metadata()) {
590  const std::string ref_id = ability_prefix + ability.id + ability.name.base_str();
591 
592  if(ability.name.empty()) {
593  continue;
594  }
595 
596  if(!start) {
597  ss << ", ";
598  } else {
599  start = false;
600  }
601 
602  std::string lang_ability = translation::gettext(ability.name.c_str());
603  ss << markup::make_link(lang_ability, ref_id);
604  }
605 
606  ss << "\n\n";
607  }
608 
609  // Print some basic information such as HP and movement points.
610  // TODO: Make this info update according to musthave traits, similar to movetype below.
611 
612  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
613  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
614  // unpleasant line breaks (issue #3256).
615  ss << _("HP:") << font::nbsp << type_.hitpoints() << " "
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  << _("Moves:") << font::nbsp << type_.movement() << " ";
620  if(type_.vision() != type_.movement()) {
621  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
622  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
623  // unpleasant line breaks (issue #3256).
624  ss << _("Vision:") << font::nbsp << type_.vision() << " ";
625  }
626  if(type_.jamming() > 0) {
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 << _("Jamming:") << font::nbsp << type_.jamming() << " ";
631  }
632  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
633  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
634  // unpleasant line breaks (issue #3256).
635  ss << _("Cost:") << font::nbsp << type_.cost() << " "
636  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
637  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
638  // unpleasant line breaks (issue #3256).
639  << _("Alignment:") << font::nbsp
640  << markup::make_link(type_.alignment_description(type_.alignment(), type_.genders().front()), "time_of_day")
641  << " ";
643  // TRANSLATORS: This string is used in the help page of a single unit. It uses
644  // non-breaking spaces to prevent unpleasant line breaks (issue #3256). In the
645  // translation use non-breaking spaces as appropriate for the target language.
646  ss << _("Required\u00a0XP:") << font::nbsp << type_.experience_needed();
647  }
648 
649  // Print the detailed description about the unit.
650  ss << "\n" << detailed_description;
651 
652  if(const auto notes = type_.special_notes(); !notes.empty()) {
653  ss << "\n" << markup::tag("header", _("Special Notes")) << "\n";
654  for(const auto& note : notes) {
655  ss << font::unicode_bullet << " " << markup::italic(note) << '\n';
656  }
657  }
658 
659  std::stringstream table_ss;
660 
661  //
662  // Attacks table
663  //
664  ss << "\n" << markup::tag("header", _("Attacks"));
665 
666  if (type_.max_attacks() > 1) {
667  ss << "\n" << markup::italic(_("Attacks per turn: ")) << type_.max_attacks();
668  }
669 
670  if(!type_.attacks().empty()) {
671  // Check if at least one attack has special.
672  // Otherwise the Special column will be hidden.
673  bool has_special = false;
674  for(const attack_type& attack : type_.attacks()) {
675  if(!attack.special_tooltips().empty()) {
676  has_special = true;
677  }
678  }
679 
680  // Print headers for the table.
681  table_ss << markup::tag("row",
682  { {"bgcolor", "table_header"} },
683  //FIXME space/tab does not work, but nbsp does
684  //empty tags will be skipped by rich_label
685  markup::tag("col", font::nbsp),
686  markup::tag("col", markup::bold(_("Name"))),
687  markup::tag("col", markup::bold(_("Strikes"))),
688  markup::tag("col", markup::bold(_("Range"))),
689  markup::tag("col", markup::bold(_("Type"))),
690  has_special ? markup::tag("col", markup::bold(_("Special"))) : "");
691 
692  // Print information about every attack.
693  for(const attack_type& attack : type_.attacks()) {
694  std::stringstream attack_ss;
695 
696  std::string lang_weapon = attack.name();
697  std::string lang_type = string_table["type_" + attack.type()];
698 
699  // Attack icon
700  attack_ss << markup::tag("col", markup::img(attack.icon()));
701 
702  // attack name
703  attack_ss << markup::tag("col", lang_weapon);
704 
705  // damage x strikes
706  if (type_.max_attacks() > 1) {
707  attack_ss << markup::tag("col",
708  attack.damage(), font::weapon_numbers_sep, attack.num_attacks(),
709  " ", attack.accuracy_parry_description(),
710  "\n",
711  VNGETTEXT(
712  "uses $num attack",
713  "uses $num attacks",
714  attack.attacks_used(),
715  { {"num", std::to_string(attack.attacks_used())} }));
716  } else {
717  attack_ss << markup::tag("col",
718  attack.damage(), font::weapon_numbers_sep, attack.num_attacks(),
719  " ", attack.accuracy_parry_description());
720  }
721 
722  // range
723  const std::string range_icon = "icons/profiles/" + attack.range() + "_attack.png~SCALE_INTO(16,16)";
724  if(attack.min_range() > 1 || attack.max_range() > 1) {
725  attack_ss << markup::tag("col",
726  markup::img(range_icon), ' ',
727  attack.min_range(), "-", attack.max_range(), ' ',
728  string_table["range_" + attack.range()]);
729  } else {
730  attack_ss << markup::tag("col",
731  markup::img(range_icon), ' ',
732  string_table["range_" + attack.range()]);
733  }
734 
735  // type
736  const std::string type_icon = "icons/profiles/" + attack.type() + ".png~SCALE_INTO(16,16)";
737  attack_ss << markup::tag("col", markup::img(type_icon), ' ', lang_type);
738 
739  // special
740  if(has_special) {
741  std::vector<std::pair<t_string, t_string>> specials = attack.special_tooltips();
742  if(!specials.empty()) {
743  std::stringstream specials_ss;
744  std::string lang_special = "";
745  const std::size_t specials_size = specials.size();
746  for(std::size_t i = 0; i != specials_size; ++i) {
747  const std::string ref_id = std::string("weaponspecial_")
748  + specials[i].first.base_str();
749  lang_special = (specials[i].first);
750  specials_ss << markup::make_link(lang_special, ref_id);
751  if(i+1 != specials_size) {
752  specials_ss << ", "; //comma placed before next special
753  }
754  }
755  attack_ss << markup::tag("col", specials_ss.str());
756  } else {
757  attack_ss << markup::tag("col", font::unicode_em_dash);
758  }
759  }
760 
761  table_ss << markup::tag("row", { {"bgcolor", "table_row1"} }, attack_ss.str());
762  }
763 
764  ss << markup::tag("table", table_ss.str());
765  }
766 
767  // Generate the movement type of the unit,
768  // with resistance, defense, movement, jamming and vision data
769  // updated according to any 'musthave' traits which always apply.
770  movetype movement_type = type_.movement_type();
771  config::const_child_itors traits = type_.possible_traits();
772  if(!traits.empty() && type_.num_traits() > 0) {
773  for(const config & t : traits) {
774  if(t["availability"].str() == "musthave") {
775  for(const config & effect : t.child_range("effect")) {
776  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.
777  && movetype::effects.find(effect["apply_to"].str()) != movetype::effects.end()) {
778  movement_type.merge(effect, effect["replace"].to_bool());
779  }
780  }
781  }
782  }
783  }
784 
785  const bool has_terrain_defense_caps = movement_type.has_terrain_defense_caps(prefs::get().encountered_terrains());
786  const bool has_vision = type_.movement_type().has_vision_data();
787  const bool has_jamming = type_.movement_type().has_jamming_data();
788 
789  //
790  // Resistances table
791  //
792  ss << "\n" << markup::tag("header", _("Resistances"));
793 
794  std::stringstream().swap(table_ss);
795  table_ss << markup::tag("row",
796  { {"bgcolor", "table_header"} },
797  markup::tag("col", markup::bold(_("Attack Type"))),
798  markup::tag("col", markup::bold(_("Resistance"))));
799 
800  bool odd_row = true;
801  for(const auto& [damage_type, damage_resistance] : movement_type.damage_table()) {
802  int resistance = 100;
803  try {
804  resistance -= std::stoi(damage_resistance);
805  } catch(std::invalid_argument&) {}
806  std::string resist = std::to_string(resistance) + '%';
807  const std::size_t pos = resist.find('-');
808  if(pos != std::string::npos) {
809  resist.replace(pos, 1, font::unicode_minus);
810  }
811  std::string color = unit_helper::resistance_color(resistance);
812  const std::string lang_type = string_table["type_" + damage_type];
813  const std::string type_icon = "icons/profiles/" + damage_type + ".png~SCALE_INTO(16,16)";
814  table_ss << markup::tag("row",
815  { {"bgcolor", (odd_row ? "table_row1" : "table_row2")} },
816  markup::tag("col", markup::img(type_icon), ' ', lang_type),
817  markup::tag("col", markup::span_color(color, resist)));
818 
819  odd_row = !odd_row;
820  }
821  ss << markup::tag("table", table_ss.str());
822 
823  //
824  // Terrain Modifiers table
825  //
826  std::stringstream().swap(table_ss);
827  if(std::shared_ptr tdata = terrain_type_data::get()) {
828  // Print the terrain modifier table of the unit.
829  ss << "\n" << markup::tag("header", _("Terrain Modifiers"));
830 
831  // Header row
832  std::stringstream row_ss;
833  row_ss << markup::tag("col", markup::bold(_("Terrain")));
834  row_ss << markup::tag("col", markup::bold(_("Defense")));
835  row_ss << markup::tag("col", markup::bold(_("Movement Cost")));
836  if(has_terrain_defense_caps) { row_ss << markup::tag("col", markup::bold(_("Defense Cap"))); }
837  if(has_vision) { row_ss << markup::tag("col", markup::bold(_("Vision Cost"))); }
838  if(has_jamming) { row_ss << markup::tag("col", markup::bold(_("Jamming Cost"))); }
839  table_ss << markup::tag("row", { {"bgcolor", "table_header"} }, row_ss.str());
840 
841  // Organize terrain movetype data
842  std::set<terrain_movement_info> terrain_moves;
843  for(t_translation::terrain_code terrain : prefs::get().encountered_terrains()) {
845  continue;
846  }
847  const terrain_type& info = tdata->get_terrain_info(terrain);
848  const int moves = movement_type.movement_cost(terrain);
849  const bool cannot_move = moves > type_.movement();
850  if(cannot_move && info.hide_if_impassable()) {
851  continue;
852  }
853 
854  if(info.is_indivisible() && info.is_nonnull()) {
855  terrain_movement_info movement_info =
856  {
857  info.name(),
858  info.id(),
859  100 - movement_type.defense_modifier(terrain),
860  moves,
861  movement_type.vision_cost(terrain),
862  movement_type.jamming_cost(terrain),
863  movement_type.get_defense().capped(terrain)
864  };
865 
866  terrain_moves.insert(movement_info);
867  }
868  }
869 
870  // Add movement table rows
871  odd_row = true;
872  for(const terrain_movement_info& m : terrain_moves)
873  {
874  std::stringstream().swap(row_ss);
875  bool high_res = false;
876  const std::string tc_base = high_res ? "images/buttons/icon-base-32.png" : "images/buttons/icon-base-16.png";
877  const std::string terrain_image = "icons/terrain/terrain_type_" + m.id + (high_res ? "_30.png" : ".png");
878  const std::string final_image = tc_base + "~RC(magenta>" + m.id + ")~BLIT(" + terrain_image + ")";
879 
880  row_ss << markup::tag("col", markup::img(final_image), ' ', markup::make_link(m.name, "..terrain_" + m.id));
881 
882  // Defense - range: +10 % .. +70 %
883  // passing false to select the more saturated red-to-green scale
884  color_t def_color = game_config::red_to_green(m.defense, false);
885  row_ss << markup::tag("col", markup::span_color(def_color, m.defense, "%"));
886 
887  // Movement - range: 1 .. 5, movetype::UNREACHABLE=impassable
888  row_ss << markup::tag("col", format_mp_entry(type_.movement(), m.movement_cost));
889 
890  // Defense cap
891  if(has_terrain_defense_caps) {
892  if(m.defense_cap) {
893  row_ss << markup::tag("col", markup::span_color(def_color, m.defense, "%"));
894  } else {
895  row_ss << markup::tag("col", markup::span_color("white", font::unicode_figure_dash));
896  }
897  }
898 
899  // Vision
900  // uses same formatting as MP
901  if(has_vision) {
902  row_ss << markup::tag("col", format_mp_entry(type_.vision(), m.vision_cost));
903  }
904 
905  // Jamming
906  // uses same formatting as MP
907  if(has_jamming) {
908  row_ss << markup::tag("col", format_mp_entry(type_.jamming(), m.jamming_cost));
909  }
910 
911  table_ss << markup::tag("row", { {"bgcolor", (odd_row ? "table_row1" : "table_row2")} }, row_ss.str());
912 
913  odd_row = !odd_row;
914  }
915 
916  ss << markup::tag("table", table_ss.str());
917 
918  } else {
919  WRN_HP << "When building unit help topics, we couldn't get the terrain info we need.";
920  }
921 
922  return ss.str();
923 }
924 
925 } // 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:856
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:848
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
static std::shared_ptr< terrain_type_data > get()
Definition: type_data.cpp:34
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:1259
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:1251
A single unit type that the player may recruit.
Definition: types.hpp:43
bool can_advance() const
Definition: types.hpp:226
const std::vector< std::string > advances_from() const
A vector of unit_type ids that can advance to this unit_type.
Definition: types.cpp:609
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:798
const unit_type & get_gender_unit_type(const std::string &gender) const
Returns a gendered variant of this unit_type.
Definition: types.cpp:415
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:224
bool show_variations_in_help() const
Whether the unit type has at least one help-visible variation.
Definition: types.cpp:722
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:165
const unit_type & get_variation(const std::string &id) const
Definition: types.cpp:436
const_attack_itors attacks() const
Definition: types.cpp:504
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:700
int movement() const
Definition: types.hpp:170
t_string unit_description() const
Definition: types.cpp:446
@ 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:252
int max_attacks() const
Definition: types.hpp:175
std::vector< std::string > variations() const
Definition: types.cpp:705
const std::string & flag_rgb() const
Definition: types.cpp:675
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:455
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:538
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:235
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:221
const config & get_cfg() const
Definition: types.hpp:281
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:1072
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
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: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:40
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:61
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:1499