The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
edit_unit.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2023 - 2025
3 by Subhraman Sarkar (babaissarkar) <sbmskmm@protonmail.com>
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/* unit type editor dialog */
17
18#define GETTEXT_DOMAIN "wesnoth-lib"
19
21
22#include "filesystem.hpp"
23#include "gettext.hpp"
30#include "gui/widgets/image.hpp"
31#include "gui/widgets/label.hpp"
41#include "picture.hpp"
45#include "units/types.hpp"
46
47#include <boost/algorithm/string/replace.hpp>
48#include <boost/filesystem.hpp>
49#include <sstream>
50
51namespace gui2::dialogs
52{
53
55
56// TODO properly support attributes from installed addons
58 : modal_dialog(window_id())
59 , game_config_(game_config)
60 , addon_id_(addon_id)
61{
62 for (const auto& [id, cfg] : unit_types.specials()) {
63 specials_list_.emplace_back("label", id, "checkbox", false);
64 }
65
66 for (const auto& [id, cfg] : unit_types.abilities()) {
67 abilities_list_.emplace_back("label", id, "checkbox", false);
68 }
69
70 connect_signal<event::SDL_KEY_DOWN>(std::bind(
71 &editor_edit_unit::signal_handler_sdl_key_down, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5, std::placeholders::_6));
72}
73
75 tab_container& tabs = find_widget<tab_container>("tabs");
76 connect_signal_notify_modified(tabs, std::bind(&editor_edit_unit::on_page_select, this));
77
78 button& quit = find_widget<button>("exit");
79 connect_signal_mouse_left_click(quit, std::bind(&editor_edit_unit::quit_confirmation, this));
80
81 //
82 // Main Stats tab
83 //
84
85 menu_button& alignments = find_widget<menu_button>("alignment_list");
86 for (auto& align : unit_alignments::values) {
87 // Show the user the translated strings,
88 // but use the untranslated align strings for generated WML
89 const std::string& icon_path = "icons/alignments/alignment_" + std::string(align) + "_30.png";
90 align_list_.emplace_back("label", t_string(static_cast<std::string>(align), "wesnoth"), "icon", icon_path);
91 }
92 alignments.set_values(align_list_);
93
94 menu_button& races = find_widget<menu_button>("race_list");
95 for(const auto& [_, race] : unit_types.races()) {
96 race_list_.emplace_back("label", race.id(), "icon", race.get_icon_path_stem() + "_30.png");
97 }
98
99 if (!race_list_.empty()) {
100 races.set_values(race_list_);
101 }
102
103 button& load = find_widget<button>("load_unit_type");
104 std::stringstream tooltip;
105 tooltip << t_string("Hotkey(s): ", "wesnoth");
106 #ifdef __APPLE__
107 tooltip << "cmd+o";
108 #else
109 tooltip << "ctrl+o";
110 #endif
111 load.set_tooltip(tooltip.str());
112 connect_signal_mouse_left_click(load, std::bind(&editor_edit_unit::load_unit_type, this));
113
114 connect_signal_mouse_left_click(
115 find_widget<button>("browse_unit_image"),
116 std::bind(&editor_edit_unit::select_file, this, "data/core/images/units", "unit_image"));
117 connect_signal_mouse_left_click(
118 find_widget<button>("preview_unit_image"),
119 std::bind(&editor_edit_unit::update_image, this, "unit_image"));
120 connect_signal_mouse_left_click(
121 find_widget<button>("browse_portrait_image"),
122 std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "portrait_image"));
123 connect_signal_mouse_left_click(
124 find_widget<button>("preview_portrait_image"),
125 std::bind(&editor_edit_unit::update_image, this, "portrait_image"));
126
127 connect_signal_notify_modified(
128 find_widget<text_box>("name_box"),
129 std::bind(&editor_edit_unit::button_state_change, this));
130 connect_signal_notify_modified(
131 find_widget<text_box>("id_box"),
132 std::bind(&editor_edit_unit::button_state_change, this));
133
134 //
135 // Advanced Tab
136 //
137
138 menu_button& movetypes = find_widget<menu_button>("movetype_list");
139 for(const auto& [name, _] : unit_types.movement_types()) {
140 movetype_list_.emplace_back("label", name);
141 }
142
143 if (!movetype_list_.empty()) {
144 movetypes.set_values(movetype_list_);
145 }
146
147 menu_button& defenses = find_widget<menu_button>("defense_list");
148 const config& defense_attr = game_config_
149 .mandatory_child("units")
150 .mandatory_child("movetype")
151 .mandatory_child("defense");
152 for (const auto& [key, _] : defense_attr.attribute_range()) {
153 defense_list_.emplace_back("label", key);
154 }
155
156 menu_button& movement_costs = find_widget<menu_button>("movement_costs_list");
157 if (!defense_list_.empty()) {
158 defenses.set_values(defense_list_);
159 def_toggles_.resize(defense_list_.size());
160 movement_costs.set_values(defense_list_);
161 move_toggles_.resize(defense_list_.size());
162 }
163
164 menu_button& resistances = find_widget<menu_button>("resistances_list");
165
166 const config& resistances_attr = game_config_
167 .mandatory_child("units")
168 .mandatory_child("movetype")
169 .mandatory_child("resistance");
170 for (const auto& [key, _] : resistances_attr.attribute_range()) {
171 resistances_list_.emplace_back("label", key, "icon", "icons/profiles/" + key + ".png");
172 }
173
174 if (!resistances_list_.empty()) {
175 resistances.set_values(resistances_list_);
176 res_toggles_.resize(resistances_list_.size());
177 }
178
179 menu_button& usage_types = find_widget<menu_button>("usage_list");
180 usage_type_list_.emplace_back("label", _("scout"));
181 usage_type_list_.emplace_back("label", _("fighter"));
182 usage_type_list_.emplace_back("label", _("archer"));
183 usage_type_list_.emplace_back("label", _("mixed fighter"));
184 usage_type_list_.emplace_back("label", _("healer"));
185 usage_types.set_values(usage_type_list_);
186
187 multimenu_button& abilities = find_widget<multimenu_button>("abilities_list");
188 abilities.set_values(abilities_list_);
189
190 connect_signal_mouse_left_click(
191 find_widget<button>("browse_small_profile_image"),
192 std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "small_profile_image"));
193 connect_signal_mouse_left_click(
194 find_widget<button>("preview_small_profile_image"),
195 std::bind(&editor_edit_unit::update_image, this, "small_profile_image"));
196 connect_signal_mouse_left_click(
197 find_widget<button>("load_movetype"),
198 std::bind(&editor_edit_unit::load_movetype, this));
199 connect_signal_notify_modified(
200 find_widget<slider>("resistances_slider"),
201 std::bind(&editor_edit_unit::store_resistances, this));
202 connect_signal_notify_modified(
203 find_widget<menu_button>("resistances_list"),
204 std::bind(&editor_edit_unit::update_resistances, this));
205 connect_signal_mouse_left_click(
206 find_widget<toggle_button>("resistances_checkbox"),
208
209 connect_signal_notify_modified(
210 find_widget<slider>("defense_slider"),
211 std::bind(&editor_edit_unit::store_defenses, this));
212 connect_signal_notify_modified(
213 find_widget<menu_button>("defense_list"),
214 std::bind(&editor_edit_unit::update_defenses, this));
215 connect_signal_mouse_left_click(
216 find_widget<toggle_button>("defense_checkbox"),
218
219 connect_signal_notify_modified(
220 find_widget<slider>("movement_costs_slider"),
222 connect_signal_notify_modified(
223 find_widget<menu_button>("movement_costs_list"),
225 connect_signal_mouse_left_click(
226 find_widget<toggle_button>("movement_costs_checkbox"),
228
229 if (!res_toggles_.empty()) {
231 }
232
233 if (!def_toggles_.empty()) {
235 }
236
237 if (!move_toggles_.empty()) {
239 }
240
241 //
242 // Attack Tab
243 //
244
245 multimenu_button& specials = find_widget<multimenu_button>("weapon_specials_list");
246 specials.set_values(specials_list_);
247
248 combobox& attack_types = find_widget<combobox>("attack_type_list");
249 if (!resistances_list_.empty()) {
250 attack_types.set_values(resistances_list_);
251 }
252
253 // Connect signals
254 connect_signal_mouse_left_click(
255 find_widget<button>("browse_attack_image"),
256 std::bind(&editor_edit_unit::select_file, this, "data/core/images/attacks", "attack_image"));
257 connect_signal_mouse_left_click(
258 find_widget<button>("preview_attack_image"),
259 std::bind(&editor_edit_unit::update_image, this, "attack_image"));
260 connect_signal_notify_modified(
261 find_widget<menu_button>("atk_list"),
262 std::bind(&editor_edit_unit::select_attack, this));
263 connect_signal_mouse_left_click(
264 find_widget<button>("atk_new"),
265 std::bind(&editor_edit_unit::add_attack, this));
266 connect_signal_mouse_left_click(
267 find_widget<button>("atk_delete"),
268 std::bind(&editor_edit_unit::delete_attack, this));
269 connect_signal_mouse_left_click(
270 find_widget<button>("atk_next"),
271 std::bind(&editor_edit_unit::next_attack, this));
272 connect_signal_mouse_left_click(
273 find_widget<button>("atk_prev"),
274 std::bind(&editor_edit_unit::prev_attack, this));
275
276 update_index();
277
278 // Disable OK button at start, since ID and Name boxes are empty
280}
281
283{
285
286 tab_container& tabs = find_widget<tab_container>("tabs");
287 if (tabs.get_active_tab_index() == 3) {
289 }
290}
291
292void editor_edit_unit::select_file(const std::string& default_dir, const std::string& id_stem)
293{
295 dlg.set_title(_("Choose File"))
296 .set_ok_label(_("Select"))
297 .set_path(default_dir)
298 .set_read_only(true);
299
300 // If the file is found inside Wesnoth's data, give its relative path
301 // if not, ask user if they want to copy it into their addon.
302 // If yes, copy and return correct relative path inside addon.
303 // return empty otherwise.
304 auto find_or_copy = [](const std::string& path, const std::string& addon_id, const std::string& type) {
305 const std::string message
306 = _("This file is outside Wesnoth’s data dirs. Do you wish to copy it into your add-on?");
307 const auto optional_path = filesystem::to_asset_path(path, addon_id, type);
308
309 if(optional_path.has_value()) {
310 return optional_path.value();
312 boost::filesystem::path output_path = filesystem::get_current_editor_dir(addon_id);
313 output_path /= type;
314 output_path /= boost::filesystem::path(path).filename();
315 filesystem::copy_file(path, output_path.string());
316 return output_path.filename().string();
317 } else {
318 return std::string();
319 }
320 };
321
322 if (dlg.show()) {
323 if((id_stem == "unit_image")
324 || (id_stem == "portrait_image")
325 || (id_stem == "small_profile_image")
326 || (id_stem == "attack_image"))
327 {
328 find_widget<text_box>("path_"+id_stem).set_value(find_or_copy(dlg.path(), addon_id_, "images"));
329 update_image(id_stem);
330 }
331 }
332}
333
335 const auto& all_type_list = unit_types.types_list();
336 const auto& type_select = gui2::dialogs::units_dialog::build_create_dialog(all_type_list);
337 type_select->set_ok_label("Load");
338
339 if (!type_select->show() && !type_select->is_selected()) {
340 return;
341 }
342
343 const auto& type = all_type_list[type_select->get_selected_index()];
344
345 find_widget<text_box>("id_box").set_value(type->id());
346 find_widget<text_box>("name_box").set_value(type->type_name().base_str());
347 find_widget<spinner>("level_box").set_value(type->level());
348 find_widget<slider>("cost_slider").set_value(type->cost());
349 find_widget<text_box>("adv_box").set_value(utils::join(type->advances_to()));
350 find_widget<slider>("hp_slider").set_value(type->hitpoints());
351 find_widget<slider>("xp_slider").set_value(type->experience_needed());
352 find_widget<slider>("move_slider").set_value(type->movement());
353 find_widget<scroll_text>("desc_box").set_value(type->unit_description().base_str());
354 find_widget<text_box>("path_unit_image").set_value(type->image());
355 find_widget<text_box>("path_portrait_image").set_value(type->big_profile());
356
357 for (const auto& gender : type->genders())
358 {
359 if (gender == unit_race::GENDER::MALE) {
360 find_widget<toggle_button>("gender_male").set_value(true);
361 }
362
363 if (gender == unit_race::GENDER::FEMALE) {
364 find_widget<toggle_button>("gender_female").set_value(true);
365 }
366 }
367
369 find_widget<menu_button>("race_list"),
371 type->race_id());
372
374 find_widget<menu_button>("alignment_list"),
376 unit_alignments::get_string(type->alignment()));
377
378 update_image("unit_image");
379
380 boost::dynamic_bitset<> enabled_abilities(abilities_list_.size());
381 for (size_t i = 0; i < abilities_list_.size(); i++) {
382 for (auto special : type->abilities()) {
383 const std::string id = special->cfg()["unique_id"].str(special->id());
384 if (abilities_list_[i]["label"] == id) {
385 enabled_abilities[i] = true;
386 }
387 }
388 }
389 find_widget<multimenu_button>("abilities_list").select_options(enabled_abilities);
390
391 find_widget<text_box>("path_small_profile_image").set_value(type->small_profile());
392
394 find_widget<menu_button>("movetype_list"),
396 type->movement_type_id());
397
398 config cfg;
399 type->movement_type().write(cfg, false);
400 movement_ = cfg.mandatory_child("movement_costs");
401 defenses_ = cfg.mandatory_child("defense");
402 resistances_ = cfg.mandatory_child("resistance");
403
404 // Overrides for resistance/defense/movement costs
405 for (unsigned i = 0; i < resistances_list_.size(); i++) {
406 if (!type->get_cfg().has_child("resistance")) {
407 break;
408 }
409
410 for (const auto& [key, _] : type->get_cfg().mandatory_child("resistance").attribute_range()) {
411 if (resistances_list_.at(i)["label"] == key) {
412 res_toggles_[i] = 1;
413 }
414 }
415 }
416
417 for (unsigned i = 0; i < defense_list_.size(); i++) {
418 if (type->get_cfg().has_child("defense")) {
419 for (const auto& [key, _] : type->get_cfg().mandatory_child("defense").attribute_range()) {
420 if (defense_list_.at(i)["label"] == key) {
421 def_toggles_[i] = 1;
422 }
423 }
424 }
425
426 if (type->get_cfg().has_child("movement_costs")) {
427 for (const auto& [key, _] : type->get_cfg().mandatory_child("movement_costs").attribute_range()) {
428 if (defense_list_.at(i)["label"] == key) {
429 move_toggles_[i] = 1;
430 }
431 }
432 }
433 }
434
438
440 find_widget<menu_button>("usage_list"),
442 type->usage());
443
444 update_image("small_profile_image");
445
446 attacks_.clear();
447 for(const auto& atk : type->attacks())
448 {
449 config attack;
450 attack["name"] = atk.id();
451 attack["description"] = atk.name().base_str();
452 attack["icon"] = atk.icon();
453 attack["range"] = atk.range();
454 attack["damage"] = atk.damage();
455 attack["number"] = atk.num_attacks();
456 attack["type"] = atk.type();
457
458 boost::dynamic_bitset<> enabled_specials(specials_list_.size());
459
460 for (size_t i = 0; i < specials_list_.size(); i++) {
461 for (auto special : atk.specials()) {
462 const std::string id = special->cfg()["unique_id"].str(special->id());
463 if (specials_list_[i]["label"] == id) {
464 enabled_specials[i] = true;
465 }
466 }
467 }
468
469 attacks_.push_back(std::make_pair(enabled_specials, attack));
470 }
471
472 if (!type->attacks().empty()) {
475 }
476
477 update_index();
478
481}
482
484
485 // Clear the config
487
488 // Textdomain
489 std::string current_textdomain = "wesnoth-" + addon_id_;
490
491 config& utype = type_cfg_.add_child("unit_type");
492 utype["id"] = find_widget<text_box>("id_box").get_value();
493 utype["name"] = t_string(find_widget<text_box>("name_box").get_value(), current_textdomain);
494 utype["image"] = find_widget<text_box>("path_unit_image").get_value();
495 utype["profile"] = find_widget<text_box>("path_portrait_image").get_value();
496 utype["level"] = find_widget<spinner>("level_box").get_value();
497 utype["advances_to"] = find_widget<text_box>("adv_box").get_value();
498 utype["hitpoints"] = find_widget<slider>("hp_slider").get_value();
499 utype["experience"] = find_widget<slider>("xp_slider").get_value();
500 utype["cost"] = find_widget<slider>("cost_slider").get_value();
501 utype["movement"] = find_widget<slider>("move_slider").get_value();
502 utype["description"] = t_string(find_widget<scroll_text>("desc_box").get_value(), current_textdomain);
503 utype["race"] = find_widget<menu_button>("race_list").get_value_string();
504 utype["alignment"] = unit_alignments::values[find_widget<menu_button>("alignment_list").get_value()];
505
506 // Gender
507 if (find_widget<toggle_button>("gender_male").get_value()) {
508 if (find_widget<toggle_button>("gender_female").get_value()) {
509 utype["gender"] = "male,female";
510 } else {
511 utype["gender"] = "male";
512 }
513 } else {
514 if (find_widget<toggle_button>("gender_female").get_value()) {
515 utype["gender"] = "female";
516 }
517 }
518
519 // Page 2
520
521 utype["small_profile"] = find_widget<text_box>("path_small_profile_image").get_value();
522 utype["movement_type"] = find_widget<menu_button>("movetype_list").get_value_string();
523 utype["usage"] = find_widget<menu_button>("usage_list").get_value_string();
524
525 if (res_toggles_.any()) {
526 config& resistances = utype.add_child("resistance");
527 int i = 0;
528 for (const auto& [key, _] : resistances_.attribute_range()) {
529 if (res_toggles_[i]) {
530 resistances[key] = resistances_[key];
531 }
532 i++;
533 }
534 }
535
536 if (def_toggles_.any()) {
537 config& defenses = utype.add_child("defense");
538 int i = 0;
539 for (const auto& [key, _] : defenses_.attribute_range()) {
540 if (def_toggles_[i]) {
541 defenses[key] = defenses_[key];
542 }
543 i++;
544 }
545 }
546
547 if (move_toggles_.any()) {
548 config& movement_costs = utype.add_child("movement_costs");
549 int i = 0;
550 for (const auto& [key, _] : movement_.attribute_range()) {
551 if (move_toggles_[i]) {
552 movement_costs[key] = movement_[key];
553 }
554 i++;
555 }
556 }
557
558 std::vector<std::string> selected_abilities;
559 const auto& abilities_states = find_widget<multimenu_button>("abilities_list").get_toggle_states();
560 if (abilities_states.any()) {
561 for (size_t i = 0; i < abilities_states.size(); i++) {
562 if (abilities_states[i]) {
563 selected_abilities.push_back(abilities_list_[i]["label"]);
564 }
565 }
566 }
567 if (!selected_abilities.empty()) {
568 utype["abilities_list"] = utils::join(selected_abilities);
569 }
570
571 // Page 3 (Attacks)
572
573 for (const auto& [special_bits, cfg] : attacks_) {
574 config& atk = utype.add_child("attack", cfg);
575 std::vector<std::string> selected_specials;
576 for (size_t i = 0; i < special_bits.size(); i++) {
577 if (special_bits[i]) {
578 selected_specials.push_back(specials_list_[i]["label"]);
579 }
580 }
581 if (!selected_specials.empty()) {
582 atk["specials_list"] = utils::join(selected_specials);
583 }
584 }
585}
586
588 find_widget<slider>("resistances_slider")
589 .set_value(
590 100 - resistances_[find_widget<menu_button>("resistances_list").get_value_string()].to_int());
591
592 find_widget<slider>("resistances_slider")
593 .set_active(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
594
595 find_widget<toggle_button>("resistances_checkbox")
596 .set_value(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
597}
598
600 resistances_[find_widget<menu_button>("resistances_list").get_value_string()]
601 = 100 - find_widget<slider>("resistances_slider").get_value();
602}
603
605 bool toggle = find_widget<toggle_button>("resistances_checkbox").get_value();
606 res_toggles_[find_widget<menu_button>("resistances_list").get_value()] = toggle;
607 find_widget<slider>("resistances_slider").set_active(toggle);
608}
609
611 find_widget<slider>("defense_slider")
612 .set_value(
613 100 - defenses_[find_widget<menu_button>("defense_list").get_value_string()].to_int());
614
615 find_widget<slider>("defense_slider")
616 .set_active(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
617
618 find_widget<toggle_button>("defense_checkbox")
619 .set_value(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
620}
621
623 defenses_[find_widget<menu_button>("defense_list").get_value_string()]
624 = 100 - find_widget<slider>("defense_slider").get_value();
625}
626
628 bool toggle = find_widget<toggle_button>("defense_checkbox").get_value();
629 def_toggles_[find_widget<menu_button>("defense_list").get_value()] = toggle;
630 find_widget<slider>("defense_slider").set_active(toggle);
631}
632
634 find_widget<slider>("movement_costs_slider")
635 .set_value(
636 movement_[find_widget<menu_button>("movement_costs_list").get_value_string()].to_int());
637
638 find_widget<slider>("movement_costs_slider")
639 .set_active(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
640
641 find_widget<toggle_button>("movement_costs_checkbox")
642 .set_value(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
643}
644
646 movement_[find_widget<menu_button>("movement_costs_list").get_value_string()]
647 = find_widget<slider>("movement_costs_slider").get_value();
648}
649
651 bool toggle = find_widget<toggle_button>("movement_costs_checkbox").get_value();
652 move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()] = toggle;
653 find_widget<slider>("movement_costs_slider").set_active(toggle);
654}
655
657 // Textdomain
658 std::string current_textdomain = "wesnoth-"+addon_id_;
659
660 // Save current attack data
661 if (selected_attack_ < 1) {
662 return;
663 }
664
665 config attack;
666 attack["name"] = find_widget<text_box>("atk_id_box").get_value();
667 attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
668 attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
669 attack["type"] = find_widget<combobox>("attack_type_list").get_value();
670 attack["damage"] = find_widget<slider>("dmg_box").get_value();
671 attack["number"] = find_widget<slider>("dmg_num_box").get_value();
672 attack["range"] = find_widget<combobox>("range_list").get_value();
673
674 attacks_.at(selected_attack_-1) = {
675 find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(),
676 attack
677 };
678}
679
681 //Load data
682 if (selected_attack_ < 1) {
683 return;
684 }
685
686 config& attack = attacks_.at(selected_attack_-1).second;
687
688 find_widget<text_box>("atk_id_box").set_value(attack["name"]);
689 find_widget<text_box>("atk_name_box").set_value(attack["description"]);
690 find_widget<text_box>("path_attack_image").set_value(attack["icon"]);
691 update_image("attack_image");
692 find_widget<slider>("dmg_box").set_value(attack["damage"].to_int());
693 find_widget<slider>("dmg_num_box").set_value(attack["number"].to_int());
694 find_widget<combobox>("range_list").set_value(attack["range"]);
695
697 find_widget<combobox>("attack_type_list"), resistances_list_, attack["type"]);
698
699 find_widget<multimenu_button>("weapon_specials_list")
700 .select_options(attacks_.at(selected_attack_-1).first);
701}
702
704 find_widget<button>("atk_prev").set_active(selected_attack_ > 1);
705 find_widget<button>("atk_delete").set_active(selected_attack_ > 0);
706 find_widget<button>("atk_next").set_active(selected_attack_ != attacks_.size());
707
708 if (!attacks_.empty()) {
709 std::vector<config> atk_name_list;
710 for(const auto& atk_data : attacks_) {
711 atk_name_list.emplace_back("label", atk_data.second["name"]);
712 }
713 menu_button& atk_list = find_widget<menu_button>("atk_list");
714 atk_list.set_values(atk_name_list);
715 atk_list.set_selected(selected_attack_-1, false);
716 }
717
718 // Set index
719 const std::string new_index_str = formatter() << selected_attack_ << "/" << attacks_.size();
720 find_widget<label>("atk_number").set_label(new_index_str);
721}
722
724 // Textdomain
725 std::string current_textdomain = "wesnoth-"+addon_id_;
726
727 config attack;
728 attack["name"] = find_widget<text_box>("atk_id_box").get_value();
729 attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
730 attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
731 attack["type"] = find_widget<combobox>("attack_type_list").get_value();
732 attack["damage"] = find_widget<slider>("dmg_box").get_value();
733 attack["number"] = find_widget<slider>("dmg_num_box").get_value();
734 attack["range"] = find_widget<combobox>("range_list").get_value();
735
737
739 attacks_.begin() + selected_attack_ - 1
740 , std::make_pair(find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(), attack));
741
742 update_index();
743}
744
746 if (!attacks_.empty()) {
747 attacks_.erase(attacks_.begin() + selected_attack_ - 1);
748 }
749
750 if (attacks_.empty()) {
751 // clear fields instead since there are no attacks to show
753 find_widget<button>("atk_delete").set_active(false);
754 } else if (selected_attack_ == 1) {
755 // 1st attack removed, show the next one
756 next_attack();
757 } else {
758 // show previous attack otherwise
759 prev_attack();
760 }
761
762 update_index();
763}
764
766 store_attack();
767
768 if (attacks_.size() > 1) {
771 }
772
773 update_index();
774}
775
777 store_attack();
778
779 if (selected_attack_ > 0) {
781 }
782
783 if (attacks_.size() > 1) {
785 }
786
787 update_index();
788}
789
791 selected_attack_ = find_widget<menu_button>("atk_list").get_value()+1;
793 update_index();
794}
795
796//TODO Check if works with non-mainline movetypes
798 for(const auto& movetype : game_config_
799 .mandatory_child("units")
800 .child_range("movetype")) {
801 if (movetype["name"] == find_widget<menu_button>("movetype_list").get_value_string()) {
802 // Set resistances
803 resistances_ = movetype.mandatory_child("resistance");
805 // Set defense
806 defenses_ = movetype.mandatory_child("defense");
808 // Set movement
809 movement_ = movetype.mandatory_child("movement_costs");
811 }
812 }
813}
814
816 store_attack();
818
819 std::stringstream wml_stream;
820
821 // Textdomain
822 std::string current_textdomain = "wesnoth-" + addon_id_;
823
824 wml_stream
825 << "#textdomain " << current_textdomain << "\n"
826 << "#\n"
827 << "# This file was generated using the scenario editor.\n"
828 << "#\n";
829
830 config_writer out(wml_stream, false);
831
832 config& utype_cfg = type_cfg_.mandatory_child("unit_type");
833
834 // Update movement, defense and resistance in cfg
835 if (!movement_.empty() && (move_toggles_.size() <= movement_.attribute_count()) && move_toggles_.any())
836 {
837 config& mvt_cfg = utype_cfg.add_child("movement_costs");
838 int i = 0;
839 for (const auto& [key, value] : movement_.attribute_range()) {
840 if (move_toggles_[i] == 1) {
841 mvt_cfg[key] = value;
842 }
843 i++;
844 }
845 }
846
847 if (!defenses_.empty() && def_toggles_.any() && (def_toggles_.size() <= defenses_.attribute_count()))
848 {
849 config& def_cfg = utype_cfg.add_child("defense");
850 int i = 0;
851 for (const auto& [key, value] : defenses_.attribute_range()) {
852 if (def_toggles_[i] == 1) {
853 def_cfg[key] = value;
854 }
855 i++;
856 }
857 }
858
860 {
861 config& res_cfg = utype_cfg.add_child("resistance");
862 int i = 0;
863 for (const auto& [key, value] : resistances_.attribute_range()) {
864 if (res_toggles_[i] == 1) {
865 res_cfg[key] = value;
866 }
867 i++;
868 }
869 }
870
871 out.write(type_cfg_);
872 generated_wml = wml_stream.str();
873 find_widget<scroll_text>("wml_view").set_label(generated_wml);
874}
875
876void editor_edit_unit::update_image(const std::string& id_stem) {
877 std::string rel_path = find_widget<text_box>("path_"+id_stem).get_value();
878
879 // remove IPF
880 if (rel_path.find("~") != std::string::npos) {
881 rel_path = rel_path.substr(0, rel_path.find("~"));
882 }
883
884 int scale_size = 200; // TODO: Arbitrary, can be changed later.
885 if (rel_path.size() > 0) {
886 point img_size = ::image::get_size(::image::locator{rel_path});
887 float aspect_ratio = static_cast<float>(img_size.x)/img_size.y;
888 if(img_size.x > scale_size) {
889 rel_path.append("~SCALE(" + std::to_string(scale_size) + "," + std::to_string(scale_size*aspect_ratio) + ")");
890 } else if (img_size.y > scale_size) {
891 rel_path.append("~SCALE(" + std::to_string(scale_size/aspect_ratio) + "," + std::to_string(scale_size) + ")");
892 }
893 }
894
895 if (id_stem == "portrait_image") {
896 // portrait image uses same [image] as unit_image
897 find_widget<image>("unit_image").set_label(rel_path);
898 } else {
899 find_widget<image>(id_stem).set_label(rel_path);
900 }
901
903 queue_redraw();
904}
905
906bool editor_edit_unit::check_id(const std::string& id) {
907 for(char c : id) {
908 if (!(std::isalnum(c) || c == '_' || c == ' ')) {
909 // One bad char means entire id string is invalid
910 return false;
911 }
912 }
913 return true;
914}
915
917 std::string id = find_widget<text_box>("id_box").get_value();
918 std::string name = find_widget<text_box>("name_box").get_value();
919
920 find_widget<button>("ok").set_active(!id.empty() && !name.empty() && check_id(id));
921
922 queue_redraw();
923}
924
926 const std::string& message
927 = _("Unsaved changes will be lost. Do you want to leave?");
930 }
931}
932
934 // Write the file
936
937 std::string unit_name = type_cfg_.mandatory_child("unit_type")["name"];
938 boost::algorithm::replace_all(unit_name, " ", "_");
939
940 // Path to <unit_type_name>.cfg
942
943 // Write to file
944 try {
946 gui2::show_transient_message("", _("Unit type saved."));
947 } catch(const filesystem::io_exception& e) {
949 }
950}
951
953 bool& handled,
954 const SDL_Keycode key,
955 SDL_Keymod modifier)
956{
957 #ifdef __APPLE__
958 // Idiomatic modifier key in macOS computers.
959 const SDL_Keycode modifier_key = SDL_KMOD_GUI;
960 #else
961 // Idiomatic modifier key in Microsoft desktop environments. Common in
962 // GNU/Linux as well, to some extent.
963 const SDL_Keycode modifier_key = SDL_KMOD_CTRL;
964 #endif
965
966 // Ctrl+O shortcut for Load Unit Type
967 switch(key) {
968 case SDLK_0:
969 if (modifier & modifier_key) {
970 handled = true;
972 }
973 break;
974 }
975
976}
977
978}
Class for writing a config out to a file in pieces.
void write(const config &cfg, bool strong_quotes=false)
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
config & add_child(std::string_view key)
Definition config.cpp:436
void insert(std::string_view key, T &&value)
Inserts an attribute into the config.
Definition config.hpp:519
std::size_t attribute_count() const
Count the number of non-blank attributes.
Definition config.cpp:307
const_attr_itors attribute_range() const
Definition config.cpp:740
child_itors child_range(std::string_view key)
Definition config.cpp:268
bool empty() const
Definition config.cpp:823
config & mandatory_child(std::string_view key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition config.cpp:362
void clear()
Definition config.cpp:802
std::ostringstream wrapper.
Definition formatter.hpp:40
A class grating read only view to a vector of config objects, viewed as one config with all children ...
const config & mandatory_child(std::string_view key) const
Simple push button.
Definition button.hpp:36
Class for a combobox.
Definition combobox.hpp:37
void set_values(const std::vector<::config > &values, unsigned selected=0)
Definition combobox.cpp:278
Dialog that allows user to create custom unit types.
Definition edit_unit.hpp:33
unsigned int selected_attack_
0 means there are no attacks.
Definition edit_unit.hpp:65
void write()
Write the cfg file.
void quit_confirmation()
Quit confirmation.
void set_selected_from_string(menu_button &list, std::vector< config > values, const std::string &item)
std::vector< config > defense_list_
Definition edit_unit.hpp:56
boost::dynamic_bitset res_toggles_
Used to control checkboxes for various resistances, defences, etc.
Definition edit_unit.hpp:54
const game_config_view & game_config_
Definition edit_unit.hpp:44
std::vector< std::pair< boost::dynamic_bitset<>, config > > attacks_
Definition edit_unit.hpp:59
std::vector< config > abilities_list_
Definition edit_unit.hpp:57
void store_attack()
Callbacks for attack page.
std::vector< config > movetype_list_
Definition edit_unit.hpp:56
std::vector< config > race_list_
Definition edit_unit.hpp:56
void update_defenses()
Callbacks for defense list.
std::string generated_wml
Generated WML.
Definition edit_unit.hpp:62
void save_unit_type()
Save Unit Type data to cfg.
std::vector< config > align_list_
Definition edit_unit.hpp:56
void button_state_change()
Callback to enable/disable OK button if ID/Name is invalid.
void signal_handler_sdl_key_down(const event::ui_event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
editor_edit_unit(const game_config_view &game_config, const std::string &addon_id)
Definition edit_unit.cpp:57
void select_file(const std::string &default_dir, const std::string &id_stem)
Callback for file select button.
boost::dynamic_bitset move_toggles_
Definition edit_unit.hpp:54
void update_movement_costs()
Callbacks for movement list.
std::vector< config > resistances_list_
Definition edit_unit.hpp:56
void on_page_select()
Callback when an tab item in the "page" listbox is selected.
void update_resistances()
Callback for resistance list.
bool check_id(const std::string &id)
Utility method to check if ID contains any invalid characters.
const std::string & addon_id_
Definition edit_unit.hpp:45
std::vector< config > specials_list_
Definition edit_unit.hpp:57
boost::dynamic_bitset def_toggles_
Definition edit_unit.hpp:54
void load_unit_type()
Load Unit Type data from cfg.
virtual void pre_show() override
Actions to be taken before showing the window.
Definition edit_unit.cpp:74
void update_wml_view()
Update wml preview.
std::vector< config > usage_type_list_
Definition edit_unit.hpp:56
void load_movetype()
Callback for loading movetype data in UI.
void update_image(const std::string &id_stem)
Callback for image update.
file_dialog & set_path(const std::string &value)
Sets the initial file selection.
file_dialog & set_ok_label(const std::string &value)
Sets the OK button label.
std::string path() const
Gets the current file selection.
file_dialog & set_read_only(bool value)
Whether to provide user interface elements for manipulating existing objects.
file_dialog & set_title(const std::string &value)
Sets the current dialog title text.
Main class to show messages to the user.
Definition message.hpp:36
@ yes_no_buttons
Shows a yes and no button.
Definition message.hpp:81
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
At the moment two kinds of tips are known:
Definition tooltip.cpp:41
static std::unique_ptr< units_dialog > build_create_dialog(const std::vector< const unit_type * > &types_list)
void set_selected(unsigned selected, bool fire_event=true)
void set_values(const std::vector<::config > &values, unsigned selected=0)
void set_values(const std::vector<::config > &values)
Set the available menu options.
A container widget that shows one of its pages of widgets depending on which tab the user clicked.
unsigned get_active_tab_index()
void queue_redraw()
Indicates that this widget should be redrawn.
Definition widget.cpp:464
point get_size() const
Returns the size of the widget.
Definition widget.cpp:316
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition window.hpp:387
void invalidate_layout()
Updates the size of the window.
Definition window.cpp:759
static const std::string & type()
Static type getter that does not rely on the widget being constructed.
Generic locator abstracting the location of an image.
Definition picture.hpp:59
The basic "size" of the unit - flying, small land, large land, etc.
Definition movetype.hpp:44
@ FEMALE
Definition race.hpp:28
const std::vector< const unit_type * > types_list() const
Definition types.hpp:399
const std::map< std::string, config > & abilities() const
Definition types.hpp:410
const race_map & races() const
Definition types.hpp:408
const std::map< std::string, config > & specials() const
Definition types.hpp:411
const movement_type_map & movement_types() const
Definition types.hpp:409
const config * cfg
Declarations for File-IO.
static std::string _(const char *str)
Definition gettext.hpp:100
std::string id
Text to match against addon_info.tags()
Definition manager.cpp:199
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
const std::string wml_extension
void copy_file(const std::string &src, const std::string &dest)
Read a file and then writes it back out.
void write_file(const std::string &fname, const std::string &data, std::ios_base::openmode mode=std::ios_base::binary)
Throws io_exception if an error occurs.
std::string get_current_editor_dir(const std::string &addon_id)
utils::optional< std::string > to_asset_path(const std::string &abs_path, const std::string &addon_id, const std::string &asset_type)
Helper function to convert absolute path to wesnoth relative path.
Game configuration data as global variables.
ui_event
The event sent to the dispatcher.
Definition handler.hpp:115
void show_transient_message(const std::string &title, const std::string &message, const std::string &image, const bool message_use_markup, const bool title_use_markup)
Shows a transient message to the user.
void show_message(const std::string &title, const std::string &msg, const std::string &button_caption, const bool auto_close, const bool message_use_markup, const bool title_use_markup)
Shows a message to the user.
Definition message.cpp:148
@ OK
Dialog was closed with the OK button.
Definition retval.hpp:35
@ CANCEL
Dialog was closed with the CANCEL button.
Definition retval.hpp:38
std::string join(const Range &v, const std::string &s=",")
Generates a new string joining container items in a list.
static config unit_name(const unit *u)
Definition reports.cpp:163
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
An exception object used when an IO error occurs.
Holds a 2D point.
Definition point.hpp:25
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
Definition enum_base.hpp:46
unit_type_data unit_types
Definition types.cpp:1499
#define i
#define e
#define c