diff --git a/data/json/artifact/relic_procgen_data.json b/data/json/artifact/relic_procgen_data.json index b9c8593b5ea80..1d0429219a883 100644 --- a/data/json/artifact/relic_procgen_data.json +++ b/data/json/artifact/relic_procgen_data.json @@ -2,6 +2,17 @@ { "type": "relic_procgen_data", "id": "cult", + "charge_types": [ + { + "weight": 100, + "charges": { "range": [ 0, 3 ], "power": 25 }, + "charges_per_use": { "range": [ 1, 1 ], "power": 25 }, + "max_charges": { "range": [ 1, 3 ], "power": 25 }, + "recharge_type": "periodic", + "time": [ "3 h", "6 h" ] + } + ], + "active_procgen_values": [ { "weight": 100, "spell_id": "AEA_PAIN", "base_power": 50 } ], "passive_add_procgen_values": [ { "weight": 100, "min_value": -1, "max_value": 1, "type": "STRENGTH", "increment": 1, "power_per_increment": 250 }, { @@ -37,12 +48,23 @@ "power_per_increment": 200 } ], - "type_weights": [ { "weight": 100, "value": "passive_enchantment_add" } ], + "type_weights": [ { "weight": 100, "value": "passive_enchantment_add" }, { "weight": 100, "value": "active_enchantment" } ], "items": [ { "weight": 100, "item": "spoon" } ] }, { "type": "relic_procgen_data", "id": "netherum_tunnels", + "charge_types": [ + { + "weight": 100, + "charges": { "range": [ 0, 3 ], "power": 25 }, + "charges_per_use": { "range": [ 1, 1 ], "power": 25 }, + "max_charges": { "range": [ 1, 3 ], "power": 25 }, + "recharge_type": "periodic", + "time": [ "3 h", "6 h" ] + } + ], + "active_procgen_values": [ { "weight": 100, "spell_id": "AEA_PAIN", "base_power": 50 } ], "passive_add_procgen_values": [ { "weight": 100, "min_value": -1, "max_value": 1, "type": "STRENGTH", "increment": 1, "power_per_increment": 250 }, { @@ -78,12 +100,23 @@ "power_per_increment": 200 } ], - "type_weights": [ { "weight": 100, "value": "passive_enchantment_add" } ], + "type_weights": [ { "weight": 100, "value": "passive_enchantment_add" }, { "weight": 100, "value": "active_enchantment" } ], "items": [ { "weight": 100, "item": "spoon" } ] }, { "type": "relic_procgen_data", "id": "alien_reality", + "charge_types": [ + { + "weight": 100, + "charges": { "range": [ 0, 3 ], "power": 25 }, + "charges_per_use": { "range": [ 1, 1 ], "power": 25 }, + "max_charges": { "range": [ 1, 3 ], "power": 25 }, + "recharge_type": "periodic", + "time": [ "3 h", "6 h" ] + } + ], + "active_procgen_values": [ { "weight": 100, "spell_id": "AEA_PAIN", "base_power": 50 } ], "passive_add_procgen_values": [ { "weight": 100, "min_value": -1, "max_value": 1, "type": "STRENGTH", "increment": 1, "power_per_increment": 250 }, { @@ -119,7 +152,7 @@ "power_per_increment": 200 } ], - "type_weights": [ { "weight": 100, "value": "passive_enchantment_add" } ], + "type_weights": [ { "weight": 100, "value": "passive_enchantment_add" }, { "weight": 100, "value": "active_enchantment" } ], "items": [ { "weight": 100, "item": "spoon" } ] } ] diff --git a/doc/ARTIFACTS.md b/doc/ARTIFACTS.md index f5beaa44006a3..24d7e5b5bc87c 100644 --- a/doc/ARTIFACTS.md +++ b/doc/ARTIFACTS.md @@ -11,6 +11,17 @@ The procedural generation of artifacts is defined in Json. The object looks like { "type": "relic_procgen_data", "id": "cult", + "charge_types": [ + { + "weight": 100, + "charges": { "range": [ 0, 3 ], "power": 25 }, + "charges_per_use":{ "range": [ 1, 1 ], "power": 25 }, + "max_charges": { "range": [ 1, 3 ], "power": 25 }, + "recharge_type": "periodic", + "time": [ "3 h", "6 h" ] + } + ], + "active_procgen_values": [ { "weight": 100, "spell_id": "AEA_PAIN" } ], "passive_add_procgen_values": [ { "weight": 100, @@ -36,6 +47,21 @@ The procedural generation of artifacts is defined in Json. The object looks like } ``` +### charge_types + +The various ways this artifact can charge and use charges. + +- **charges** the number of charges this artifact starts with - a random value between the two ones in 'range' are picked, and power is the power value. +- **charges_per_use** how many charges you spend with each use of this artifact - a random value between the two ones in 'range' are picked, and power is the power value. +- **max_charges** The maximum number of charges this artifact can have. - a random value between the two ones in 'range' are picked, and power is the power value. +- **recharge_type** How this artifact recharges +- **time** The amount of time this artifact takes to recharge - a random value between the two ones provided is picked. + +#### recharge_types + +- **none** This artifact does not recharge +- **periodic** This artifact takes 'time' amount of time to recharge + ### passive_add_procgen_values and passive_mult_procgen_values As the names suggest, these are *passive* benefits/penalties to having the artifact (ie. always present without activating the artifact's abilities). **Add** values add or subtract from existing scores, and **mult** values multiply them. These are entered as a list of possible 'abilities' the artifact could get. It does not by default get all these abilities, rather when it spawns it selects from the list provided. diff --git a/src/avatar.cpp b/src/avatar.cpp index e56df571fae8d..5c04491887610 100644 --- a/src/avatar.cpp +++ b/src/avatar.cpp @@ -1586,11 +1586,15 @@ bool avatar::wield( item &target, const int obtain_cost ) bool avatar::invoke_item( item *used, const tripoint &pt ) { const std::map &use_methods = used->type->use_methods; + const int num_methods = use_methods.size(); - if( use_methods.empty() ) { + const bool has_relic = used->is_relic(); + if( use_methods.empty() && !has_relic ) { return false; - } else if( use_methods.size() == 1 ) { + } else if( num_methods == 1 && !has_relic ) { return invoke_item( used, use_methods.begin()->first, pt ); + } else if( num_methods == 0 && has_relic ) { + return used->use_relic( *this, pt ); } uilist umenu; @@ -1603,6 +1607,10 @@ bool avatar::invoke_item( item *used, const tripoint &pt ) umenu.addentry_desc( MENU_AUTOASSIGN, res.success(), MENU_AUTOASSIGN, e.second.get_name(), res.str() ); } + if( has_relic ) { + umenu.addentry_desc( MENU_AUTOASSIGN, true, MENU_AUTOASSIGN, _( "Use relic" ), + _( "Activate this relic." ) ); + } umenu.desc_enabled = std::any_of( umenu.entries.begin(), umenu.entries.end(), []( const uilist_entry & elem ) { @@ -1612,7 +1620,11 @@ bool avatar::invoke_item( item *used, const tripoint &pt ) umenu.query(); int choice = umenu.ret; - if( choice < 0 || choice >= static_cast( use_methods.size() ) ) { + // Use the relic + if( choice == num_methods ) { + return used->use_relic( *this, pt ); + } + if( choice < 0 || choice >= num_methods ) { return false; } diff --git a/src/item.cpp b/src/item.cpp index 9bbda7db0ae31..efba86c3a1e76 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -4719,6 +4719,9 @@ std::string item::tname( unsigned int quantity, bool with_prefix, unsigned int t if( gunmod_find( itype_barrel_small ) ) { modtext += _( "sawn-off " ); } + if( is_relic() && relic_data->max_charges() > 0 && relic_data->charges_per_use() > 0 ) { + tagtext += string_format( " (%d/%d)", relic_data->charges(), relic_data->max_charges() ); + } if( has_flag( flag_DIAMOND ) ) { modtext += std::string( pgettext( "Adjective, as in diamond katana", "diamond" ) ) + " "; } @@ -9311,6 +9314,11 @@ void item::overwrite_relic( const relic &nrelic ) this->relic_data = cata::make_value( nrelic ); } +bool item::use_relic( Character &guy, const tripoint &pos ) +{ + return relic_data->activate( guy, pos ); +} + void item::process_relic( Character *carrier ) { if( !is_relic() ) { @@ -9328,6 +9336,8 @@ void item::process_relic( Character *carrier ) ench.activate_passive( *carrier ); } + relic_data->try_recharge(); + // Recalculate, as it might have changed (by mod_*_bonus above) carrier->str_cur = carrier->get_str(); carrier->int_cur = carrier->get_int(); diff --git a/src/item.h b/src/item.h index 5be2c16efa7d5..0e4c22cd43f22 100644 --- a/src/item.h +++ b/src/item.h @@ -1353,6 +1353,8 @@ class item : public visitable */ void on_damage( int qty, damage_type dt ); + bool use_relic( Character &guy, const tripoint &pos ); + /** * Name of the item type (not the item), with proper plural. * This is only special when the item itself has a special name ("name" entry in diff --git a/src/player.cpp b/src/player.cpp index 0092634e70580..3991682515b87 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -2879,6 +2879,8 @@ void player::use( item_location loc ) } else { add_msg( m_info, need_splint.str() ); } + } else if( used.is_relic() ) { + invoke_item( &used, loc.position() ); } else { add_msg( m_info, _( "You can't do anything interesting with your %s." ), used.tname() ); diff --git a/src/relic.cpp b/src/relic.cpp index d1490421996cf..7520af15ed7f3 100644 --- a/src/relic.cpp +++ b/src/relic.cpp @@ -30,6 +30,19 @@ namespace io abort(); } // *INDENT-ON* +template<> +std::string enum_to_string( relic_recharge type ) +{ + // *INDENT-OFF* + switch( type ) { + case relic_recharge::none: return "none"; + case relic_recharge::periodic: return "periodic"; + case relic_recharge::num: break; + } + // *INDENT-ON* + debugmsg( "Invalid relic recharge type" ); + abort(); +} } // namespace io namespace @@ -139,6 +152,24 @@ void relic_procgen_data::load( const JsonObject &jo, const std::string & ) item_weights.add( it, weight ); } + + for( const JsonObject &jo_inner : jo.get_array( "active_procgen_values" ) ) { + int weight = 0; + mandatory( jo_inner, was_loaded, "weight", weight ); + relic_procgen_data::enchantment_active val; + val.load( jo_inner ); + + active_procgen_values.add( val, weight ); + } + + for( const JsonObject &jo_inner : jo.get_array( "charge_types" ) ) { + int weight = 0; + mandatory( jo_inner, was_loaded, "weight", weight ); + relic_charge_template charge; + charge.load( jo_inner ); + + charge_values.add( charge, weight ); + } } void relic_procgen_data::generation_rules::load( const JsonObject &jo ) @@ -160,6 +191,73 @@ void relic_procgen_data::deserialize( JsonIn &jsin ) load( jobj ); } +void relic_charge_template::deserialize( JsonIn &jsin ) +{ + load( jsin.get_object() ); +} + +void relic_charge_template::load( const JsonObject &jo ) +{ + int tmp_power = 0; + + const JsonObject max_charge = jo.get_object( "max_charges" ); + max_charge.read( "range", max_charges ); + max_charge.read( "power", tmp_power ); + power_level += tmp_power; + + const JsonObject charge = jo.get_object( "charges" ); + charge.read( "range", init_charges ); + charge.read( "power", tmp_power ); + power_level += tmp_power; + + const JsonObject init_charge = jo.get_object( "charges_per_use" ); + init_charge.read( "range", charges_per_use ); + init_charge.read( "power", tmp_power ); + power_level += tmp_power; + + jo.read( "recharge_type", type ); + jo.read( "time", time ); +} + +relic_charge_info relic_charge_template::generate() const +{ + relic_charge_info ret; + ret.max_charges = rng( max_charges.first, max_charges.second ); + ret.charges_per_use = rng( charges_per_use.first, charges_per_use.second ); + ret.charges = rng( init_charges.first, init_charges.second ); + ret.activation_time = rng( time.first, time.second ); + ret.type = type; + ret.power = power_level; + return ret; +} + +void relic_charge_info::deserialize( JsonIn &jsin ) +{ + load( jsin.get_object() ); +} + +void relic_charge_info::load( const JsonObject &jo ) +{ + jo.read( "charges", charges ); + jo.read( "charges_per_use", charges_per_use ); + jo.read( "max_charges", max_charges ); + jo.read( "recharge_type", type ); + jo.read( "last_charge", last_charge ); + jo.read( "time", activation_time ); +} + +void relic_charge_info::serialize( JsonOut &jsout ) const +{ + jsout.start_object(); + jsout.member( "charges", charges ); + jsout.member( "charges_per_use", charges_per_use ); + jsout.member( "max_charges", max_charges ); + jsout.member( "recharge_type", type ); + jsout.member( "last_charge", last_charge ); + jsout.member( "time", activation_time ); + jsout.end_object(); +} + void relic::load( const JsonObject &jo ) { if( jo.has_array( "active_effects" ) ) { @@ -179,8 +277,11 @@ void relic::load( const JsonObject &jo ) add_passive_effect( ench ); } } + jo.read( "charge_info", charge ); + if( jo.has_member( "charges_per_activation" ) ) { + charge.charges_per_use = jo.get_int( "charges_per_activation", 1 ); + } jo.read( "name", item_name_override ); - charges_per_activation = jo.get_int( "charges_per_activation", 1 ); moves = jo.get_int( "moves", 100 ); } @@ -195,7 +296,6 @@ void relic::serialize( JsonOut &jsout ) const jsout.start_object(); jsout.member( "moves", moves ); - jsout.member( "charges_per_activation", charges_per_activation ); // item_name_override is not saved, in case the original json text changes: // in such case names read back from a save wouold no longer be properly translated. @@ -217,16 +317,81 @@ void relic::serialize( JsonOut &jsout ) const jsout.end_array(); } + jsout.member( "charge_info", charge ); + jsout.end_object(); } -int relic::activate( Creature &caster, const tripoint &target ) const +int relic::activate( Creature &caster, const tripoint &target ) { + if( charges() - charge.charges_per_use < 0 ) { + caster.add_msg_if_player( m_bad, _( "This artifact lacks the charges to activate." ) ); + return 0; + } caster.moves -= moves; for( const fake_spell &sp : active_effects ) { sp.get_spell( sp.level ).cast_all_effects( caster, target ); } - return charges_per_activation; + charge.charges -= charge.charges_per_use; + return charge.charges_per_use; +} + +int relic::charges() const +{ + return charge.charges; +} + +int relic::charges_per_use() const +{ + return charge.charges_per_use; +} + +int relic::max_charges() const +{ + return charge.max_charges; +} + +// Adds num charges to the relic, as long as it doesn't exceed max_charges +static void add_charges( relic_charge_info &rel, int num = 1 ) +{ + if( rel.charges + num <= rel.max_charges ) { + rel.charges += num; + } +} + +void relic::try_recharge() +{ + if( charge.charges == charge.max_charges ) { + return; + } + switch( charge.type ) { + case relic_recharge::none: { + return; + } + case relic_recharge::periodic: { + if( calendar::turn - charge.last_charge >= charge.activation_time ) { + add_charges( charge ); + break; + } + return; + } + case relic_recharge::num: { + debugmsg( "Attempted to recharge relic with invalid recharge type" ); + return; + } + } + + charge.last_charge = calendar::turn; +} + +relic_charge_info::relic_charge_info() +{ + last_charge = calendar::turn; +} + +void relic::overwrite_charge( const relic_charge_info &info ) +{ + charge = info; } int relic::modify_value( const enchant_vals::mod value_type, const int value ) const @@ -266,6 +431,7 @@ int relic::power_level( const relic_procgen_id &ruleset ) const for( const fake_spell &sp : active_effects ) { total_power_level += ruleset->power_level( sp ); } + total_power_level += charge.power; return total_power_level; } @@ -453,6 +619,10 @@ relic relic_procgen_data::generate( const relic_procgen_data::generation_rules & } } } + const relic_charge_template *charge = charge_values.pick(); + if( charge != nullptr ) { + ret.overwrite_charge( charge->generate() ); + } return ret; } diff --git a/src/relic.h b/src/relic.h index 1e0aba90dee15..61daca4080f38 100644 --- a/src/relic.h +++ b/src/relic.h @@ -15,6 +15,8 @@ class JsonIn; class JsonObject; class JsonOut; class relic; +struct relic_charge_info; +struct relic_charge_template; class relic_procgen_data; struct tripoint; @@ -98,6 +100,7 @@ class relic_procgen_data }; private: + weighted_int_list charge_values; weighted_int_list> passive_add_procgen_values; weighted_int_list> passive_mult_procgen_values; weighted_int_list passive_hit_you; @@ -123,6 +126,47 @@ class relic_procgen_data void deserialize( JsonIn &jsin ); }; +enum class relic_recharge : int { + none, + periodic, + num +}; + +struct relic_charge_template { + std::pair max_charges; + std::pair init_charges; + std::pair charges_per_use; + std::pair time; + relic_recharge type; + + int power_level = 0; + + void deserialize( JsonIn &jsin ); + void load( const JsonObject &jo ); + relic_charge_info generate() const; +}; + +struct relic_charge_info { + + int charges = 0; + int charges_per_use = 0; + int max_charges = 0; + relic_recharge type = relic_recharge::num; + + time_point last_charge; + time_duration activation_time = 0_seconds; + + relic_charge_info(); + + // Because multiple different charge types can overlap, cache the power + // level from the charge type we were generated from here to avoid confusion + int power = 0; + + void deserialize( JsonIn &jsin ); + void load( const JsonObject &jo ); + void serialize( JsonOut &jsout ) const; +}; + class relic { private: @@ -132,13 +176,18 @@ class relic // the item's name will be replaced with this if the string is not empty translation item_name_override; - int charges_per_activation = 0; + relic_charge_info charge; + // activating an artifact overrides all spell casting costs int moves = 0; public: std::string name() const; // returns number of charges that should be consumed - int activate( Creature &caster, const tripoint &target ) const; + int activate( Creature &caster, const tripoint &target ); + int charges() const; + int charges_per_use() const; + int max_charges() const; + void try_recharge(); void load( const JsonObject &jo ); @@ -151,6 +200,7 @@ class relic std::vector get_enchantments() const; int modify_value( enchant_vals::mod value_type, int value ) const; + void overwrite_charge( const relic_charge_info &info ); // what is the power level of this artifact, given a specific ruleset int power_level( const relic_procgen_id &ruleset ) const; @@ -163,4 +213,9 @@ struct enum_traits { static constexpr relic_procgen_data::type last = relic_procgen_data::type::last; }; +template<> +struct enum_traits { + static constexpr relic_recharge last = relic_recharge::num; +}; + #endif // CATA_SRC_RELIC_H