Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow more customizable XP costs for spell leveling #78583

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions data/mods/Magiclysm/eoc_spell_learn_boost.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
[
{
"type": "jmath_function",
"id": "mc_spell_exp_diff",
"num_args": 1,
"//": "accept the spell level, return a difference in experience between spell's current level and the next level",
"return": "spell_exp_for_level(_0 + 1) - spell_exp_for_level(_0)"
},
{
"type": "jmath_function",
"id": "mc_spell_train_focus_factor",
Expand Down Expand Up @@ -48,7 +41,7 @@
"math": [
"u_spell_exp(_spell)",
"+=",
"mc_spell_exp_diff(u_spell_level(_spell)) / mc_spell_cost_train_factor(_cost, 16, 65) * mc_spell_train_focus_factor(u_val('focus'),u_spell_level(_spell))"
"(spell_exp_for_level(_spell, u_spell_level(_spell)+1) - spell_exp_for_level(_spell, u_spell_level(_spell))) / mc_spell_cost_train_factor(_cost, 16, 65) * mc_spell_train_focus_factor(u_val('focus'),u_spell_level(_spell))"
]
}
]
Expand Down Expand Up @@ -107,7 +100,7 @@
"math": [
"u_spell_exp(_spell)",
"+=",
"mc_spell_exp_diff(u_spell_level(_spell)) / mc_spell_cost_train_factor(_cost, 20, 75) * mc_spell_train_focus_factor(u_val('focus'),u_spell_level(_spell))"
"(spell_exp_for_level(_spell, u_spell_level(_spell)+1) - spell_exp_for_level(_spell, u_spell_level(_spell))) / mc_spell_cost_train_factor(_cost, 20, 75) * mc_spell_train_focus_factor(u_val('focus'),u_spell_level(_spell))"
]
}
]
Expand All @@ -124,7 +117,7 @@
"math": [
"u_spell_exp(_spell)",
"+=",
"mc_spell_exp_diff(u_spell_level(_spell)) * (_cost / 5000) * mc_spell_train_focus_factor(u_val('focus'),u_spell_level(_spell))"
"(spell_exp_for_level(_spell, u_spell_level(_spell)+1) - spell_exp_for_level(_spell, u_spell_level(_spell))) * (_cost / 5000) * mc_spell_train_focus_factor(u_val('focus'),u_spell_level(_spell))"
]
}
]
Expand Down Expand Up @@ -165,14 +158,14 @@
"math": [
"u_spell_exp(_spell)",
"+=",
"mc_spell_exp_diff(u_spell_level(_spell)) / mc_spell_cost_train_factor(_cost, 16, 65) * mc_spell_train_focus_factor(u_val('focus'),u_spell_level(_spell))"
"(spell_exp_for_level(_spell, u_spell_level(_spell)+1) - spell_exp_for_level(_spell, u_spell_level(_spell))) / mc_spell_cost_train_factor(_cost, 16, 65) * mc_spell_train_focus_factor(u_val('focus'),u_spell_level(_spell))"
]
},
{
"math": [
"debug_mc_additional_spell_xp",
"=",
"mc_spell_exp_diff(u_spell_level(_spell)) / mc_spell_cost_train_factor(_cost, 16, 65) * mc_spell_train_focus_factor(u_val('focus'),u_spell_level(_spell))"
"(spell_exp_for_level(_spell, u_spell_level(_spell)+1) - spell_exp_for_level(_spell, u_spell_level(_spell))) / mc_spell_cost_train_factor(_cost, 16, 65) * mc_spell_train_focus_factor(u_val('focus'),u_spell_level(_spell))"
]
},
{ "u_message": "debug_mc_additional_spell_xp: <global_val:debug_mc_additional_spell_xp>", "type": "debug" }
Expand Down
4 changes: 2 additions & 2 deletions data/mods/Xedra_Evolved/eocs/chronomancer_eocs.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
"effect": [
{
"math": [
"u_spell_exp('xedra_chronomancer_rewrite_wound_causality') += (spell_exp_for_level(u_spell_level('xedra_chronomancer_rewrite_wound_causality')+1) - spell_exp_for_level(u_spell_level('xedra_chronomancer_rewrite_wound_causality'))) / 20 "
"u_spell_exp('xedra_chronomancer_rewrite_wound_causality') += (spell_exp_for_level('xedra_chronomancer_rewrite_wound_causality', u_spell_level('xedra_chronomancer_rewrite_wound_causality')+1) - spell_exp_for_level('xedra_chronomancer_rewrite_wound_causality', u_spell_level('xedra_chronomancer_rewrite_wound_causality'))) / 20 "
]
},
{
Expand Down Expand Up @@ -283,7 +283,7 @@
"effect": [
{
"math": [
"u_spell_exp('xedra_chronomancer_rewrite_equipment_causality') += (spell_exp_for_level(u_spell_level('xedra_chronomancer_rewrite_equipment_causality')+1) - spell_exp_for_level(u_spell_level('xedra_chronomancer_rewrite_equipment_causality'))) / 20 "
"u_spell_exp('xedra_chronomancer_rewrite_equipment_causality') += (spell_exp_for_level('xedra_chronomancer_rewrite_equipment_causality', u_spell_level('xedra_chronomancer_rewrite_equipment_causality')+1) - spell_exp_for_level('xedra_chronomancer_rewrite_equipment_causality', u_spell_level('xedra_chronomancer_rewrite_equipment_causality'))) / 20 "
]
},
{
Expand Down
20 changes: 17 additions & 3 deletions data/mods/Xedra_Evolved/eocs/spell_learning_eoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,29 @@
]
},
"effect": [
{ "math": [ "debug_xe_spell_exp_diff = (spell_exp_diff(u_spell_level(_spell)))" ] },
{
"math": [
"debug_xe_spell_exp_diff = spell_exp_for_level(_spell, u_spell_level(_spell)+1) - spell_exp_for_level(_spell, u_spell_level(_spell))"
]
},
{ "math": [ "debug_xe_spell_train_factor = (spell_train_factor(_cost))" ] },
{ "math": [ "debug_xe_spell_xp_give = spell_exp_diff(u_spell_level(_spell))/spell_train_factor(_cost)" ] },
{
"math": [
"debug_xe_spell_xp_give = (spell_exp_for_level(_spell, u_spell_level(_spell)+1) - spell_exp_for_level(_spell, u_spell_level(_spell)))/spell_train_factor(_cost)"
]
},
{ "u_message": "school: <context_val:school>", "type": "debug" },
{ "u_message": "spell: <context_val:spell>", "type": "debug" },
{ "u_message": "spell_exp_diff: <global_val:debug_xe_spell_exp_diff>", "type": "debug" },
{ "u_message": "spell_train_factor: <global_val:debug_xe_spell_train_factor>", "type": "debug" },
{ "u_message": "amount of XP added: <global_val:debug_xe_spell_xp_give>", "type": "debug" },
{ "math": [ "u_spell_exp(_spell)", "+=", "spell_exp_diff(u_spell_level(_spell))/spell_train_factor(_cost)" ] }
{
"math": [
"u_spell_exp(_spell)",
"+=",
"(spell_exp_for_level(_spell, u_spell_level(_spell)+1) - spell_exp_for_level(_spell, u_spell_level(_spell)))/spell_train_factor(_cost)"
]
}
]
},
{
Expand Down
7 changes: 0 additions & 7 deletions data/mods/Xedra_Evolved/jmath.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@
"num_args": 3,
"return": " ((_0 * _1) + _2) * scaling_factor(u_val('intelligence'))"
},
{
"type": "jmath_function",
"id": "spell_exp_diff",
"num_args": 1,
"//": "accept the spell level, return a difference in experience between spell's current level and the next level",
"return": "spell_exp_for_level(_0 + 1) - spell_exp_for_level(_0)"
},
{
"type": "jmath_function",
"id": "spell_time",
Expand Down
18 changes: 13 additions & 5 deletions data/mods/Xedra_Evolved/spells/spell_eocs.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"math": [
"u_spell_exp('xedra_dreamer_generate_accelerated_time')",
"+=",
"spell_exp_diff(u_spell_level('xedra_dreamer_generate_accelerated_time'))/spell_train_factor(200)"
"(spell_exp_for_level('xedra_dreamer_generate_accelerated_time', u_spell_level('xedra_dreamer_generate_accelerated_time')+1) - spell_exp_for_level('xedra_dreamer_generate_accelerated_time', u_spell_level('xedra_dreamer_generate_accelerated_time')))/spell_train_factor(200)"
]
}
],
Expand Down Expand Up @@ -143,7 +143,7 @@
"math": [
"u_spell_exp('spell_ethereal_wings')",
"+=",
"spell_exp_diff(u_spell_level('spell_ethereal_wings'))/spell_train_factor(200)"
"(spell_exp_for_level('spell_ethereal_wings', u_spell_level('spell_ethereal_wings')+1) - spell_exp_for_level('spell_ethereal_wings', u_spell_level('spell_ethereal_wings')))/spell_train_factor(200)"
]
}
],
Expand Down Expand Up @@ -184,7 +184,11 @@
"time_in_future": { "math": [ "((u_spell_level('spell_karma_arms')+1)*500)" ] }
},
{
"math": [ "u_spell_exp('spell_karma_arms')", "+=", "spell_exp_diff(u_spell_level('spell_karma_arms'))/spell_train_factor(200)" ]
"math": [
"u_spell_exp('spell_karma_arms')",
"+=",
"(spell_exp_for_level('spell_karma_arms', u_spell_level('spell_karma_arms')+1) - spell_exp_for_level('spell_karma_arms', u_spell_level('spell_karma_arms')))/spell_train_factor(200)"
]
}
],
"false_effect": [
Expand Down Expand Up @@ -218,7 +222,11 @@
"time_in_future": { "math": [ "((u_spell_level('spell_devil_tail')+1)*900)" ] }
},
{
"math": [ "u_spell_exp('spell_devil_tail')", "+=", "spell_exp_diff(u_spell_level('spell_devil_tail'))/spell_train_factor(100)" ]
"math": [
"u_spell_exp('spell_devil_tail')",
"+=",
"(spell_exp_for_level('spell_devil_tail', u_spell_level('spell_devil_tail')+1) - spell_exp_for_level('spell_devil_tail', u_spell_level('spell_devil_tail')))/spell_train_factor(100)"
]
}
],
"false_effect": [ { "u_lose_trait": "devil_tail" }, { "u_message": "Your devil tail fades away.", "type": "neutral" } ]
Expand Down Expand Up @@ -258,7 +266,7 @@
"math": [
"u_spell_exp('spell_stalker_eyes')",
"+=",
"spell_exp_diff(u_spell_level('spell_stalker_eyes'))/spell_train_factor(200)"
"(spell_exp_for_level('spell_stalker_eyes', u_spell_level('spell_stalker_eyes')+1) - spell_exp_for_level('spell_stalker_eyes', u_spell_level('spell_stalker_eyes')))/spell_train_factor(200)"
]
}
],
Expand Down
38 changes: 38 additions & 0 deletions doc/MAGIC.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ In `data/mods/Magiclysm` there is a template spell, copied here for your perusal
"components": [requirement_id] // an id from a requirement, like the ones you use for crafting. spell components require to cast.
"difficulty": 12, // the difficulty to learn/cast the spell
"max_level": 10, // maximum level you can achieve in the spell
"get_level_formula_id": "jmath_get_level_formula", // The id of a jmath formula that calculates what level the spell is for a given exp value. Must be the inverse of exp_for_level_formula_id.
"exp_for_level_formula_id": "jmath_exp_for_level_formula",// The id of a jmath formula that calculates how much exp is required for a given level. Must be the inverse of get_level_formula_id.
"min_accuracy" -20, // the accuracy bonus of the spell. around -15 and it gets blocked all the time
"max_accuracy": 20, // around 20 accuracy and it's basically impossible to block
"accuracy_increment": 1.5
Expand Down Expand Up @@ -615,6 +617,42 @@ Explanation: Here we have one main spell with two subspells: one on the caster a

See [Monster special attacks - Spells](MONSTER_SPECIAL_ATTACKS.md#spell-monster-spells).

### Custom Spell Experience Requirements

Spells may have custom formulas for their xp requirements to level by combining the `get_level_formula_id` and `exp_for_level_formula_id` fields.
These fields both take the id of a jmath_function that they can use to calculate the xp requirement instead of the default formula.
The given jmath functions must only have 1 argument (for the spell level or experience) and they should not make any calls to the alpha or beta talkers. Spell calculations are intended to be character agnostic, and while using functions such as u_skill('DEDUCTION') will not error, it will always be calculated from the player characters data, even if for an npc or other uses of spell xp calculations.
Also, keep in mind that changing the spell experience requirements of an existing spell will likely do strange things to any characters that already knew the spell before the xp requirements were changed.

Note: the exp_for_level_formula_id requires the total experience required for a spell level, not the difference in experience between the current and next level. IE, if a spell requires 1000 xp to level a level 10 spell should require 10,000 experience in its exp_for_level_formula_id, not 1,000.

Constant Spell Exp Requirement Example:
{
"id": "test_spell",
"type": "SPELL",
"name": "test",
"description": "constant spell experience formula.",
"valid_targets": [ "hostile" ],
"effect": "attack",
"min_damage": 1,
"max_damage": 1,
"get_level_formula_id": "magic_test_func_get_level",
"exp_for_level_formula_id": "magic_test_func_exp_for_level"
},
{
"type": "jmath_function",
"id": "magic_test_func_get_level",
"num_args": 1,
"return": "_0 / 1000"
},
{
"type": "jmath_function",
"id": "magic_test_func_exp_for_level",
"num_args": 1,
"return": "1000 * _0"
},
```


## Enchantments

Expand Down
2 changes: 1 addition & 1 deletion doc/NPCs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,7 @@ _some functions support array arguments or kwargs, denoted with square brackets
| skill(`s`/`v`) | ✅ | ✅ | u, n | Return or set skill level<br/><br/>Example:<br/>`"condition": { "math": [ "u_skill('driving') >= 5"] }`<br/>`"condition": { "math": [ "u_skill(someskill) >= 5"] }`|
| skill_exp(`s`/`v`) | ✅ | ✅ | u, n | Return or set skill experience<br/> Argument is skill ID. <br/><br/> Optional kwargs:<br/>`format`: `s`/`v` - must be `percentage` or `raw`.<br/><br/>Example:<br/>`{ "math": [ "u_skill_exp('driving', 'format': crt_format)--"] }`|
| spell_exp(`s`/`v`) | ✅ | ✅ | u, n | Return or set spell xp<br/> Example:<br/>`"condition": { "math": [ "u_spell_exp('SPELL_ID') >= 5"] }`|
| spell_exp_for_level(`d`/`v`) | ✅ | ❌ | g | Return the amount of XP necessary for a spell level. <br/> Example:<br/>`"math": [ "spell_exp_for_level(u_spell_level('SPELL_ID')) * 5"] }`|
| spell_exp_for_level(`s`/`v`, `d`/`v`) | ✅ | ❌ | g | Return the amount of XP necessary for a spell level for the given spell. Arguments are spell id and desired level. <br/> Example:<br/>`"math": [ "spell_exp_for_level('SPELL_ID', u_spell_level('SPELL_ID') ) * 5"] }`|
| spell_count() | ✅ | ❌ | u, n | Return number of spells the character knows.<br/><br/> Optional kwargs:<br/>`school`: `s/v` - return number of spells known of that school.<br/><br/> Example:<br/>`"condition": { "math": [ "u_spell_count('school': 'MAGUS') >= 10"] }`|
| spell_level_sum() | ✅ | ❌ | u, n | Return sum of all spell levels character has; having one spell of class A with level 5, and another with lvl 10 would return 15. <br/><br/> Optional kwargs:<br/>`school`: `s/v` - return number of spells known of that school. Omitting return sum of all spells character has, no matter of the class.<br/>`level`: `d/v` - count only spells that are higher or equal this field. Default 0.<br/><br/> Example:<br/>`{ "math": [ "test_var1 = u_spell_level_sum()" ] }`<br/>`{ "math": [ "test_var2 = u_spell_level_sum('school': 'MAGUS')" ] }`<br/>`{ "math": [ "test_var3 = u_spell_level_sum('school': 'MAGUS', 'level': '10')" ] }`|
| spell_level(`s`/`v`) | ✅ | ✅ | u, n | Return or set level of a given spell. -1 means the spell is not known when read and that the spell should be forgotten if written.<br/>Argument is spell ID. If `"null"` is given, return the highest level of spells the character knows (read only).<br/> Example:<br/>`"condition": { "math": [ "u_spell_level('SPELL_ID') == -1"] }`|
Expand Down
4 changes: 3 additions & 1 deletion src/debug_menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1387,7 +1387,9 @@ static void change_spells( Character &character )
character.magic->get_spellbook().emplace( splt.id, spl );
}

character.magic->get_spell( splt.id ).set_exp( spell::exp_for_level( spell_level ) );
// storing the spell to be used instead of getting it twice somehow breaks the debug functionality.
int set_to_exp = character.magic->get_spell( splt.id ).exp_for_level( spell_level );
character.magic->get_spell( splt.id ).set_exp( set_to_exp );
};

ui_adaptor spellsui;
Expand Down
41 changes: 37 additions & 4 deletions src/magic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include "magic_enchantment.h"
#include "map.h"
#include "map_iterator.h"
#include "math_parser_jmath.h"
#include "messages.h"
#include "mongroup.h"
#include "monster.h"
Expand Down Expand Up @@ -486,6 +487,13 @@ void spell_type::load( const JsonObject &jo, const std::string_view src )
optional( jo, was_loaded, "spell_class", spell_class, spell_class_default );
optional( jo, was_loaded, "energy_source", energy_source, energy_source_default );
optional( jo, was_loaded, "damage_type", dmg_type, dmg_type_default );
optional( jo, was_loaded, "get_level_formula_id", get_level_formula_id );
optional( jo, was_loaded, "exp_for_level_formula_id", exp_for_level_formula_id );
if( ( get_level_formula_id.has_value() && !exp_for_level_formula_id.has_value() ) ||
( !get_level_formula_id.has_value() && exp_for_level_formula_id.has_value() ) ) {
debugmsg( "spell id:%s has a get_level_formula_id or exp_for_level_formula_id but not the other! This breaks the calculations for xp/level!",
id.c_str() );
}
if( !was_loaded || jo.has_member( "difficulty" ) ) {
difficulty = get_dbl_or_var( jo, "difficulty", false, difficulty_default );
}
Expand Down Expand Up @@ -624,6 +632,8 @@ void spell_type::serialize( JsonOut &json ) const
static_cast<int>( base_casting_time.min.dbl_val.value() ) );
json.member( "casting_time_increment",
static_cast<float>( casting_time_increment.min.dbl_val.value() ), casting_time_increment_default );
json.member( "get_level_formula_id", get_level_formula_id );
json.member( "exp_for_level_formula_id", exp_for_level_formula_id );

if( !learn_spells.empty() ) {
json.member( "learn_spells" );
Expand Down Expand Up @@ -675,6 +685,13 @@ void spell_type::check_consistency()
if( sp_t.spell_tags[spell_flag::WONDER] && sp_t.additional_spells.empty() ) {
debugmsg( "ERROR: %s has WONDER flag but no spells to choose from!", sp_t.id.c_str() );
}
if( sp_t.exp_for_level_formula_id.has_value() &&
sp_t.exp_for_level_formula_id.value()->num_params != 1 ) {
debugmsg( "ERROR: %s exp_for_level_formula_id has params that != 1!", sp_t.id.c_str() );
}
if( sp_t.get_level_formula_id.has_value() && sp_t.get_level_formula_id.value()->num_params != 1 ) {
debugmsg( "ERROR: %s get_level_formula_id has params that != 1!", sp_t.id.c_str() );
}
}
}

Expand Down Expand Up @@ -1651,9 +1668,6 @@ bool spell::ignore_by_species_id( const tripoint_bub_ms &p ) const
return valid;
}




std::string spell::description() const
{
return type->description.translated();
Expand Down Expand Up @@ -1682,8 +1696,18 @@ static constexpr double b = 0.146661;
static constexpr double c = -62.5;

int spell::get_level() const
{
return type->get_level( experience );
}

int spell_type::get_level( int experience ) const
{
// you aren't at the next level unless you have the requisite xp, so floor
if( get_level_formula_id.has_value() ) {
return std::max( static_cast<int>( std::floor( get_level_formula_id.value()->eval( dialogue(
std::make_unique<talker>(), nullptr ), { static_cast<double>( experience ) } ) ) ), 0 );
}

return std::max( static_cast<int>( std::floor( std::log( experience + a ) / b + c ) ), 0 );
}

Expand Down Expand Up @@ -1755,12 +1779,21 @@ void spell::clear_temp_adjustments()
// helper function to calculate xp needed to be at a certain level
// pulled out as a helper function to make it easier to either be used in the future
// or easier to tweak the formula
int spell::exp_for_level( int level )
int spell::exp_for_level( int level ) const
{
return type->exp_for_level( level );
}

int spell_type::exp_for_level( int level ) const
{
// level 0 never needs xp
if( level == 0 ) {
return 0;
}
if( exp_for_level_formula_id.has_value() ) {
return std::ceil( exp_for_level_formula_id.value()->eval( dialogue( std::make_unique<talker>(),
nullptr ), { static_cast<double>( level ) } ) );
}
return std::ceil( std::exp( ( level - c ) * b ) ) - a;
}

Expand Down
Loading
Loading