Add functionality for achievements to be partially complete.

Instead of being either complete or incomplete, achievements can now specify a value at which they will be considered complete. For such achievements that are not yet complete, a progress bar is added to the achievements dialog showing how close to completion the achievement is.
This commit is contained in:
Pentarctagon 2023-01-27 00:07:15 -06:00 committed by Pentarctagon
parent cbbd34a79e
commit e3bb346b39
16 changed files with 341 additions and 46 deletions

View file

@ -2,5 +2,12 @@
[achievement_group]
display_name=_"Tutorial"
content_for=tutorial
{ACHIEVEMENT "completed" _"Complete the Tutorial" _"Complete all scenarios of the Tutorial campaign."}
[achievement]
id="completed"
name=_"Completed the Tutorial"
description=_"Completed all scenarios of the Tutorial campaign."
icon="data/core/images/icons/potion_red_small.png"
icon_completed="data/core/images/icons/potion_green_small.png"
max_progress=2
[/achievement]
[/achievement_group]

View file

@ -1183,6 +1183,13 @@
caption= _ "Victory"
message= _ "After your victory notice, the map will be grayed out to indicate that the scenario is over; this is called <i>linger mode</i>. You will still be able to examine the final positions and state of your troops and any surviving enemies. When youre finished, click the <b>End Scenario</b> button to go on to the next scenario in the campaign."
[/hint_message]
[progress_achievement]
content_for=tutorial
id="completed"
amount=1
limit=1
[/progress_achievement]
[/event]
[event]

View file

@ -1498,10 +1498,11 @@ Rest-healing is an exception to the rule — if a unit doesnt do anything for
message= _ "You can also refer to the in-game help browser if you ever need to refresh your memory on gameplay mechanics."
[/message]
[set_achievement]
[progress_achievement]
content_for=tutorial
id="completed"
[/set_achievement]
amount=1
[/progress_achievement]
{CLEAR_VARIABLE low_hp_unit_message,lhpu_msg_i}
{CLEAR_VARIABLE spoke_about_income,spoke_about_orcs_crossing_river}

View file

@ -1,7 +1,6 @@
#textdomain wesnoth-lib
###
### Definition of an progress bar, which has the same height on normal and tiny
### gui.
### Definition of an progress bar, which has the same height on normal and tiny gui.
###
[progress_bar_definition]
@ -56,3 +55,4 @@
[/resolution]
[/progress_bar_definition]

View file

@ -0,0 +1,57 @@
#textdomain wesnoth-lib
###
### Definition of a thin progress bar, which has the same height on normal and tiny gui.
###
[progress_bar_definition]
id = "default_thin_achievements"
description = "A thin progress_bar."
[resolution]
min_width = 14
min_height = 10
default_width = 480
default_height = 10
max_width = 0
max_height = 0
[state_enabled]
[draw]
[rectangle]
x = 0
y = 0
w = "(width)"
h = "(height)"
border_thickness = 1
border_color = {GUI__BORDER_COLOR_DARK}
[/rectangle]
[rectangle]
x = 1
y = 1
w = "(width - 2)"
h = "(height - 2)"
fill_color = {GUI__BACKGROUND_COLOR_ENABLED}
[/rectangle]
[rectangle]
x = 2
y = 2
w = "(((width - 4) * percentage) / 100)"
h = "(height - 4)"
border_thickness = 0
fill_color = "0, 55, 82, 255"
[/rectangle]
[/draw]
[/state_enabled]
[/resolution]
[/progress_bar_definition]

View file

@ -50,20 +50,6 @@
[/label]
[/column]
[column]
horizontal_alignment = "right"
grow_factor = 0
border = "all"
border_size = 5
[label]
definition = "default"
label = _ "Content:"
[/label]
[/column]
[column]
horizontal_alignment = "right"
@ -131,8 +117,8 @@
[image]
name = "(text)"
w = "(min(image_original_width, 60))"
h = "(min(image_original_height, 60))"
w = "(min(image_original_width, 72))"
h = "(min(image_original_height, 72))"
{GUI_CENTERED_IMAGE}
[/image]
@ -157,6 +143,7 @@
[label]
id = "name"
definition = "default_large"
characters_per_line = 70
use_markup = true
[/label]
[/column]
@ -177,6 +164,20 @@
[/label]
[/column]
[/row]
[row]
[column]
grow_factor = 1
horizontal_grow = true
border = "all"
border_size = 10
[progress_bar]
id = "achievement_progress"
definition = "default_thin_achievements"
[/progress_bar]
[/column]
[/row]
[/grid]
[/column]
[/row]
@ -195,22 +196,39 @@
[/row]
[row]
grow_factor = 0
[column]
border = "all"
border_size = 5
horizontal_alignment = "right"
horizontal_grow = true
[grid]
[row]
[column]
grow_factor = 0
border = "all"
border_size = 5
horizontal_alignment = "left"
[button]
id = "ok"
definition = "default"
[label]
id = "achievement_count"
definition = "default_small"
[/label]
[/column]
label = _ "OK"
[/button]
[column]
grow_factor = 1
border = "all"
border_size = 5
horizontal_alignment = "right"
[button]
id = "ok"
definition = "default"
label = _ "Close"
[/button]
[/column]
[/row]
[/grid]
[/column]
[/row]
[/grid]

View file

@ -1020,3 +1020,11 @@ function wml_actions.set_achievement(cfg)
gui.show_popup(achievement.name_completed, achievement.description_completed, achievement.icon_completed)
end
end
function wml_actions.progress_achievement(cfg)
local pcfg = wesnoth.achievements.progress(cfg.content_for, cfg.id, cfg.amount, cfg.limit or 999999999)
-- if this update completes the achievement, mark it as complete and show the popup
if pcfg.progress ~= -1 and pcfg.max_progress > 0 and pcfg.progress >= pcfg.max_progress then
wml_actions.set_achievement(cfg)
end
end

View file

@ -21,6 +21,7 @@
{SIMPLE_KEY hidden bool}
{SIMPLE_KEY hidden_name t_string}
{SIMPLE_KEY hidden_hint t_string}
{SIMPLE_KEY max_progress int}
[/tag]
[/tag]
[/tag]

View file

@ -107,8 +107,11 @@ achievement_group::achievement_group(const config& cfg)
if(id.empty()) {
ERR_CONFIG << content_for_ + " achievement missing id attribute:\n" << ach.debug();
} else if(id.find(',') != std::string::npos) {
ERR_CONFIG << content_for_ + " achievement missing id " << id << " contains a comma, skipping.";
continue;
} else {
achievements_.emplace_back(ach, preferences::achievement(content_for_, id));
achievements_.emplace_back(ach, preferences::achievement(content_for_, id), preferences::progress_achievement(content_for_, id));
}
}
}

View file

@ -49,8 +49,12 @@ struct achievement
t_string hidden_hint_;
/** Whether the achievement has been completed. */
bool achieved_;
/** When the achievement's current progress matches or equals this value, then it should be marked as completed */
int max_progress_;
/** The current progress value of the achievement */
int current_progress_;
achievement(const config& cfg, bool achieved)
achievement(const config& cfg, bool achieved, int progress)
: id_(cfg["id"].str())
, name_(cfg["name"].t_str())
, name_completed_(cfg["name_completed"].t_str())
@ -62,6 +66,8 @@ struct achievement
, hidden_name_(cfg["hidden_name"].t_str())
, hidden_hint_(cfg["hidden_hint"].t_str())
, achieved_(achieved)
, max_progress_(cfg["max_progress"].to_int(0))
, current_progress_(progress)
{
if(name_completed_.empty()) {
name_completed_ = name_;
@ -70,7 +76,8 @@ struct achievement
description_completed_ = description_;
}
if(icon_completed_.empty()) {
icon_completed_ = icon_;
// avoid the ~GS() appended to icon_
icon_completed_ = cfg["icon"].str();
}
}
};

View file

@ -20,6 +20,9 @@
#include "game_config_manager.hpp"
#include "gettext.hpp"
#include "gui/auxiliary/find_widget.hpp"
#include "gui/widgets/grid.hpp"
#include "gui/widgets/label.hpp"
#include "gui/widgets/progress_bar.hpp"
#include "gui/widgets/window.hpp"
#include "log.hpp"
@ -28,6 +31,8 @@ namespace gui2::dialogs
REGISTER_DIALOG(achievements_dialog)
unsigned int achievements_dialog::selected_index_ = 0;
achievements_dialog::achievements_dialog()
: modal_dialog(window_id())
, achieve_()
@ -44,25 +49,36 @@ void achievements_dialog::pre_show(window& win)
achievements_box_ = find_widget<listbox>(&win, "achievements_list", false, true);
auto* a = game_config_manager::get();
auto b = a->get_achievements();
for(const auto& list : b) {
// populate all possibilities into the dropdown
content_list.emplace_back("label", list.display_name_);
std::vector<achievement_group> groups = game_config_manager::get()->get_achievements();
// reset the selected achievement group in case add-ons with achievements are uninstalled between closing and re-opening the dialog
if(selected_index_ > groups.size()) {
selected_index_ = 0;
}
for(const auto& list : groups) {
// only display the achievements for the first dropdown option on first showing the dialog
if(content_list.size() == 1) {
if(content_list.size() == selected_index_) {
int achieved_count = 0;
for(const auto& ach : list.achievements_) {
widget_data row;
widget_item item;
if(ach.achieved_) {
achieved_count++;
}
item["label"] = !ach.achieved_ ? ach.icon_ : ach.icon_completed_;
row.emplace("icon", item);
if(ach.hidden_ && !ach.achieved_) {
item["label"] = ach.hidden_name_;
} else if(!ach.achieved_) {
item["label"] = ach.name_;
std::string name = ach.name_;
if(ach.max_progress_ != 0 && ach.current_progress_ != -1) {
name += " ("+std::to_string(ach.current_progress_)+"/"+std::to_string(ach.max_progress_)+")";
}
item["label"] = name;
} else {
item["label"] = "<span color='green'>"+ach.name_completed_+"</span>";
}
@ -77,12 +93,25 @@ auto b = a->get_achievements();
}
row.emplace("description", item);
achievements_box_->add_row(row);
grid& newrow = achievements_box_->add_row(row);
progress_bar* achievement_progress = static_cast<progress_bar*>(newrow.find("achievement_progress", false));
if(ach.max_progress_ != 0 && ach.current_progress_ != -1) {
achievement_progress->set_percentage((ach.current_progress_/double(ach.max_progress_))*100);
} else {
achievement_progress->set_visible(gui2::widget::visibility::invisible);
}
}
label* achieved_label = find_widget<label>(&win, "achievement_count", false, true);
achieved_label->set_label(_("Completed")+" "+std::to_string(achieved_count)+"/"+std::to_string(list.achievements_.size()));
}
// populate all possibilities into the dropdown
content_list.emplace_back("label", list.display_name_);
}
if(content_list.size() > 0) {
content_names_->set_values(content_list);
content_names_->set_selected(selected_index_, false);
}
}
@ -93,10 +122,18 @@ void achievements_dialog::post_show(window&)
void achievements_dialog::set_achievements_content()
{
achievements_box_->clear();
for(const auto& ach : game_config_manager::get()->get_achievements().at(content_names_->get_value()).achievements_) {
int achieved_count = 0;
selected_index_ = content_names_->get_value();
achievement_group list = game_config_manager::get()->get_achievements().at(selected_index_);
for(const auto& ach : list.achievements_) {
widget_data row;
widget_item item;
if(ach.achieved_) {
achieved_count++;
}
item["label"] = !ach.achieved_ ? ach.icon_ : ach.icon_completed_;
row.emplace("icon", item);
@ -118,8 +155,17 @@ void achievements_dialog::set_achievements_content()
}
row.emplace("description", item);
achievements_box_->add_row(row);
grid& newrow = achievements_box_->add_row(row);
progress_bar* achievement_progress = static_cast<progress_bar*>(newrow.find("achievement_progress", false));
if(ach.max_progress_ != 0 && ach.current_progress_ != -1) {
achievement_progress->set_percentage((ach.current_progress_/double(ach.max_progress_))*100);
} else {
achievement_progress->set_visible(gui2::widget::visibility::invisible);
}
}
label* achieved_label = find_widget<label>(get_window(), "achievement_count", false, true);
achieved_label->set_label(_("Completed")+" "+std::to_string(achieved_count)+"/"+std::to_string(list.achievements_.size()));
}
} // namespace gui2::dialogs

View file

@ -46,6 +46,8 @@ private:
achievements achieve_;
listbox* achievements_box_;
menu_button* content_names_;
/** variable of the most recently selected achievements, static to persist between closing and re-opening the dialog */
static unsigned int selected_index_;
void set_achievements_content();

View file

@ -1037,10 +1037,16 @@ void set_achievement(const std::string& content_for, const std::string& id)
if(ach["content_for"].str() == content_for)
{
std::vector<std::string> ids = utils::split(ach["ids"]);
if(std::find(ids.begin(), ids.end(), id) == ids.end())
if(ids.empty())
{
ach["ids"] = id;
}
else if(std::find(ids.begin(), ids.end(), id) == ids.end())
{
ach["ids"] = ach["ids"].str() + "," + id;
}
ach.remove_children("in_progress", [&id](config cfg){return cfg["id"].str() == id;});
return;
}
}
@ -1052,5 +1058,59 @@ void set_achievement(const std::string& content_for, const std::string& id)
prefs.add_child("achievements", ach);
}
int progress_achievement(const std::string& content_for, const std::string& id, int limit, int max_progress, int amount)
{
if(achievement(content_for, id))
{
return -1;
}
for(config& ach : prefs.child_range("achievements"))
{
// if achievements already exist for this content and the achievement has not already been set, add it
if(ach["content_for"].str() == content_for)
{
// check if this achievement has progressed before - if so then increment it
for(config& in_progress : ach.child_range("in_progress"))
{
if(in_progress["id"].str() == id)
{
in_progress["progress_at"] = std::clamp(in_progress["progress_at"].to_int() + amount, 0, std::min(limit, max_progress));
return in_progress["progress_at"].to_int();
}
}
// else this is the first time this achievement is progressing
if(amount != 0)
{
config set_progress;
set_progress["id"] = id;
set_progress["progress_at"] = std::clamp(amount, 0, std::min(limit, max_progress));
config& child = ach.add_child("in_progress", set_progress);
return child["progress_at"].to_int();
}
return 0;
}
}
// else not only has this achievement not progressed before, this is the first achievement for this achievement group to be added
if(amount != 0)
{
config ach;
config set_progress;
set_progress["id"] = id;
set_progress["progress_at"] = std::clamp(amount, 0, std::min(limit, max_progress));
ach["content_for"] = content_for;
ach["ids"] = "";
config& child = ach.add_child("in_progress", set_progress);
prefs.add_child("achievements", ach);
return child["progress_at"].to_int();
}
return 0;
}
} // end namespace preferences

View file

@ -274,7 +274,32 @@ namespace preferences {
sort_order::type addon_manager_saved_order_direction();
void set_addon_manager_saved_order_direction(sort_order::type value);
/**
* @param content_for The achievement group the achievement is part of.
* @param id The ID of the achievement within the achievement group.
* @return True if the achievement exists and is completed, false otherwise.
*/
bool achievement(const std::string& content_for, const std::string& id);
/**
* Marks the specified achievement as completed.
*
* @param content_for The achievement group the achievement is part of.
* @param id The ID of the achievement within the achievement group.
*/
void set_achievement(const std::string& content_for, const std::string& id);
/**
* Increments the achievement's current progress by @a amount if it hasn't already been completed.
* If you only want to check the achievement's current progress, then omit the last three arguments.
* @a amount defaults to 0, which will result in the current progress value being returned without being changed (x + 0 == x).
*
* @param content_for The id of the achievement group this achievement is in.
* @param id The id for the specific achievement in the achievement group.
* @param limit The maximum value that a specific call to this function can increase the achievement progress value.
* @param max_progress The value when the achievement is considered completed.
* @param amount The amount to progress the achievement.
* @return The achievement's current progress, or -1 if it has already been completed.
*/
int progress_achievement(const std::string& content_for, const std::string& id, int limit = 999999, int max_progress = 999999, int amount = 0);
} // end namespace preferences

View file

@ -3145,6 +3145,8 @@ int game_lua_kernel::intf_get_achievement(lua_State *L)
cfg["hidden_name"] = achieve.hidden_name_;
cfg["hidden_hint"] = achieve.hidden_hint_;
cfg["achieved"] = achieve.achieved_;
cfg["max_progress"] = achieve.max_progress_;
cfg["current_progress"] = achieve.current_progress_;
luaW_pushconfig(L, cfg);
return 1;
}
@ -3161,6 +3163,55 @@ int game_lua_kernel::intf_get_achievement(lua_State *L)
return 1;
}
/**
* Progresses the provided achievement.
* - Arg 1: string - content_for.
* - Arg 2: string - id.
* - Arg 3: int - the amount to progress the achievement.
* - Arg 4: int - the limit the achievement can progress by
* - Ret 1: WML table returned by the function.
*/
int game_lua_kernel::intf_progress_achievement(lua_State *L)
{
const char *content_for = luaL_checkstring(L, 1);
const char *id = luaL_checkstring(L, 2);
int amount = luaL_checkinteger(L, 3);
int limit = luaL_checkinteger(L, 4);
config cfg;
cfg["progress"] = 0;
cfg["max_progress"] = 0;
for(achievement_group& group : game_config_manager::get()->get_achievements()) {
if(group.content_for_ == content_for) {
for(achievement& achieve : group.achievements_) {
if(achieve.id_ == id) {
// found the achievement
if(!achieve.achieved_) {
int progress = preferences::progress_achievement(content_for, id, limit, achieve.max_progress_, amount);
cfg["progress"] = progress;
achieve.current_progress_ = progress;
} else {
cfg["progress"] = -1;
}
cfg["max_progress"] = achieve.max_progress_;
luaW_pushconfig(L, cfg);
return 1;
}
}
// achievement not found - existing achievement group but non-existing achievement id
ERR_LUA << "Achievement " << id << " not found for achievement group " << content_for;
luaW_pushconfig(L, cfg);
return 1;
}
}
// achievement group not found
ERR_LUA << "Achievement group " << content_for << " not found";
luaW_pushconfig(L, cfg);
return 1;
}
/**
* Scrolls to given tile.
* - Arg 1: location.
@ -4988,6 +5039,7 @@ game_lua_kernel::game_lua_kernel(game_state & gs, play_controller & pc, reports
{ "set", &dispatch<&game_lua_kernel::intf_set_achievement> },
{ "has", &dispatch<&game_lua_kernel::intf_has_achievement> },
{ "get", &dispatch<&game_lua_kernel::intf_get_achievement> },
{ "progress", &dispatch<&game_lua_kernel::intf_progress_achievement> },
{ nullptr, nullptr }
};
lua_getglobal(L, "wesnoth");

View file

@ -122,6 +122,7 @@ class game_lua_kernel : public lua_kernel_base
int intf_set_achievement(lua_State *L);
int intf_has_achievement(lua_State *L);
int intf_get_achievement(lua_State *L);
int intf_progress_achievement(lua_State *L);
int intf_set_floating_label(lua_State* L, bool spawn);
int intf_remove_floating_label(lua_State* L);
int intf_move_floating_label(lua_State* L);