The Battle for Wesnoth  1.19.10+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  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() != 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  ss << "\n";
396 
397  // Print cross-references to units that this unit advances from/to.
398  // Cross reference to the topics containing information about those units.
399  const bool first_reverse_value = true;
400  bool reverse = first_reverse_value;
401  if(variation_.empty()) {
402  do {
403  std::vector<std::string> adv_units =
405  bool first = true;
406 
407  for(const std::string &adv : adv_units) {
409  if(!type || type->hide_help()) {
410  continue;
411  }
412 
413  if(first) {
414  if(reverse) {
415  ss << _("Advances from:");
416  } else {
417  ss << _("Advances to:");
418  }
419  ss << font::nbsp;
420  first = false;
421  } else {
422  ss << ", ";
423  }
424 
425  std::string lang_unit = type->type_name();
426  std::string ref_id;
428  const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
429  ref_id = section_prefix + unit_prefix + type->id();
430  } else {
431  ref_id = unknown_unit_topic;
432  lang_unit += " (?)";
433  }
434  ss << markup::make_link(lang_unit, ref_id);
435  }
436  if(!first) {
437  ss << "\n";
438  }
439 
440  reverse = !reverse; //switch direction
441  } while(reverse != first_reverse_value); // don't restart
442  }
443 
444  const unit_type* parent = variation_.empty() ? &type_ :
446  if(!variation_.empty()) {
447  ss << _("Base unit:") << font::nbsp << markup::make_link(parent->type_name(), ".." + unit_prefix + type_.id()) << "\n";
448  } else {
449  bool first = true;
450  for(const std::string& base_id : utils::split(type_.get_cfg()["base_ids"])) {
451  if(first) {
452  ss << _("Base units:") << font::nbsp;
453  first = false;
454  }
455  const unit_type* base_type = unit_types.find(base_id, unit_type::HELP_INDEXED);
456  const std::string section_prefix = base_type->show_variations_in_help() ? ".." : "";
457  ss << markup::make_link(base_type->type_name(), section_prefix + unit_prefix + base_id) << "\n";
458  }
459  }
460 
461  bool first = true;
462  for(const std::string& var_id : parent->variations()) {
463  const unit_type& type = parent->get_variation(var_id);
464 
465  if(type.hide_help()) {
466  continue;
467  }
468 
469  if(first) {
470  ss << _("Variations:") << font::nbsp;
471  first = false;
472  } else {
473  ss << ", ";
474  }
475 
476  std::string ref_id;
477 
478  std::string var_name = type.variation_name();
480  ref_id = variation_prefix + type.id() + "_" + var_id;
481  } else {
482  ref_id = unknown_unit_topic;
483  var_name += " (?)";
484  }
485 
486  ss << markup::make_link(var_name, ref_id);
487  }
488 
489  if(!parent->variations().empty()) {
490  ss << "\n";
491  }
492 
493  // Print the race of the unit, cross-reference it to the respective topic.
494  const std::string race_id = type_.race_id();
495  std::string race_name = type_.race()->plural_name();
496  if(race_name.empty()) {
497  race_name = _ ("race^Miscellaneous");
498  }
499  ss << _("Race:") << font::nbsp;
500  ss << markup::make_link(race_name, "..race_" + race_id);
501  ss << "\n";
502 
503  // Print the possible traits of the unit, cross-reference them
504  // to their respective topics.
506  std::vector<trait_data> must_have_traits;
507  std::vector<trait_data> random_traits;
508  int must_have_nameless_traits = 0;
509 
510  for(const config& trait : traits) {
511  const std::string& male_name = trait["male_name"].str();
512  const std::string& female_name = trait["female_name"].str();
513  std::string trait_name;
514  if(type_.has_gender_variation(unit_race::MALE) && ! male_name.empty())
515  trait_name = male_name;
516  else if(type_.has_gender_variation(unit_race::FEMALE) && ! female_name.empty())
517  trait_name = female_name;
518  else if(! trait["name"].str().empty())
519  trait_name = trait["name"].str();
520  else
521  continue; // Hidden trait
522 
523  std::string lang_trait_name = translation::gettext(trait_name.c_str());
524  if(lang_trait_name.empty() && trait["availability"].str() == "musthave") {
525  ++must_have_nameless_traits;
526  continue;
527  }
528  const std::string ref_id = "traits_"+trait["id"].str();
529  ((trait["availability"].str() == "musthave") ? must_have_traits : random_traits).emplace_back(lang_trait_name, ref_id);
530  }
531 
532  bool line1 = !must_have_traits.empty();
533  bool line2 = !random_traits.empty() && type_.num_traits() > must_have_traits.size();
534 
535  if(line1) {
536  ss << _("Traits");
537  if(line2) {
538  ss << "\n(" << must_have_traits.size() << "):" << font::nbsp;
539  print_trait_list(ss, must_have_traits);
540 
541  ss << "\n" << "("
542  << (type_.num_traits() - must_have_traits.size() - must_have_nameless_traits)
543  << "):" << font::nbsp;
544  print_trait_list(ss, random_traits);
545  } else {
546  ss << ":" << font::nbsp;
547  print_trait_list(ss, must_have_traits);
548  }
549  ss << "\n";
550  } else {
551  if(line2) {
552  ss << _("Traits") << " (" << (type_.num_traits() - must_have_nameless_traits) << "):" << font::nbsp;
553  print_trait_list(ss, random_traits);
554  ss << "\n";
555  }
556  }
557  }
558 
559  // Print the abilities the units has, cross-reference them
560  // to their respective topics. TODO: Update this according to musthave trait effects, similar to movetype below
561  if(!type_.abilities_metadata().empty()) {
562  ss << _("Abilities:") << font::nbsp;
563 
564  bool start = true;
565 
566  for(auto iter = type_.abilities_metadata().begin(); iter != type_.abilities_metadata().end(); ++iter) {
567  const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
568 
569  if(iter->name.empty()) {
570  continue;
571  }
572 
573  if(!start) {
574  ss << ", ";
575  } else {
576  start = false;
577  }
578 
579  std::string lang_ability = translation::gettext(iter->name.c_str());
580  ss << markup::make_link(lang_ability, ref_id);
581  }
582 
583  ss << "\n\n";
584  }
585 
586  // Print the extra AMLA upgrade abilities, cross-reference them to their respective topics.
587  if(!type_.adv_abilities_metadata().empty()) {
588  ss << _("Ability Upgrades:") << font::nbsp;
589 
590  bool start = true;
591 
592  for(auto iter = type_.adv_abilities_metadata().begin(); iter != type_.adv_abilities_metadata().end(); ++iter) {
593  const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
594 
595  if(iter->name.empty()) {
596  continue;
597  }
598 
599  if(!start) {
600  ss << ", ";
601  } else {
602  start = false;
603  }
604 
605  std::string lang_ability = translation::gettext(iter->name.c_str());
606  ss << markup::make_link(lang_ability, ref_id);
607  }
608 
609  ss << "\n\n";
610  }
611 
612  // Print some basic information such as HP and movement points.
613  // TODO: Make this info update according to musthave traits, similar to movetype below.
614 
615  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
616  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
617  // unpleasant line breaks (issue #3256).
618  ss << _("HP:") << font::nbsp << type_.hitpoints() << " "
619  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
620  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
621  // unpleasant line breaks (issue #3256).
622  << _("Moves:") << font::nbsp << type_.movement() << " ";
623  if(type_.vision() != type_.movement()) {
624  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
625  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
626  // unpleasant line breaks (issue #3256).
627  ss << _("Vision:") << font::nbsp << type_.vision() << " ";
628  }
629  if(type_.jamming() > 0) {
630  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
631  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
632  // unpleasant line breaks (issue #3256).
633  ss << _("Jamming:") << font::nbsp << type_.jamming() << " ";
634  }
635  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
636  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
637  // unpleasant line breaks (issue #3256).
638  ss << _("Cost:") << font::nbsp << type_.cost() << " "
639  // TRANSLATORS: This string is used in the help page of a single unit. If the translation
640  // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
641  // unpleasant line breaks (issue #3256).
642  << _("Alignment:") << font::nbsp
643  << markup::make_link(type_.alignment_description(type_.alignment(), type_.genders().front()), "time_of_day")
644  << " ";
646  // TRANSLATORS: This string is used in the help page of a single unit. It uses
647  // non-breaking spaces to prevent unpleasant line breaks (issue #3256). In the
648  // translation use non-breaking spaces as appropriate for the target language.
649  ss << _("Required\u00a0XP:") << font::nbsp << type_.experience_needed();
650  }
651 
652  // Print the detailed description about the unit.
653  ss << "\n" << detailed_description;
654 
655  if(const auto notes = type_.special_notes(); !notes.empty()) {
656  ss << "\n" << markup::tag("header", _("Special Notes")) << "\n";
657  for(const auto& note : notes) {
658  ss << font::unicode_bullet << " " << markup::italic(note) << '\n';
659  }
660  }
661 
662  std::stringstream table_ss;
663 
664  //
665  // Attacks table
666  //
667  ss << "\n" << markup::tag("header", _("Attacks"));
668 
669  if (type_.max_attacks() > 1) {
670  ss << "\n" << markup::italic(_("Attacks per turn: ")) << type_.max_attacks();
671  }
672 
673  if(!type_.attacks().empty()) {
674  // Check if at least one attack has special.
675  // Otherwise the Special column will be hidden.
676  bool has_special = false;
677  for(const attack_type& attack : type_.attacks()) {
678  if(!attack.special_tooltips().empty()) {
679  has_special = true;
680  }
681  }
682 
683  // Print headers for the table.
684  table_ss << markup::tag("row",
685  { {"bgcolor", "table_header"} },
686  //FIXME space/tab does not work, but nbsp does
687  //empty tags will be skipped by rich_label
688  markup::tag("col", font::nbsp),
689  markup::tag("col", markup::bold(_("Name"))),
690  markup::tag("col", markup::bold(_("Strikes"))),
691  markup::tag("col", markup::bold(_("Range"))),
692  markup::tag("col", markup::bold(_("Type"))),
693  has_special ? markup::tag("col", markup::bold(_("Special"))) : "");
694 
695  // Print information about every attack.
696  for(const attack_type& attack : type_.attacks()) {
697  std::stringstream attack_ss;
698 
699  std::string lang_weapon = attack.name();
700  std::string lang_type = string_table["type_" + attack.type()];
701 
702  // Attack icon
703  attack_ss << markup::tag("col", markup::img(attack.icon()));
704 
705  // attack name
706  attack_ss << markup::tag("col", lang_weapon);
707 
708  // damage x strikes
709  if (type_.max_attacks() > 1) {
710  attack_ss << markup::tag("col",
711  attack.damage(), font::weapon_numbers_sep, attack.num_attacks(),
712  " ", attack.accuracy_parry_description(),
713  "\n",
714  VNGETTEXT(
715  "uses $num attack",
716  "uses $num attacks",
717  attack.attacks_used(),
718  { {"num", std::to_string(attack.attacks_used())} }));
719  } else {
720  attack_ss << markup::tag("col",
721  attack.damage(), font::weapon_numbers_sep, attack.num_attacks(),
722  " ", attack.accuracy_parry_description());
723  }
724 
725  // range
726  const std::string range_icon = "icons/profiles/" + attack.range() + "_attack.png~SCALE_INTO(16,16)";
727  if(attack.min_range() > 1 || attack.max_range() > 1) {
728  attack_ss << markup::tag("col",
729  markup::img(range_icon), ' ',
730  attack.min_range(), "-", attack.max_range(), ' ',
731  string_table["range_" + attack.range()]);
732  } else {
733  attack_ss << markup::tag("col",
734  markup::img(range_icon), ' ',
735  string_table["range_" + attack.range()]);
736  }
737 
738  // type
739  const std::string type_icon = "icons/profiles/" + attack.type() + ".png~SCALE_INTO(16,16)";
740  attack_ss << markup::tag("col", markup::img(type_icon), ' ', lang_type);
741 
742  // special
743  if(has_special) {
744  std::vector<std::pair<t_string, t_string>> specials = attack.special_tooltips();
745  if(!specials.empty()) {
746  std::stringstream specials_ss;
747  std::string lang_special = "";
748  const std::size_t specials_size = specials.size();
749  for(std::size_t i = 0; i != specials_size; ++i) {
750  const std::string ref_id = std::string("weaponspecial_")
751  + specials[i].first.base_str();
752  lang_special = (specials[i].first);
753  specials_ss << markup::make_link(lang_special, ref_id);
754  if(i+1 != specials_size) {
755  specials_ss << ", "; //comma placed before next special
756  }
757  }
758  attack_ss << markup::tag("col", specials_ss.str());
759  } else {
760  attack_ss << markup::tag("col", font::unicode_em_dash);
761  }
762  }
763 
764  table_ss << markup::tag("row", { {"bgcolor", "table_row1"} }, attack_ss.str());
765  }
766 
767  ss << markup::tag("table", table_ss.str());
768  }
769 
770  // Generate the movement type of the unit,
771  // with resistance, defense, movement, jamming and vision data
772  // updated according to any 'musthave' traits which always apply.
773  movetype movement_type = type_.movement_type();
774  config::const_child_itors traits = type_.possible_traits();
775  if(!traits.empty() && type_.num_traits() > 0) {
776  for(const config & t : traits) {
777  if(t["availability"].str() == "musthave") {
778  for(const config & effect : t.child_range("effect")) {
779  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.
780  && movetype::effects.find(effect["apply_to"].str()) != movetype::effects.end()) {
781  movement_type.merge(effect, effect["replace"].to_bool());
782  }
783  }
784  }
785  }
786  }
787 
788  const bool has_terrain_defense_caps = movement_type.has_terrain_defense_caps(prefs::get().encountered_terrains());
789  const bool has_vision = type_.movement_type().has_vision_data();
790  const bool has_jamming = type_.movement_type().has_jamming_data();
791 
792  //
793  // Resistances table
794  //
795  ss << "\n" << markup::tag("header", _("Resistances"));
796 
797  std::stringstream().swap(table_ss);
798  table_ss << markup::tag("row",
799  { {"bgcolor", "table_header"} },
800  markup::tag("col", markup::bold(_("Attack Type"))),
801  markup::tag("col", markup::bold(_("Resistance"))));
802 
803  utils::string_map_res dam_tab = movement_type.damage_table();
804  bool odd_row = true;
805  for(std::pair<std::string, std::string> dam_it : dam_tab) {
806  int resistance = 100;
807  try {
808  resistance -= std::stoi(dam_it.second);
809  } catch(std::invalid_argument&) {}
810  std::string resist = std::to_string(resistance) + '%';
811  const std::size_t pos = resist.find('-');
812  if(pos != std::string::npos) {
813  resist.replace(pos, 1, font::unicode_minus);
814  }
815  std::string color = unit_helper::resistance_color(resistance);
816  const std::string lang_type = string_table["type_" + dam_it.first];
817  const std::string type_icon = "icons/profiles/" + dam_it.first + ".png~SCALE_INTO(16,16)";
818  table_ss << markup::tag("row",
819  { {"bgcolor", (odd_row ? "table_row1" : "table_row2")} },
820  markup::tag("col", markup::img(type_icon), ' ', lang_type),
821  markup::tag("col", markup::span_color(color, resist)));
822 
823  odd_row = !odd_row;
824  }
825  ss << markup::tag("table", table_ss.str());
826 
827  //
828  // Terrain Modifiers table
829  //
830  std::stringstream().swap(table_ss);
831  if(std::shared_ptr<terrain_type_data> tdata = load_terrain_types_data()) {
832  // Print the terrain modifier table of the unit.
833  ss << "\n" << markup::tag("header", _("Terrain Modifiers"));
834 
835  // Header row
836  std::stringstream row_ss;
837  row_ss << markup::tag("col", markup::bold(_("Terrain")));
838  row_ss << markup::tag("col", markup::bold(_("Defense")));
839  row_ss << markup::tag("col", markup::bold(_("Movement Cost")));
840  if(has_terrain_defense_caps) { row_ss << markup::tag("col", markup::bold(_("Defense Cap"))); }
841  if(has_vision) { row_ss << markup::tag("col", markup::bold(_("Vision Cost"))); }
842  if(has_jamming) { row_ss << markup::tag("col", markup::bold(_("Jamming Cost"))); }
843  table_ss << markup::tag("row", { {"bgcolor", "table_header"} }, row_ss.str());
844 
845  // Organize terrain movetype data
846  std::set<terrain_movement_info> terrain_moves;
847  for(t_translation::terrain_code terrain : prefs::get().encountered_terrains()) {
849  continue;
850  }
851  const terrain_type& info = tdata->get_terrain_info(terrain);
852  const int moves = movement_type.movement_cost(terrain);
853  const bool cannot_move = moves > type_.movement();
854  if(cannot_move && info.hide_if_impassable()) {
855  continue;
856  }
857 
858  if(info.is_indivisible() && info.is_nonnull()) {
859  terrain_movement_info movement_info =
860  {
861  info.name(),
862  info.id(),
863  100 - movement_type.defense_modifier(terrain),
864  moves,
865  movement_type.vision_cost(terrain),
866  movement_type.jamming_cost(terrain),
867  movement_type.get_defense().capped(terrain)
868  };
869 
870  terrain_moves.insert(movement_info);
871  }
872  }
873 
874  // Add movement table rows
875  odd_row = true;
876  for(const terrain_movement_info& m : terrain_moves)
877  {
878  std::stringstream().swap(row_ss);
879  bool high_res = false;
880  const std::string tc_base = high_res ? "images/buttons/icon-base-32.png" : "images/buttons/icon-base-16.png";
881  const std::string terrain_image = "icons/terrain/terrain_type_" + m.id + (high_res ? "_30.png" : ".png");
882  const std::string final_image = tc_base + "~RC(magenta>" + m.id + ")~BLIT(" + terrain_image + ")";
883 
884  row_ss << markup::tag("col", markup::img(final_image), ' ', markup::make_link(m.name, "..terrain_" + m.id));
885 
886  // Defense - range: +10 % .. +70 %
887  // passing false to select the more saturated red-to-green scale
888  color_t def_color = game_config::red_to_green(m.defense, false);
889  row_ss << markup::tag("col", markup::span_color(def_color, m.defense, "%"));
890 
891  // Movement - range: 1 .. 5, movetype::UNREACHABLE=impassable
892  row_ss << markup::tag("col", format_mp_entry(type_.movement(), m.movement_cost));
893 
894  // Defense cap
895  if(has_terrain_defense_caps) {
896  if(m.defense_cap) {
897  row_ss << markup::tag("col", markup::span_color(def_color, m.defense, "%"));
898  } else {
899  row_ss << markup::tag("col", markup::span_color("white", font::unicode_figure_dash));
900  }
901  }
902 
903  // Vision
904  // uses same formatting as MP
905  if(has_vision) {
906  row_ss << markup::tag("col", format_mp_entry(type_.vision(), m.vision_cost));
907  }
908 
909  // Jamming
910  // uses same formatting as MP
911  if(has_jamming) {
912  row_ss << markup::tag("col", format_mp_entry(type_.jamming(), m.jamming_cost));
913  }
914 
915  table_ss << markup::tag("row", { {"bgcolor", (odd_row ? "table_row1" : "table_row2")} }, row_ss.str());
916 
917  odd_row = !odd_row;
918  }
919 
920  ss << markup::tag("table", table_ss.str());
921 
922  } else {
923  WRN_HP << "When building unit help topics, we couldn't get the terrain info we need.";
924  }
925 
926  return ss.str();
927 }
928 
929 } // end namespace help
double t
Definition: astarsearch.cpp:63
std::vector< std::string > names
Definition: build_info.cpp:67
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:282
std::ostringstream wrapper.
Definition: formatter.hpp:40
virtual std::string operator()() const
virtual std::string operator()() const
bool capped(const t_translation::terrain_code &terrain) const
Returns whether there is a defense cap associated to this terrain.
Definition: movetype.hpp:196
The basic "size" of the unit - flying, small land, large land, etc.
Definition: movetype.hpp:44
static const std::set< std::string > effects
The set of applicable effects for movement types.
Definition: movetype.hpp:340
void merge(const config &new_cfg, bool overwrite=true)
Merges the given config over the existing data, the config should have zero or more children named "m...
Definition: movetype.cpp:859
bool has_terrain_defense_caps(const std::set< t_translation::terrain_code > &ts) const
Returns whether or not there are any terrain caps with respect to a set of terrains.
Definition: movetype.cpp:851
int defense_modifier(const t_translation::terrain_code &terrain) const
Returns the defensive value of the indicated terrain.
Definition: movetype.hpp:288
int jamming_cost(const t_translation::terrain_code &terrain, bool slowed=false) const
Returns the cost to "jam" through the indicated terrain.
Definition: movetype.hpp:284
int vision_cost(const t_translation::terrain_code &terrain, bool slowed=false) const
Returns the cost to see through the indicated terrain.
Definition: movetype.hpp:281
int movement_cost(const t_translation::terrain_code &terrain, bool slowed=false) const
Returns the cost to move through the indicated terrain.
Definition: movetype.hpp:278
utils::string_map_res damage_table() const
Returns a map from damage types to resistances.
Definition: movetype.hpp:295
terrain_defense & get_defense()
Definition: movetype.hpp:263
static prefs & get()
bool empty() const
Definition: tstring.hpp:195
const std::string & str() const
Definition: tstring.hpp:199
const std::string & editor_group() const
Definition: terrain.hpp:155
bool has_default_base() const
Definition: terrain.hpp:180
const std::string & icon_image() const
Definition: terrain.hpp:44
const t_string & income_description() const
Definition: terrain.hpp:150
bool is_combined() const
True for instances created by the terrain_code(base, overlay) constructor.
Definition: terrain.hpp:170
const std::string & editor_image() const
Definition: terrain.hpp:47
bool is_nonnull() const
True if this object represents some sentinel values.
Definition: terrain.hpp:129
bool is_keep() const
Definition: terrain.hpp:146
bool is_castle() const
Definition: terrain.hpp:145
const std::string & id() const
Definition: terrain.hpp:52
const t_string & help_topic_text() const
Definition: terrain.hpp:51
bool is_village() const
Definition: terrain.hpp:144
const t_translation::ter_list & def_type() const
Definition: terrain.hpp:76
const t_translation::ter_list & mvt_type() const
The underlying type of the terrain.
Definition: terrain.hpp:75
int light_bonus(int base) const
Returns the light (lawful) bonus for this terrain when the time of day gives a base bonus.
Definition: terrain.hpp:135
const t_translation::ter_list & union_type() const
Definition: terrain.hpp:78
bool is_overlay() const
Definition: terrain.hpp:158
static bool is_indivisible(t_translation::terrain_code id, const t_translation::ter_list &underlying)
Returns true if a terrain has no underlying types other than itself, in respect of either union,...
Definition: terrain.hpp:100
const t_string & editor_name() const
Definition: terrain.hpp:49
int gives_healing() const
Definition: terrain.hpp:143
t_translation::terrain_code default_base() const
Overlay terrains defined by a [terrain_type] can declare a fallback base terrain, for use when the ov...
Definition: terrain.hpp:179
bool hide_in_editor() const
Definition: terrain.hpp:62
t_translation::terrain_code number() const
Definition: terrain.hpp:66
const t_string & plural_name() const
Definition: race.hpp:38
@ FEMALE
Definition: race.hpp:28
@ MALE
Definition: race.hpp:28
const unit_type * find(const std::string &key, unit_type::BUILD_STATUS status=unit_type::FULL) const
Finds a unit_type by its id() and makes sure it is built to the specified level.
Definition: types.cpp:1265
void build_unit_type(const unit_type &ut, unit_type::BUILD_STATUS status) const
Makes sure the provided unit_type is built to the specified level.
Definition: types.cpp:1257
A single unit type that the player may recruit.
Definition: types.hpp:43
bool can_advance() const
Definition: types.hpp:222
const std::vector< std::string > advances_from() const
A vector of unit_type ids that can advance to this unit_type.
Definition: types.cpp:654
std::string race_id() const
Returns the ID of this type's race without the need to build the type.
Definition: types.hpp:272
static std::string alignment_description(unit_alignments::type align, unit_race::GENDER gender=unit_race::MALE)
Implementation detail of unit_type::alignment_description.
Definition: types.cpp:841
const unit_type & get_gender_unit_type(const std::string &gender) const
Returns a gendered variant of this unit_type.
Definition: types.cpp:455
const std::string & image() const
Definition: types.hpp:176
const std::string & id() const
The id for this unit_type.
Definition: types.hpp:141
const std::vector< ability_metadata > & adv_abilities_metadata() const
Some extra abilities that may be gained through AMLA advancements.
Definition: types.hpp:220
bool show_variations_in_help() const
Whether the unit type has at least one help-visible variation.
Definition: types.cpp:764
const unit_race * race() const
Never returns nullptr, but may point to the null race.
Definition: types.hpp:277
int hitpoints() const
Definition: types.hpp:161
const unit_type & get_variation(const std::string &id) const
Definition: types.cpp:476
const_attack_itors attacks() const
Definition: types.cpp:545
const std::vector< std::string > & advances_to() const
A vector of unit_type ids that this unit_type can advance to.
Definition: types.hpp:115
bool has_gender_variation(const unit_race::GENDER gender) const
Definition: types.hpp:250
int movement() const
Definition: types.hpp:166
t_string unit_description() const
Definition: types.cpp:486
@ HELP_INDEXED
Definition: types.hpp:74
@ FULL
Definition: types.hpp:74
const std::vector< unit_race::GENDER > & genders() const
The returned vector will not be empty, provided this has been built to the HELP_INDEXED status.
Definition: types.hpp:249
int max_attacks() const
Definition: types.hpp:171
std::vector< std::string > variations() const
Definition: types.cpp:747
const std::string & flag_rgb() const
Definition: types.cpp:722
int vision() const
Definition: types.hpp:167
std::vector< t_string > special_notes() const
Returns all notes that should be displayed in the help page for this type, including those found in a...
Definition: types.cpp:495
config::const_child_itors modification_advancements() const
Returns two iterators pointing to a range of AMLA configs.
Definition: types.hpp:120
int cost() const
Definition: types.hpp:172
int experience_needed(bool with_acceleration=true) const
Definition: types.cpp:579
const std::string & big_profile() const
Definition: types.hpp:179
const t_string & type_name() const
The name of the unit in the current language setting.
Definition: types.hpp:138
config::const_child_itors possible_traits() const
Definition: types.hpp:231
int level() const
Definition: types.hpp:164
unit_alignments::type alignment() const
Definition: types.hpp:193
const std::string & small_profile() const
Definition: types.hpp:178
const std::vector< ability_metadata > & abilities_metadata() const
Definition: types.hpp:217
const config & get_cfg() const
Definition: types.hpp:281
unsigned int num_traits() const
Definition: types.hpp:135
int jamming() const
Definition: types.hpp:170
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
#define VNGETTEXT(msgid, msgid_plural, count,...)
std::size_t i
Definition: function.cpp:1030
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:1184
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:1458
static void print_trait_list(std::stringstream &ss, const std::vector< trait_data > &l)
logger & info()
Definition: log.cpp:318
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:31
std::string make_link(const std::string &text, const std::string &dst)
Generates a Help markup tag corresponding to a reference or link.
Definition: markup.cpp:25
std::string bold(Args &&... data)
Applies bold Pango markup to the input.
Definition: markup.hpp: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:154
std::map< std::string, t_string, res_compare > string_map_res
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:432
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:60
bool operator<(const terrain_movement_info &other) const
A terrain string which is converted to a terrain is a string with 1 or 2 layers the layers are separa...
Definition: translation.hpp:49
static map_location::direction s
unit_type_data unit_types
Definition: types.cpp:1504