The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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
36#include "video.hpp" // fore current_resolution
37
38#include <set>
39
40static 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
44namespace help {
45
47{
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
62namespace {
63
64std::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
71std::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
99typedef 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.
101std::string print_behavior_description(
102 const ter_iter& start,
103 const ter_iter& end,
104 const terrain_type_data& tdata,
105 bool first_level = true,
106 bool begin_best = true)
107{
108
109 if(start == end) return "";
110 if(*start == t_translation::MINUS || *start == t_translation::PLUS) {
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
171std::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()) {
215 }
216
217 ss << "\n";
218 if(!type_.help_topic_text().empty()) {
219 ss << type_.help_topic_text().str() << "\n";
220 }
221
222 auto tdata = terrain_type_data::get();
223 if(!tdata) {
224 WRN_HP << "When building terrain help topics, we couldn't acquire any terrain types data";
225 return ss.str();
226 }
227
228 if(type_.is_combined()) {
229 ss << "Base terrain: ";
230 const auto base_t = tdata->get_terrain_info(
232 ss << markup::make_link(base_t.editor_name(),
233 (base_t.hide_help() ? "." : "") + terrain_prefix + base_t.id());
234 ss << ", ";
235 ss << "Overlay terrain: ";
236 const auto overlay_t = tdata->get_terrain_info(
238 ss << markup::make_link(overlay_t.editor_name(),
239 (overlay_t.hide_help() ? "." : "") + terrain_prefix + overlay_t.id());
240 ss << "\n";
241 }
242
243 const auto& notes = get_special_notes(type_);
244 if(!notes.empty()) {
245 ss << "\n\n" << markup::tag("header", _("Special Notes")) << "\n\n";
246 for(const auto& note : notes) {
247 ss << font::unicode_bullet << " " << note << '\n';
248 }
249 }
250
251 // Almost all terrains will show the data in this conditional block. The ones that don't are the
252 // archetypes used in [movetype]'s subtags such as [movement_costs].
253 if(!type_.is_indivisible()) {
254 std::vector<t_string> underlying;
255 for(const auto& underlying_terrain : type_.union_type()) {
256 const terrain_type& base = tdata->get_terrain_info(underlying_terrain);
257 if(!base.editor_name().empty()) {
258 underlying.push_back(markup::make_link(base.editor_name(), ".." + terrain_prefix + base.id()));
259 }
260 }
261 utils::string_map symbols;
262 symbols["types"] = utils::format_conjunct_list("", underlying);
263 // TRANSLATORS: $types is a conjunct list, typical values will be "Castle" or "Flat and Shallow Water".
264 // The terrain names will be hypertext links to the help page of the corresponding terrain type.
265 // There will always be at least 1 item in the list, but unlikely to be more than 3.
266 ss << "\n" << VNGETTEXT("Basic terrain type: $types", "Basic terrain types: $types", underlying.size(), symbols);
267
268 if(type_.has_default_base()) {
269 const terrain_type& base = tdata->get_terrain_info(type_.default_base());
270
271 symbols.clear();
272 symbols["type"] = markup::make_link(base.editor_name(),
273 (base.is_indivisible() ? ".." : "") + terrain_prefix + base.id());
274 // TRANSLATORS: In the help for a terrain type, for example Dwarven Village is often placed on Cave Floor
275 ss << "\n" << VGETTEXT("Typical base terrain: $type", symbols);
276 }
277
278 ss << "\n";
279
280 const t_translation::ter_list& underlying_mvt_terrains = type_.mvt_type();
281 ss << "\n" << _("Movement properties: ");
282 ss << print_behavior_description(underlying_mvt_terrains.begin(), underlying_mvt_terrains.end(), *tdata) << "\n";
283
284 const t_translation::ter_list& underlying_def_terrains = type_.def_type();
285 ss << "\n" << _("Defense properties: ");
286 ss << print_behavior_description(underlying_def_terrains.begin(), underlying_def_terrains.end(), *tdata) << "\n";
287 }
288
290
291 ss << "\n";
292 ss << "ID: " << type_.id() << "\n";
293
294 ss << "Village: " << (type_.is_village() ? "Yes" : "No") << "\n";
295 ss << "Gives Healing: " << type_.gives_healing() << "\n";
296
297 ss << "Keep: " << (type_.is_keep() ? "Yes" : "No") << "\n";
298 ss << "Castle: " << (type_.is_castle() ? "Yes" : "No") << "\n";
299
300 ss << "Overlay: " << (type_.is_overlay() ? "Yes" : "No") << "\n";
301 ss << "Combined: " << (type_.is_combined() ? "Yes" : "No") << "\n";
302
303 ss << "Nonnull: " << (type_.is_nonnull() ? "Yes" : "No") << "\n";
304
305 ss << "Terrain string: " << type_.number() << "\n";
306
307 ss << "Hide in Editor: " << (type_.hide_in_editor() ? "Yes" : "No") << "\n";
308 ss << "Editor Group: " << type_.editor_group() << "\n";
309
310 ss << "Light Bonus: " << type_.light_bonus(0) << "\n";
311
313
314 ss << "\nEditor Image: ";
315 // Note: this is purely temporary to help make a different help entry
316 ss << (type_.editor_image().empty() ? "Empty" : type_.editor_image());
317 ss << "\n";
318
319 ss << "\nDebug Mvt Description String:";
321 ss << " " << t;
322 }
323
324 ss << "\nDebug Def Description String:";
326 ss << " " << t;
327 }
328
329 }
330
331 return ss.str();
332}
333
334//Typedef to help with formatting list of traits
335// Maps localized trait name to trait help topic ID
336typedef std::pair<std::string, std::string> trait_data;
337
338//Helper function for printing a list of trait data
339static void print_trait_list(std::stringstream & ss, const std::vector<trait_data> & l)
340{
341 if(l.empty()) return;
342 std::size_t i = 0;
343 ss << markup::make_link(l[i].first, l[i].second);
344
345 // This doesn't skip traits with empty names
346 for(i++; i < l.size(); i++) {
347 ss << ", " << markup::make_link(l[i].first,l[i].second);
348 }
349}
350
352 // Force the lazy loading to build this unit.
354
355 std::stringstream ss;
356 const std::string detailed_description = type_.unit_description();
359
360 const int screen_width = video::game_canvas_size().x;
361
362 ss << _("Level") << " " << type_.level();
363
364 // Portraits
365 const std::string &male_portrait = male_type.small_profile().empty() ?
366 male_type.big_profile() : male_type.small_profile();
367 const std::string &female_portrait = female_type.small_profile().empty() ?
368 female_type.big_profile() : female_type.small_profile();
369
370 const bool has_male_portrait = !male_portrait.empty() && male_portrait != male_type.image() && male_portrait != "unit_image";
371 const bool has_female_portrait = !female_portrait.empty() && female_portrait != male_portrait && female_portrait != female_type.image() && female_portrait != "unit_image";
372
373 // TODO: figure out why the second checks don't match but the last does
374 if(has_male_portrait) {
375 ss << markup::img(male_portrait + "~FL(horiz)", "right", true);
376 }
377
378 if(has_female_portrait) {
379 ss << markup::img(female_portrait + "~FL(horiz)", "right", true);
380 }
381
382 // Unit Images
383 if(!male_type.image().empty()) {
384 ss << markup::img(formatter()
385 << male_type.image() << "~RC(" << male_type.flag_rgb() << ">red)"
386 << (screen_width >= 1200 ? "~SCALE_SHARP(200%,200%)" : ""));
387
388 if(!female_type.image().empty() && female_type.image() != male_type.image()) {
389 ss << markup::img(formatter()
390 << female_type.image() << "~RC(" << female_type.flag_rgb() << ">red)"
391 << (screen_width >= 1200 ? "~SCALE_SHARP(200%,200%)" : ""));
392 }
393 }
394
395 ss << "\n";
396
397 // Print cross-references to units that this unit advances from/to.
398 // Cross reference to the topics containing information about those units.
399 const bool first_reverse_value = true;
400 bool reverse = first_reverse_value;
401 if(variation_.empty()) {
402 do {
403 std::vector<std::string> adv_units =
404 reverse ? type_.advances_from() : type_.advances_to();
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;
427 if(description_type(*type) == FULL_DESCRIPTION) {
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> possible_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 : possible_random_traits).emplace_back(lang_trait_name, ref_id);
530 }
531
532 // minimum of remaining num_traits and possible random traits (if there are insufficient random traits num_traits cannot be fulfilled)
533 int nr_random_traits = std::min(type_.num_traits() - must_have_traits.size() - must_have_nameless_traits, possible_random_traits.size());
534 if(must_have_traits.empty()) {
535 if(nr_random_traits > 0) {
536 ss << _("Traits") << " "<< VNGETTEXT("(1 of):", "(random $number of):", nr_random_traits, utils::string_map{{"number", std::to_string(nr_random_traits)}}) << font::nbsp;
537 print_trait_list(ss, possible_random_traits);
538 ss << "\n";
539 }
540 } else {
541 ss << _("Traits");
542 if(nr_random_traits > 0) {
543 ss << "\n(" << must_have_traits.size() << "):" << font::nbsp;
544 print_trait_list(ss, must_have_traits);
545
546 ss << "\n" << VNGETTEXT("(1 of):", "(random $number of):", nr_random_traits, utils::string_map{{"number", std::to_string(nr_random_traits)}}) << font::nbsp;
547 print_trait_list(ss, possible_random_traits);
548 } else {
549 ss << ":" << font::nbsp;
550 print_trait_list(ss, must_have_traits);
551 }
552 ss << "\n";
553 }
554 }
555
556 // Print the abilities the units has, cross-reference them
557 // to their respective topics. TODO: Update this according to musthave trait effects, similar to movetype below
558 if(!type_.abilities_metadata().empty()) {
559 ss << _("Abilities:") << font::nbsp;
560
561 bool start = true;
562
563 for(const auto& ability : type_.abilities_metadata()) {
564 const std::string ref_id = ability_prefix + ability.help_topic_id;
565
566 if(ability.name.empty()) {
567 continue;
568 }
569
570 if(!start) {
571 ss << ", ";
572 } else {
573 start = false;
574 }
575
576 std::string lang_ability = translation::gettext(ability.name.c_str());
577 ss << markup::make_link(lang_ability, ref_id);
578 }
579
580 ss << "\n\n";
581 }
582
583 // Print the extra AMLA upgrade abilities, cross-reference them to their respective topics.
584 if(!type_.adv_abilities_metadata().empty()) {
585 ss << _("Ability Upgrades:") << font::nbsp;
586
587 bool start = true;
588
589 for(const auto& ability : type_.adv_abilities_metadata()) {
590 const std::string ref_id = ability_prefix + ability.help_topic_id;
591
592 if(ability.name.empty()) {
593 continue;
594 }
595
596 if(!start) {
597 ss << ", ";
598 } else {
599 start = false;
600 }
601
602 std::string lang_ability = translation::gettext(ability.name.c_str());
603 ss << markup::make_link(lang_ability, ref_id);
604 }
605
606 ss << "\n\n";
607 }
608
609 // Print some basic information such as HP and movement points.
610 // TODO: Make this info update according to musthave traits, similar to movetype below.
611
612 // TRANSLATORS: This string is used in the help page of a single unit. If the translation
613 // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
614 // unpleasant line breaks (issue #3256).
615 ss << _("HP:") << font::nbsp << type_.hitpoints() << " "
616 // TRANSLATORS: This string is used in the help page of a single unit. If the translation
617 // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
618 // unpleasant line breaks (issue #3256).
619 << _("Moves:") << font::nbsp << type_.movement() << " ";
620 if(type_.vision() != type_.movement()) {
621 // TRANSLATORS: This string is used in the help page of a single unit. If the translation
622 // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
623 // unpleasant line breaks (issue #3256).
624 ss << _("Vision:") << font::nbsp << type_.vision() << " ";
625 }
626 if(type_.jamming() > 0) {
627 // TRANSLATORS: This string is used in the help page of a single unit. If the translation
628 // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
629 // unpleasant line breaks (issue #3256).
630 ss << _("Jamming:") << font::nbsp << type_.jamming() << " ";
631 }
632 // TRANSLATORS: This string is used in the help page of a single unit. If the translation
633 // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
634 // unpleasant line breaks (issue #3256).
635 ss << _("Cost:") << font::nbsp << type_.cost() << " "
636 // TRANSLATORS: This string is used in the help page of a single unit. If the translation
637 // uses spaces, use non-breaking spaces as appropriate for the target language to prevent
638 // unpleasant line breaks (issue #3256).
639 << _("Alignment:") << font::nbsp
641 << " ";
643 // TRANSLATORS: This string is used in the help page of a single unit. It uses
644 // non-breaking spaces to prevent unpleasant line breaks (issue #3256). In the
645 // translation use non-breaking spaces as appropriate for the target language.
646 ss << _("Required\u00a0XP:") << font::nbsp << type_.experience_needed();
647 }
648
649 // Print the detailed description about the unit.
650 ss << "\n" << detailed_description;
651
652 if(const auto notes = type_.special_notes(); !notes.empty()) {
653 ss << "\n" << markup::tag("header", _("Special Notes")) << "\n";
654 for(const auto& note : notes) {
655 ss << font::unicode_bullet << " " << markup::italic(note) << '\n';
656 }
657 }
658
659 std::stringstream table_ss;
660
661 //
662 // Attacks table
663 //
664 ss << "\n" << markup::tag("header", _("Attacks"));
665
666 if (type_.max_attacks() > 1) {
667 ss << "\n" << markup::italic(_("Attacks per turn: ")) << type_.max_attacks();
668 }
669
670 if(!type_.attacks().empty()) {
671 // Check if at least one attack has special.
672 // Otherwise the Special column will be hidden.
673 bool has_special = false;
674 for(const attack_type& attack : type_.attacks()) {
675 if(!attack.special_tooltips().empty()) {
676 has_special = true;
677 }
678 }
679
680 // Print headers for the table.
681 table_ss << markup::tag_attr("row",
682 {{ "bgcolor", "table_header" }},
683 //FIXME space/tab does not work, but nbsp does
684 //empty tags will be skipped by rich_label
685 markup::tag("col", font::nbsp),
686 markup::tag("col", markup::bold(_("Name"))),
687 markup::tag("col", markup::bold(_("Strikes"))),
688 markup::tag("col", markup::bold(_("Range"))),
689 markup::tag("col", markup::bold(_("Type"))),
690 has_special ? markup::tag("col", markup::bold(_("Special"))) : "");
691
692 // Print information about every attack.
693 for(const attack_type& attack : type_.attacks()) {
694 std::stringstream attack_ss;
695
696 std::string lang_weapon = attack.name();
697 std::string lang_type = string_table["type_" + attack.type()];
698
699 // Attack icon
700 attack_ss << markup::tag("col", markup::img(attack.icon()));
701
702 // attack name
703 attack_ss << markup::tag("col", lang_weapon);
704
705 // damage x strikes
706 if (type_.max_attacks() > 1) {
707 attack_ss << markup::tag("col",
708 attack.damage(), font::weapon_numbers_sep, attack.num_attacks(),
709 " ", attack.accuracy_parry_description(),
710 "\n",
711 VNGETTEXT(
712 "uses $num attack",
713 "uses $num attacks",
714 attack.attacks_used(),
715 { {"num", std::to_string(attack.attacks_used())} }));
716 } else {
717 attack_ss << markup::tag("col",
718 attack.damage(), font::weapon_numbers_sep, attack.num_attacks(),
719 " ", attack.accuracy_parry_description());
720 }
721
722 // range
723 const std::string range_icon = "icons/profiles/" + attack.range() + "_attack.png~SCALE_INTO(16,16)";
724 if(attack.min_range() > 1 || attack.max_range() > 1) {
725 attack_ss << markup::tag("col",
726 markup::img(range_icon), ' ',
727 attack.min_range(), "-", attack.max_range(), ' ',
728 string_table["range_" + attack.range()]);
729 } else {
730 attack_ss << markup::tag("col",
731 markup::img(range_icon), ' ',
732 string_table["range_" + attack.range()]);
733 }
734
735 // type
736 const std::string type_icon = "icons/profiles/" + attack.type() + ".png~SCALE_INTO(16,16)";
737 attack_ss << markup::tag("col", markup::img(type_icon), ' ', lang_type);
738
739 // special
740 if(has_special) {
741 auto specials = attack.special_tooltips();
742 if(!specials.empty()) {
743 std::stringstream specials_ss;
744 std::string lang_special = "";
745 const std::size_t specials_size = specials.size();
746 for(std::size_t i = 0; i != specials_size; ++i) {
747 const std::string ref_id = std::string("weaponspecial_") + specials[i].help_topic_id;
748 lang_special = (specials[i].name);
749 specials_ss << markup::make_link(lang_special, ref_id);
750 if(i+1 != specials_size) {
751 specials_ss << ", "; //comma placed before next special
752 }
753 }
754 attack_ss << markup::tag("col", specials_ss.str());
755 } else {
756 attack_ss << markup::tag("col", font::unicode_em_dash);
757 }
758 }
759
760 table_ss << markup::tag_attr("row", {{ "bgcolor", "table_row1" }}, attack_ss.str());
761 }
762
763 ss << markup::tag("table", table_ss.str());
764 }
765
766 // Generate the movement type of the unit,
767 // with resistance, defense, movement, jamming and vision data
768 // updated according to any 'musthave' traits which always apply.
769 movetype movement_type = type_.movement_type();
770 config::const_child_itors traits = type_.possible_traits();
771 if(!traits.empty() && type_.num_traits() > 0) {
772 for(const config & t : traits) {
773 if(t["availability"].str() == "musthave") {
774 for(const config & effect : t.child_range("effect")) {
775 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.
776 && movetype::effects.find(effect["apply_to"].str()) != movetype::effects.end()) {
777 movement_type.merge(effect, effect["replace"].to_bool());
778 }
779 }
780 }
781 }
782 }
783
784 const bool has_terrain_defense_caps = movement_type.has_terrain_defense_caps(prefs::get().encountered_terrains());
785 const bool has_vision = type_.movement_type().has_vision_data();
786 const bool has_jamming = type_.movement_type().has_jamming_data();
787
788 //
789 // Resistances table
790 //
791 ss << "\n" << markup::tag("header", _("Resistances"));
792
793 std::stringstream().swap(table_ss);
794 table_ss << markup::tag_attr("row",
795 {{ "bgcolor", "table_header" }},
796 markup::tag("col", markup::bold(_("Attack Type"))),
797 markup::tag("col", markup::bold(_("Resistance"))));
798
799 bool odd_row = true;
800 for(const auto& [damage_type, damage_resistance] : movement_type.damage_table()) {
801 int resistance = 100;
802 try {
803 resistance -= std::stoi(damage_resistance);
804 } catch(std::invalid_argument&) {}
805 std::string resist = std::to_string(resistance) + '%';
806 const std::size_t pos = resist.find('-');
807 if(pos != std::string::npos) {
808 resist.replace(pos, 1, font::unicode_minus);
809 }
810 std::string color = unit_helper::resistance_color(resistance);
811 const std::string lang_type = string_table["type_" + damage_type];
812 const std::string type_icon = "icons/profiles/" + damage_type + ".png~SCALE_INTO(16,16)";
813 table_ss << markup::tag_attr("row",
814 {{ "bgcolor", (odd_row ? "table_row1" : "table_row2") }},
815 markup::tag("col", markup::img(type_icon), ' ', lang_type),
816 markup::tag_attr("col", {{ "halign", "right" }}, markup::span_color(color, resist)));
817
818 odd_row = !odd_row;
819 }
820 ss << markup::tag("table", table_ss.str());
821
822 //
823 // Terrain Modifiers table
824 //
825 std::stringstream().swap(table_ss);
826 if(auto tdata = terrain_type_data::get()) {
827 // Print the terrain modifier table of the unit.
828 ss << "\n" << markup::tag("header", _("Terrain Modifiers"));
829
830 // Header row
831 std::stringstream row_ss;
832 row_ss << markup::tag("col", markup::bold(_("Terrain")));
833 row_ss << markup::tag("col", markup::bold(_("Defense")));
834 row_ss << markup::tag("col", markup::bold(_("Movement Cost")));
835 if(has_terrain_defense_caps) { row_ss << markup::tag("col", markup::bold(_("Defense Cap"))); }
836 if(has_vision) { row_ss << markup::tag("col", markup::bold(_("Vision Cost"))); }
837 if(has_jamming) { row_ss << markup::tag("col", markup::bold(_("Jamming Cost"))); }
838 table_ss << markup::tag_attr("row", {{ "bgcolor", "table_header" }}, row_ss.str());
839
840 // Organize terrain movetype data
841 std::set<terrain_movement_info> terrain_moves;
842 for(t_translation::terrain_code terrain : prefs::get().encountered_terrains()) {
844 continue;
845 }
846 const terrain_type& info = tdata->get_terrain_info(terrain);
847 const int moves = movement_type.movement_cost(terrain);
848 const bool cannot_move = moves > type_.movement();
849 if(cannot_move && info.hide_if_impassable()) {
850 continue;
851 }
852
853 if(info.is_indivisible() && info.is_nonnull()) {
854 terrain_movement_info movement_info =
855 {
856 info.name(),
857 info.id(),
858 100 - movement_type.defense_modifier(terrain),
859 moves,
860 movement_type.vision_cost(terrain),
861 movement_type.jamming_cost(terrain),
862 movement_type.get_defense().capped(terrain)
863 };
864
865 terrain_moves.insert(movement_info);
866 }
867 }
868
869 // Add movement table rows
870 odd_row = true;
871 for(const terrain_movement_info& m : terrain_moves)
872 {
873 std::stringstream().swap(row_ss);
874 bool high_res = false;
875 const std::string tc_base = high_res ? "images/buttons/icon-base-32.png" : "images/buttons/icon-base-16.png";
876 const std::string terrain_image = "icons/terrain/terrain_type_" + m.id + (high_res ? "_30.png" : ".png");
877 const std::string final_image = tc_base + "~RC(magenta>" + m.id + ")~BLIT(" + terrain_image + ")";
878
879 row_ss << markup::tag("col", markup::img(final_image), ' ', markup::make_link(m.name, "..terrain_" + m.id));
880
881 // Defense - range: +10 % .. +70 %
882 // passing false to select the more saturated red-to-green scale
883 color_t def_color = game_config::red_to_green(m.defense, false);
884 row_ss << markup::tag_attr("col", {{ "halign", "right"}}, markup::span_color(def_color, m.defense, "%"));
885
886 // Movement - range: 1 .. 5, movetype::UNREACHABLE=impassable
887 row_ss << markup::tag("col", format_mp_entry(type_.movement(), m.movement_cost));
888
889 // Defense cap
890 if(has_terrain_defense_caps) {
891 if(m.defense_cap) {
892 row_ss << markup::tag("col", markup::span_color(def_color, m.defense, "%"));
893 } else {
894 row_ss << markup::tag("col", markup::span_color("white", font::unicode_figure_dash));
895 }
896 }
897
898 // Vision
899 // uses same formatting as MP
900 if(has_vision) {
901 row_ss << markup::tag("col", format_mp_entry(type_.vision(), m.vision_cost));
902 }
903
904 // Jamming
905 // uses same formatting as MP
906 if(has_jamming) {
907 row_ss << markup::tag("col", format_mp_entry(type_.jamming(), m.jamming_cost));
908 }
909
910 table_ss << markup::tag_attr("row", {{ "bgcolor", (odd_row ? "table_row1" : "table_row2") }}, row_ss.str());
911
912 odd_row = !odd_row;
913 }
914
915 ss << markup::tag("table", table_ss.str());
916
917 } else {
918 WRN_HP << "When building unit help topics, we couldn't get the terrain info we need.";
919 }
920
921 return ss.str();
922}
923
924} // end namespace help
double t
std::vector< std::string > names
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:895
void merge(const config &new_cfg, bool overwrite=true)
Merges the given config over the existing data, the config should have zero or more children named "m...
Definition movetype.cpp:854
bool has_terrain_defense_caps(const std::set< t_translation::terrain_code > &ts) const
Returns whether or not there are any terrain caps with respect to a set of terrains.
Definition movetype.cpp:846
int defense_modifier(const t_translation::terrain_code &terrain) const
Returns the defensive value of the indicated terrain.
Definition movetype.hpp:288
int jamming_cost(const t_translation::terrain_code &terrain, bool slowed=false) const
Returns the cost to "jam" through the indicated terrain.
Definition movetype.hpp:284
int vision_cost(const t_translation::terrain_code &terrain, bool slowed=false) const
Returns the cost to see through the indicated terrain.
Definition movetype.hpp:281
int movement_cost(const t_translation::terrain_code &terrain, bool slowed=false) const
Returns the cost to move through the indicated terrain.
Definition movetype.hpp:278
terrain_defense & get_defense()
Definition movetype.hpp:263
static prefs & get()
bool empty() const
Definition tstring.hpp:200
const std::string & str() const
Definition tstring.hpp:204
Contains the database of all known terrain types, both those defined explicitly by WML [terrain_type]...
Definition type_data.hpp:41
static terrain_type_data * get()
Definition type_data.cpp:28
const terrain_type & get_terrain_info(const t_translation::terrain_code &terrain) const
Get the corresponding terrain_type information object for a given type of terrain.
bool has_default_base() const
Definition terrain.hpp:187
bool is_combined() const
True for instances created by the terrain_code(base, overlay) constructor.
Definition terrain.hpp:177
bool is_nonnull() const
True if this object represents some sentinel values.
Definition terrain.hpp:136
bool is_keep() const
Definition terrain.hpp:153
const std::string & id() const
Definition terrain.hpp:52
bool is_castle() const
Definition terrain.hpp:152
const t_string & income_description() const
Definition terrain.hpp:157
const t_string & help_topic_text() const
Definition terrain.hpp:51
const std::string & icon_image() const
Definition terrain.hpp:44
const std::string & editor_image() const
Definition terrain.hpp:47
bool is_village() const
Definition terrain.hpp:151
const std::string & editor_group() const
Definition terrain.hpp:162
int light_bonus(int base) const
Returns the light (lawful) bonus for this terrain when the time of day gives a base bonus.
Definition terrain.hpp:142
bool is_overlay() const
Definition terrain.hpp:165
static bool is_indivisible(t_translation::terrain_code id, const t_translation::ter_list &underlying)
Returns true if a terrain has no underlying types other than itself, in respect of either union,...
Definition terrain.hpp:107
const t_translation::ter_list & union_type() const
Definition terrain.hpp:87
int gives_healing() const
Definition terrain.hpp:150
t_translation::terrain_code default_base() const
Overlay terrains defined by a [terrain_type] can declare a fallback base terrain, for use when the ov...
Definition terrain.hpp:186
const t_translation::ter_list & def_type() const
Definition terrain.hpp:86
bool hide_in_editor() const
Definition terrain.hpp:62
const t_translation::ter_list & mvt_type() const
The underlying movement type of the terrain.
Definition terrain.hpp:85
t_translation::terrain_code number() const
Definition terrain.hpp:66
const t_string & editor_name() const
Definition terrain.hpp:49
const t_string & plural_name() const
Definition race.hpp:38
@ FEMALE
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:1253
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:1245
A single unit type that the player may recruit.
Definition types.hpp:43
bool can_advance() const
Definition types.hpp:230
const std::vector< std::string > advances_from() const
A vector of unit_type ids that can advance to this unit_type.
Definition types.cpp:610
std::string race_id() const
Returns the ID of this type's race without the need to build the type.
Definition types.hpp:274
const std::string & big_profile() const
Definition types.hpp:183
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:779
const std::string & image() const
Definition types.hpp:180
const std::vector< ability_metadata > & adv_abilities_metadata() const
Some extra abilities that may be gained through AMLA advancements.
Definition types.hpp:228
const unit_type & get_gender_unit_type(const std::string &gender) const
Returns a gendered variant of this unit_type.
Definition types.cpp:416
const unit_race * race() const
Never returns nullptr, but may point to the null race.
Definition types.hpp:279
bool show_variations_in_help() const
Whether the unit type has at least one help-visible variation.
Definition types.cpp:703
int hitpoints() const
Definition types.hpp:165
const std::vector< ability_metadata > & abilities_metadata() const
Definition types.hpp:225
const unit_type & get_variation(const std::string &id) const
Definition types.cpp:437
const_attack_itors attacks() const
Definition types.cpp:505
const std::string & small_profile() const
Definition types.hpp:182
bool has_gender_variation(const unit_race::GENDER gender) const
Definition types.cpp:681
int movement() const
Definition types.hpp:170
t_string unit_description() const
Definition types.cpp:447
const config & get_cfg() const
Definition types.hpp:283
@ HELP_INDEXED
Definition types.hpp:77
int max_attacks() const
Definition types.hpp:175
std::vector< std::string > variations() const
Definition types.cpp:686
const std::string & flag_rgb() const
Definition types.cpp:676
const t_string & type_name() const
The name of the unit in the current language setting.
Definition types.hpp:142
int vision() const
Definition types.hpp:171
std::vector< t_string > special_notes() const
Returns all notes that should be displayed in the help page for this type, including those found in a...
Definition types.cpp:456
config::const_child_itors modification_advancements() const
Returns two iterators pointing to a range of AMLA configs.
Definition types.hpp:124
int cost() const
Definition types.hpp:176
const std::vector< std::string > & advances_to() const
A vector of unit_type ids that this unit_type can advance to.
Definition types.hpp:119
int experience_needed(bool with_acceleration=true) const
Definition types.cpp:539
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:254
config::const_child_itors possible_traits() const
Definition types.hpp:239
int level() const
Definition types.hpp:168
unit_alignments::type alignment() const
Definition types.hpp:198
const std::string & id() const
The id for this unit_type.
Definition types.hpp:145
unsigned int num_traits() const
Definition types.hpp:139
int jamming() const
Definition types.hpp:174
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
#define VNGETTEXT(msgid, msgid_plural, count,...)
static std::string _(const char *str)
Definition gettext.hpp:100
#define WRN_HP
static lg::log_domain log_help("help")
auto string_table
Definition language.hpp:81
Standard logging facilities (interface).
CURSOR_TYPE get()
Definition cursor.cpp:185
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
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....
@ FULL_DESCRIPTION
const std::string unit_prefix
Definition help_impl.cpp:63
const std::string variation_prefix
Definition help_impl.cpp:68
UNIT_DESCRIPTION_TYPE description_type(const unit_type &type)
Return the type of description that should be shown for a unit of the given kind.
const std::string ability_prefix
Definition help_impl.cpp:69
std::pair< std::string, std::string > trait_data
const std::string terrain_prefix
Definition help_impl.cpp:64
const std::string unknown_unit_topic
Definition help_impl.cpp:62
static void print_trait_list(std::stringstream &ss, const std::vector< trait_data > &l)
logger & info()
Definition log.cpp:351
std::string italic(Args &&... data)
Applies italic Pango markup to the input.
Definition markup.hpp:176
std::string img(const std::string &src, const std::string &align, bool floating)
Generates a Help markup tag corresponding to an image.
Definition markup.cpp:37
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:31
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_attr(std::string_view tag, const tag_attributes &attrs, Args &&... data)
Wraps the given data in the specified tag.
Definition markup.hpp:69
std::string tag(std::string_view tag, Args &&... data)
Wraps the given data in the specified tag.
Definition markup.hpp:45
T end(const std::pair< T, T > &p)
const terrain_code VOID_TERRAIN
VOID_TERRAIN is used for shrouded hexes.
const terrain_code MINUS
bool terrain_matches(const terrain_code &src, const terrain_code &dest)
Tests whether a specific terrain matches an expression, for matching rules see above.
std::vector< terrain_code > ter_list
const terrain_code BASE
const ter_match ALL_OFF_MAP
const terrain_code PLUS
constexpr ter_layer NO_LAYER
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
std::string join(const Range &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:51
bool operator<(const terrain_movement_info &other) const
A terrain string which is converted to a terrain is a string with 1 or 2 layers the layers are separa...
static map_location::direction s
unit_type_data unit_types
Definition types.cpp:1499
#define i