Add a new "rechargeable" flag to items

A way to set this flag is not yet exposed in the scenario editor.
The flag is intended only for non-stackable items, but this currently isn't enforced.
Items now have a maximum number of charges, which is equal to their default number set in the item record.
Enchanted items with charges are now rechargeable.
This commit is contained in:
2024-08-29 01:06:11 -04:00
committed by Celtic Minstrel
parent 753dbbcc59
commit a4231005f6
17 changed files with 51 additions and 30 deletions

View File

@@ -106,6 +106,7 @@
<xs:element name="cursed" type="bool" minOccurs="0"/>
<xs:element name="concealed" type="bool" minOccurs="0"/>
<xs:element name="enchanted" type="bool" minOccurs="0"/>
<xs:element name="rechargeable" type="bool" minOccurs="0"/>
<xs:element name="unsellable" type="bool" minOccurs="0"/>
</xs:all>
</xs:complexType>

View File

@@ -391,8 +391,8 @@ arrows, bolts, thrown missiles, or missiles with no ammo).
`help-one`, `harm-all`, `help-all`.
* `<properties>` - Contains several boolean subtags specifying properties of this item.
Recognized subtags are `<identified>` (indicating it is _always_ identified), `<magic>`,
`<cursed>`, `<concealed>`, `<enchanted>`, `<unsellable>`. Note that the editor UI gives no
access to the `<enchanted>` flag.
`<cursed>`, `<concealed>`, `<enchanted>`, `<unsellable>`, `<rechargeable>`.
Note that the editor UI gives no access to the `<enchanted>` flag.
* `<description>` - A description of the item. The scenario editor wraps the contents of
this element in a `CDATA` declaration.

View File

@@ -1112,6 +1112,7 @@ void readItemsFromXml(ticpp::Document&& data, cScenario& scenario) {
item->GetText(&the_item.protection);
} else if(type == "charges") {
item->GetText(&the_item.charges);
the_item.max_charges = the_item.charges;
} else if(type == "weapon-type") {
item->GetText(&the_item.weap_type);
} else if(type == "missile-type") {
@@ -1169,6 +1170,8 @@ void readItemsFromXml(ticpp::Document&& data, cScenario& scenario) {
the_item.concealed = state();
} else if(type == "enchanted") {
the_item.enchanted = state();
} else if(type == "rechargeable") {
the_item.rechargeable = state();
} else if(type == "unsellable") {
the_item.unsellable = state();
} else throw xBadNode(type, prop->Row(), prop->Column(), fname);

View File

@@ -971,7 +971,7 @@ void handle_item_shop_action(short item_hit) {
else {
play_sound(68);
ASB("Your item is recharged.");
target.charges += 5;
target.charges = target.max_charges;
}
break;
case MODE_SELL_WEAP: case MODE_SELL_ARMOR: case MODE_SELL_ANY:

View File

@@ -603,6 +603,10 @@ void use_item(short pc,short item) {
add_string_to_buf("Use: Can't use this item.");
take_charge = false;
}
if(item_rec.rechargeable && item_rec.charges == 0) {
add_string_to_buf("Use: No charges left.");
take_charge = false;
}
if(univ.party[pc].traits[eTrait::MAGICALLY_INEPT] && !inept_ok){
add_string_to_buf("Use: Can't - magically inept.");
take_charge = false;

View File

@@ -335,7 +335,7 @@ void put_item_screen(eItemWinMode screen_num) {
if((stat_screen_mode == MODE_SHOP) &&
((is_town()) || (is_out()) || ((is_combat()) && (pc == univ.cur_pc)))) { // place give and drop and use
place_item_graphic(i,item.graphic_num);
if(item.can_use()) // place use if can
if(item.can_use() && (item.rechargeable ? item.charges > 0 : true)) // place use if can
place_item_button(ITEMBTN_NORM,i);
else place_item_button(ITEMBTN_ALL,i);
}
@@ -346,7 +346,7 @@ void put_item_screen(eItemWinMode screen_num) {
((is_town()) || (is_out()) || ((is_combat()) && (pc == univ.cur_pc)))) { // place give and drop and use
place_item_button(1,i,ITEMBTN_GIVE);
place_item_button(2,i,ITEMBTN_DROP);
if(item.can_use()) // place use if can
if(item.can_use() && (item.rechargeable ? item.charges > 0 : true)) // place use if can
place_item_button(0,i,ITEMBTN_USE);
}
}
@@ -393,7 +393,7 @@ void place_buy_button(short position,short pc_num,short item_num) {
}
break;
case MODE_RECHARGE:
if(item.charges == 0 && item.can_use()) {
if(item.rechargeable && item.charges == 0 && item.can_use()) {
item_area_button_active[position][ITEMBTN_SPEC] = true;
source_rect = button_sources[3];
val_to_place = shop_identify_cost;

View File

@@ -195,7 +195,7 @@ cItem::cItem(){
awkward = 0;
bonus = 0;
protection = 0;
charges = 0;
charges = max_charges = 0;
weap_type = eSkill::INVALID;
magic_use_type = eItemUse::HELP_ONE;
graphic_num = 0;
@@ -212,7 +212,7 @@ cItem::cItem(){
item_loc.y = 0;
treas_class = 0;
ident = property = magic = contained = held = false;
cursed = concealed = enchanted = unsellable = false;
cursed = concealed = enchanted = rechargeable = unsellable = false;
}
cItem::cItem(eItemPreset preset) : cItem() {
@@ -336,6 +336,7 @@ cItem::cItem(eItemPreset preset) : cItem() {
graphic_num = 105; // The blank graphic
break;
}
max_charges = charges;
}
cItem::cItem(eAlchemy recipe) : cItem(ITEM_POTION) {
@@ -366,7 +367,8 @@ void cItem::enchant_weapon(eEnchant enchant_type) {
abil_data = info.abil_data;
}
if(info.charges > 0) {
charges = info.charges;
charges = max_charges = info.charges;
rechargeable = true;
}
if(value > 15000)
value = 15000;
@@ -381,7 +383,7 @@ void cItem::import_legacy(legacy::item_record_type& old){
awkward = old.awkward;
bonus = old.bonus;
protection = old.protection;
charges = old.charges;
charges = max_charges = old.charges;
if(old.type >= 1 && old.type <= 3)
weap_type = eSkill(old.type + 2);
else weap_type = eSkill::INVALID;
@@ -923,7 +925,7 @@ void cItem::import_legacy(legacy::item_record_type& old){
contained = old.item_properties & 8;
cursed = old.item_properties & 16;
concealed = old.item_properties & 32;
enchanted = held = false;
enchanted = rechargeable = held = false;
unsellable = old.item_properties & 16;
// Set missile, if needed
if(variety == eItemType::ARROW || variety == eItemType::BOLTS) {
@@ -1260,7 +1262,7 @@ void cItem::writeTo(cTagFile_Page& page) const {
page["AWKWARD"] << awkward;
page["BONUS"] << bonus;
page["PROT"] << protection;
page["CHARGES"] << charges;
page["CHARGES"] << charges << max_charges;
page["WEAPON"] << weap_type;
page["USE"] << magic_use_type;
page["ICON"] << graphic_num;
@@ -1285,6 +1287,7 @@ void cItem::writeTo(cTagFile_Page& page) const {
if(cursed) page.add("CURSED");
if(concealed) page.add("CONCEALED");
if(enchanted) page.add("ENCHANTED");
if(rechargeable) page.add("RECHARGEABLE");
if(unsellable) page.add("UNSELLABLE");
}
@@ -1294,7 +1297,7 @@ void cItem::readFrom(const cTagFile_Page& page){
page["AWKWARD"] >> awkward;
page["BONUS"] >> bonus;
page["PROT"] >> protection;
page["CHARGES"] >> charges;
page["CHARGES"] >> charges >> max_charges;
page["WEAPON"] >> weap_type;
page["USE"] >> magic_use_type;
page["ICON"] >> graphic_num;
@@ -1319,6 +1322,7 @@ void cItem::readFrom(const cTagFile_Page& page){
cursed = page.contains("CURSED");
concealed = page.contains("CONCEALED");
enchanted = page.contains("ENCHANTED");
rechargeable = page.contains("RECHARGEABLE");
unsellable = page.contains("UNSELLABLE");
}

View File

@@ -46,7 +46,7 @@ public:
int awkward;
int bonus;
int protection;
int charges;
int charges, max_charges;
eSkill weap_type;
eItemUse magic_use_type;
unsigned short graphic_num;
@@ -63,7 +63,7 @@ public:
std::string full_name;
std::string name;
unsigned int treas_class;
bool ident, property, magic, contained, held, cursed, concealed, enchanted, unsellable;
bool ident, property, magic, contained, held, cursed, concealed, enchanted, unsellable, rechargeable;
std::string desc;
unsigned char rec_treas_class() const;
short item_weight() const;

View File

@@ -464,6 +464,7 @@ void writeItemsToXml(ticpp::Printer&& data, cScenario& scenario) {
if(item.cursed) data.PushElement("cursed", "true");
if(item.concealed) data.PushElement("concealed", "true");
if(item.enchanted) data.PushElement("enchanted", "true");
if(item.rechargeable) data.PushElement("rechargeable", "true");
if(item.unsellable) data.PushElement("unsellable", "true");
data.CloseElement("properties");

View File

@@ -629,7 +629,7 @@ bool cParty::take_abil(eItemAbil abil, short dat) {
for(int i = 0; i < 6; i++)
if(adven[i]->main_status == eMainStatus::ALIVE)
if(cInvenSlot item = adven[i]->has_abil(abil,dat)) {
if(item->charges > 1)
if(item->charges > 1 || item->rechargeable)
item->charges--;
else adven[i]->take_item(item.slot);
return true;
@@ -637,12 +637,12 @@ bool cParty::take_abil(eItemAbil abil, short dat) {
return false;
}
bool cParty::has_class(unsigned int item_class) {
bool cParty::has_class(unsigned int item_class, bool require_charges) {
if(item_class == 0)
return false;
for(auto& pc : *this)
if(pc.main_status == eMainStatus::ALIVE)
if(cInvenSlot item = pc.has_class(item_class)) {
if(cInvenSlot item = pc.has_class(item_class, require_charges)) {
return true;
}
return false;
@@ -653,8 +653,8 @@ bool cParty::take_class(unsigned int item_class) const {
return false;
for(auto& pc : *this)
if(pc.main_status == eMainStatus::ALIVE)
if(cInvenSlot item = pc.has_class(item_class)) {
if(item->charges > 1)
if(cInvenSlot item = pc.has_class(item_class, true)) {
if(item->charges > 1 || item->rechargeable)
item->charges--;
else pc.take_item(item.slot);
return true;

View File

@@ -203,7 +203,7 @@ public:
bool forced_give(cItem item,eItemAbil abil,short dat = -1);
bool has_abil(eItemAbil abil, short dat = -1) const;
bool take_abil(eItemAbil abil, short dat = -1);
bool has_class(unsigned int item_class);
bool has_class(unsigned int item_class, bool require_charges = false);
bool take_class(unsigned int item_class) const;
bool start_split(short x, short y, snd_num_t noise, short who);

View File

@@ -768,6 +768,7 @@ cInvenSlot cPlayer::has_abil(eItemAbil abil,short dat) {
return find_item_matching([this,abil,dat](int, const cItem& item) {
if(item.variety == eItemType::NO_ITEM) return false;
if(item.ability != abil) return false;
if(item.charges == 0) return false;
if(dat >= 0 && dat != item.abil_data.value) return false;
return true;
});
@@ -807,14 +808,14 @@ const cInvenSlot cPlayer::has_class_equip(unsigned int item_class) const {
return const_cast<cPlayer*>(this)->has_class_equip(item_class);
}
cInvenSlot cPlayer::has_class(unsigned int item_class) {
return find_item_matching([item_class](int, const cItem& item) {
return item.special_class == item_class;
cInvenSlot cPlayer::has_class(unsigned int item_class, bool require_charges) {
return find_item_matching([item_class, require_charges](int, const cItem& item) {
return item.special_class == item_class && (!require_charges || item.charges > 0);
});
}
const cInvenSlot cPlayer::has_class(unsigned int item_class) const {
return const_cast<cPlayer*>(this)->has_class(item_class);
const cInvenSlot cPlayer::has_class(unsigned int item_class, bool require_charges) const {
return const_cast<cPlayer*>(this)->has_class(item_class, require_charges);
}
cInvenSlot::operator bool() const {
@@ -894,7 +895,7 @@ void cPlayer::take_item(int which_item) {
void cPlayer::remove_charge(int which_item) {
if(items[which_item].charges > 0) {
items[which_item].charges--;
if(items[which_item].charges == 0) {
if(items[which_item].charges == 0 && !items[which_item].rechargeable) {
take_item(which_item);
}
}

View File

@@ -168,8 +168,8 @@ public:
cInvenSlot has_type(eItemType type);
const cInvenSlot has_class_equip(unsigned int item_class) const;
cInvenSlot has_class_equip(unsigned int item_class);
const cInvenSlot has_class(unsigned int item_class) const;
cInvenSlot has_class(unsigned int item_class);
const cInvenSlot has_class(unsigned int item_class, bool require_charges = false) const;
cInvenSlot has_class(unsigned int item_class, bool require_charges = false);
short skill(eSkill skill) const;
short stat_adj(eSkill skill) const;

View File

@@ -28,8 +28,9 @@
<cursed>true</cursed>
<concealed>true</concealed>
<enchanted>true</enchanted>
<rechargeable>true</rechargeable>
<unsellable>true</unsellable>
</properties>
<description>This is a silly, silly description.</description>
</item>
</items>
</items>

View File

@@ -37,6 +37,7 @@ TEST_CASE("Converting items from legacy scenarios") {
CHECK(new_item.bonus == 1);
CHECK(new_item.protection == 3);
CHECK(new_item.charges == 10);
CHECK(new_item.max_charges == 10);
CHECK(new_item.graphic_num == 62);
CHECK(new_item.type_flag == 100);
CHECK(new_item.value == 500);

View File

@@ -116,6 +116,7 @@ TEST_CASE("Loading an item type definition") {
CHECK(scen.scen_items[0].bonus == 5);
CHECK(scen.scen_items[0].protection == 4);
CHECK(scen.scen_items[0].charges == 20);
CHECK(scen.scen_items[0].max_charges == 20);
CHECK(scen.scen_items[0].weap_type == eSkill::DEFENSE);
CHECK(scen.scen_items[0].missile == 3);
CHECK(scen.scen_items[0].type_flag == 9);
@@ -130,6 +131,7 @@ TEST_CASE("Loading an item type definition") {
CHECK(scen.scen_items[0].cursed);
CHECK(scen.scen_items[0].concealed);
CHECK(scen.scen_items[0].enchanted);
CHECK(scen.scen_items[0].rechargeable);
CHECK(scen.scen_items[0].unsellable);
CHECK(scen.scen_items[0].desc == "This is a silly, silly description.");
}

View File

@@ -78,6 +78,7 @@ TEST_CASE("Saving item types") {
scen.scen_items[0].cursed = true;
scen.scen_items[0].concealed = true;
scen.scen_items[0].enchanted = true;
scen.scen_items[0].rechargeable = true;
scen.scen_items[0].unsellable = true;
scen.scen_items[0].desc = " This is a silly, silly description. ";
in_and_out("full", scen);
@@ -86,6 +87,7 @@ TEST_CASE("Saving item types") {
CHECK(scen.scen_items[0].bonus == 5);
CHECK(scen.scen_items[0].protection == 4);
CHECK(scen.scen_items[0].charges == 20);
CHECK(scen.scen_items[0].max_charges == 20);
CHECK(scen.scen_items[0].weap_type == eSkill::DEFENSE);
CHECK(scen.scen_items[0].missile == 3);
CHECK(scen.scen_items[0].type_flag == 9);
@@ -100,6 +102,7 @@ TEST_CASE("Saving item types") {
CHECK(scen.scen_items[0].cursed);
CHECK(scen.scen_items[0].concealed);
CHECK(scen.scen_items[0].enchanted);
CHECK(scen.scen_items[0].rechargeable);
CHECK(scen.scen_items[0].unsellable);
CHECK(scen.scen_items[0].desc == " This is a silly, silly description. ");
}