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