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

Magicylsm: NPCs can now teach spells #34709

Merged
merged 25 commits into from Oct 20, 2019
Merged
Show file tree
Hide file tree
Changes from 14 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
15 changes: 15 additions & 0 deletions data/mods/Magiclysm/npc/classes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
{
"type": "npc_class",
"id": "NC_MAGE_TEST",
"name": "Mage Test",
"job_description": "I'm a wandering debug tester for the arcane arts.",
"traits": [
{ "trait": "STORMSHAPER" },
{ "group": "BG_survival_story_EVACUEE" },
{ "group": "NPC_starting_traits" },
{ "group": "Appearance_demographics" }
],
"spells": [ { "id": "lightning_bolt", "level": 6 }, { "id": "windrun", "level": 6 } ]
}
]
9 changes: 4 additions & 5 deletions doc/MAGIC.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ In `data/mods/Magiclysm` there is a template spell, copied here for your perusal
"effect_str": "template" // special. see below
"extra_effects": [ { "id": "fireball", "hit_self": false, "max_level": 3 } ], // this allows you to cast multiple spells with only one spell
"effected_body_parts": [ "HEAD", "TORSO", "MOUTH", "EYES", "ARM_L", "ARM_R", "HAND_R", "HAND_L", "LEG_L", "FOOT_L", "FOOT_R" ], // body parts affected by effects
"spell_class": "NONE" //
"spell_class": "NONE" //
"base_casting_time": 100, // this is the casting time (in moves)
"base_energy_cost": 10, // the amount of energy (of the requisite type) to cast the spell
"energy_source": "MANA", // the type of energy used to cast the spell. types are: MANA, BIONIC, HP, STAMINA, FATIGUE, NONE (none will not use mana)
Expand Down Expand Up @@ -56,7 +56,7 @@ In `data/mods/Magiclysm` there is a template spell, copied here for your perusal
Most of the default values for the above are either 0 or "NONE", so you may leave out most of the values if they do not pertain to your spell.

When deciding values for some of these, it is important to note that some of the formulae are not linear.
For example, this is the formula for spell failure chance:
For example, this is the formula for spell failure chance:

```( ( ( ( spell_level - spell_difficulty ) * 2 + intelligence + spellcraft_skill ) - 30 ) / 30 ) ^ 2```

Expand Down Expand Up @@ -180,9 +180,9 @@ Currently there is only one way of learning spells that is implemented: learning
You can study this spellbook for a rate of ~1 experience per turn depending on intelligence, spellcraft, and focus.


#### Spells in professions
#### Spells in professions and NPC classes

You can add a "spell" member to professions like so:
You can add a "spell" member to professions or an NPC class definition like so:
```json
"spells": [ { "id": "summon_zombie", "level": 0 }, { "id": "magic_missile", "level": 10 } ]
```
Expand All @@ -198,4 +198,3 @@ You can assign a spell as a special attack for a monster.
* spell_id: the id for the spell being cast.
* spell_level: the level at which the spell is cast. Spells cast by monsters do not gain levels like player spells.
* cooldown: how often the monster can cast this spell

21 changes: 20 additions & 1 deletion src/activity_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1956,8 +1956,27 @@ void activity_handlers::train_finish( player_activity *act, player *p )
add_msg( m_good, _( "You learn %s." ), mastyle.name );
g->events().send<event_type::learns_martial_art>( p->getID(), ma_id );
p->add_martialart( mastyle.id );
}
const spell_id &sp_id = spell_id( act->name );
if( sp_id.is_valid() ) {
This conversation was marked as resolved.
Show resolved Hide resolved
const bool knows = g->u.magic.knows_spell( sp_id );
if( knows ) {
spell &studying = p->magic.get_spell( sp_id );
const int expert_multiplier = act->values.empty() ? 0 : act->values[0];
const int xp = roll_remainder( studying.exp_modifier( *p ) * expert_multiplier );
studying.gain_exp( xp );
} else {
p->magic.learn_spell( act->name, *p );
// you can decline to learn this spell , as it may lock you out of other magic.
if( p->magic.knows_spell( sp_id ) ) {
add_msg( m_good, _( "You learn %s." ), sp_id->name.translated() );
} else {
act->set_to_null();
return;
}
}
} else {
debugmsg( "train_finish without a valid skill or style name" );
debugmsg( "train_finish without a valid skill or style or spell name" );
}

act->set_to_null();
Expand Down
2 changes: 2 additions & 0 deletions src/character.h
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,8 @@ class Character : public Creature, public visitable<Character>
time_died = time;
}
}
// magic mod
known_magic magic;

void make_bleed( body_part bp, time_duration duration, int intensity = 1,
bool permanent = false,
Expand Down
23 changes: 23 additions & 0 deletions src/debug_menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ enum debug_menu_index {
DEBUG_MAP_EXTRA,
DEBUG_DISPLAY_NPC_PATH,
DEBUG_PRINT_FACTION_INFO,
DEBUG_PRINT_NPC_MAGIC,
DEBUG_QUIT_NOSAVE,
DEBUG_TEST_WEATHER,
DEBUG_SAVE_SCREENSHOT,
Expand Down Expand Up @@ -210,6 +211,7 @@ static int info_uilist( bool display_all_entries = true )
{ uilist_entry( DEBUG_CRASH_GAME, true, 'C', _( "Crash game (test crash handling)" ) ) },
{ uilist_entry( DEBUG_DISPLAY_NPC_PATH, true, 'n', _( "Toggle NPC pathfinding on map" ) ) },
{ uilist_entry( DEBUG_PRINT_FACTION_INFO, true, 'f', _( "Print faction info to console" ) ) },
{ uilist_entry( DEBUG_PRINT_NPC_MAGIC, true, 'M', _( "Print NPC magic info to console" ) ) },
{ uilist_entry( DEBUG_TEST_WEATHER, true, 'W', _( "Test weather" ) ) },
};
uilist_initializer.insert( uilist_initializer.begin(), debug_only_options.begin(),
Expand Down Expand Up @@ -1492,6 +1494,27 @@ void debug()
std::cout << "Player faction is " << g->u.get_faction()->id.str() << std::endl;
break;
}
case DEBUG_PRINT_NPC_MAGIC: {
for( npc &guy : g->all_npcs() ) {
const std::vector<spell_id> spells = guy.magic.spells();
if( spells.empty() ) {
std::cout << guy.disp_name() << " does not know any spells." << std::endl;
continue;
}
std::cout << guy.disp_name() << "knows : ";
int counter = 1;
for( const spell_id sp : spells ) {
std::cout << sp->name.translated() << " ";
if( counter < static_cast<int>( spells.size() ) ) {
std::cout << "and ";
} else {
std::cout << "." << std::endl;
}
counter++;
}
}
break;
}
case DEBUG_QUIT_NOSAVE:
if( query_yn(
_( "Quit without saving? This may cause issues such as duplicated or missing items and vehicles!" ) ) ) {
Expand Down
7 changes: 7 additions & 0 deletions src/dialogue.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ struct talk_response {
mission *mission_selected = nullptr;
skill_id skill = skill_id::NULL_ID();
matype_id style = matype_id::NULL_ID();
spell_id dialogue_spell = spell_id();

talk_effect_t success;
talk_effect_t failure;
Expand Down Expand Up @@ -301,6 +302,12 @@ struct dialogue {
*/
talk_response &add_response( const std::string &text, const std::string &r, const skill_id &skill,
bool first = false );
/**
* Add a simple response that switches the topic to the new one and sets the currently
* talked about magic spell to the given one.
*/
talk_response &add_response( const std::string &text, const std::string &r,
const spell_id &sp, bool first = false );
/**
* Add a simple response that switches the topic to the new one and sets the currently
* talked about martial art style to the given one.
Expand Down
8 changes: 8 additions & 0 deletions src/npc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,14 @@ void npc::randomize( const npc_class_id &type )
add_bionic( bl.first );
}
}
// Add spells for magiclysm mod
for( std::pair<spell_id, int> spell_pair : type->_starting_spells ) {
this->magic.learn_spell( spell_pair.first, *this, true );
spell &sp = this->magic.get_spell( spell_pair.first );
while( sp.get_level() < spell_pair.second && !sp.is_max_level() ) {
sp.gain_level();
}
}
}

void npc::randomize_from_faction( faction *fac )
Expand Down
6 changes: 6 additions & 0 deletions src/npc.h
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,10 @@ struct npc_chatbin {
* The martial art style this NPC offers to train.
*/
matype_id style;
/**
* The spell this NPC offers to train
*/
spell_id dialogue_spell;
std::string first_topic = "TALK_NONE";

npc_chatbin() = default;
Expand Down Expand Up @@ -1008,6 +1012,8 @@ class npc : public player
// Finds something to complain about and complains. Returns if complained.
bool complain();

int calc_spell_training_cost( const bool knows, int difficulty, int level );

void handle_sound( int priority, const std::string &description, int heard_volume,
const tripoint &spos );

Expand Down
9 changes: 9 additions & 0 deletions src/npc_class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,15 @@ void npc_class::load( JsonObject &jo, const std::string & )
traits = trait_group::load_trait_group( *jo.get_raw( "traits" ), "collection" );
}

if( jo.has_array( "spells" ) ) {
JsonArray array = jo.get_array( "spells" );
while( array.has_more() ) {
JsonObject subobj = array.next_object();
const int level = subobj.get_int( "level" );
const spell_id sp = spell_id( subobj.get_string( "id" ) );
_starting_spells.emplace( sp, level );
}
}
/* Mutation rounds can be specified as follows:
* "mutation_rounds": {
* "ANY" : { "constant": 1 },
Expand Down
2 changes: 2 additions & 0 deletions src/npc_class.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ class npc_class

std::map<Mutation_category_tag, distribution> mutation_rounds;
trait_group::Trait_group_tag traits = trait_group::Trait_group_tag( "EMPTY_GROUP" );
// the int is what level the spell starts at
std::map<spell_id, int> _starting_spells;
std::map<bionic_id, int> bionic_list;
npc_class();

Expand Down
87 changes: 79 additions & 8 deletions src/npctalk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,18 @@ int calc_ma_style_training_cost( const npc &p, const matype_id & /* id */ )
return 800;
}

int npc::calc_spell_training_cost( const bool knows, int difficulty, int level )
{
if( is_player_ally() ) {
return 0;
}
int ret = ( 100 * std::max( 1, difficulty ) * std::max( 1, level ) );
if( !knows ) {
ret = ret * 2;
}
return ret;
}

// Rescale values from "mission scale" to "opinion scale"
int npc_trading::cash_to_favor( const npc &, int cash )
{
Expand Down Expand Up @@ -897,7 +909,14 @@ std::string dialogue::dynamic_line( const talk_topic &the_topic ) const
}
std::vector<skill_id> trainable = p->skills_offered_to( g->u );
std::vector<matype_id> styles = p->styles_offered_to( g->u );
if( trainable.empty() && styles.empty() ) {
const std::vector<spell_id> spells = p->magic.spells();
std::vector<spell_id> teachable_spells;
for( const spell_id &sp : spells ) {
if( g->u.magic.can_learn_spell( g->u, sp ) ) {
teachable_spells.push_back( sp );
This conversation was marked as resolved.
Show resolved Hide resolved
}
}
if( trainable.empty() && styles.empty() && teachable_spells.empty() ) {
return _( "Sorry, but it doesn't seem I have anything to teach you." );
} else {
return _( "Here's what I can teach you..." );
Expand Down Expand Up @@ -1118,6 +1137,14 @@ talk_response &dialogue::add_response( const std::string &text, const std::strin
return result;
}

talk_response &dialogue::add_response( const std::string &text, const std::string &r,
const spell_id &sp, const bool first )
{
talk_response &result = add_response( text, r, first );
result.dialogue_spell = sp;
return result;
}

talk_response &dialogue::add_response( const std::string &text, const std::string &r,
const martialart &style, const bool first )
{
Expand Down Expand Up @@ -1170,28 +1197,67 @@ void dialogue::gen_responses( const talk_topic &the_topic )
}
}
} else if( topic == "TALK_TRAIN" ) {
if( !g->u.backlog.empty() && g->u.backlog.front().id() == activity_id( "ACT_TRAIN" ) ) {
if( !g->u.backlog.empty() && g->u.backlog.front().id() == activity_id( "ACT_TRAIN" ) &&
g->u.backlog.front().index == p->getID().get_value() ) {
player_activity &backlog = g->u.backlog.front();
std::stringstream resume;
resume << _( "Yes, let's resume training " );
const skill_id skillt( backlog.name );
// TODO: This is potentially dangerous. A skill and a martial art
// could have the same ident!
if( !skillt.is_valid() ) {
auto &style = matype_id( backlog.name ).obj();
resume << style.name;
add_response( resume.str(), "TALK_TRAIN_START", style );
const matype_id styleid = matype_id( backlog.name );
if( !styleid.is_valid() ) {
const spell_id &sp_id = spell_id( backlog.name );
if( p->magic.knows_spell( sp_id ) ) {
resume << sp_id->name.translated();
add_response( resume.str(), "TALK_TRAIN_START", sp_id );
}
} else {
martialart style = styleid.obj();
resume << style.name;
add_response( resume.str(), "TALK_TRAIN_START", style );
}
} else {
resume << skillt.obj().name();
add_response( resume.str(), "TALK_TRAIN_START", skillt );
}
}
std::vector<matype_id> styles = p->styles_offered_to( g->u );
std::vector<skill_id> trainable = p->skills_offered_to( g->u );
if( trainable.empty() && styles.empty() ) {
const std::vector<spell_id> spells = p->magic.spells();
std::vector<spell_id> teachable_spells;
for( const spell_id &sp : spells ) {
const spell &temp_spell = p->magic.get_spell( sp );
if( g->u.magic.can_learn_spell( g->u, sp ) ) {
if( g->u.magic.knows_spell( sp ) ) {
const spell &player_spell = g->u.magic.get_spell( sp );
if( player_spell.is_max_level() || player_spell.get_level() >= temp_spell.get_level() ) {
continue;
}
}
teachable_spells.push_back( sp );
}
}
if( trainable.empty() && styles.empty() && teachable_spells.empty() ) {
add_response_none( _( "Oh, okay." ) );
return;
}
for( const spell_id &sp : teachable_spells ) {
const spell &temp_spell = p->magic.get_spell( sp );
const bool knows = g->u.magic.knows_spell( sp );
const int cost = p->calc_spell_training_cost( knows, temp_spell.get_difficulty(),
temp_spell.get_level() );
std::string text;
if( knows ) {
text = string_format( _( "%s: variable exp gain (cost %s)" ), temp_spell.name(),
This conversation was marked as resolved.
Show resolved Hide resolved
format_money( cost ) );
} else {
text = string_format( _( "%s: teaching spell knowledge (cost %s)" ), temp_spell.name(),
format_money( cost ) );
}
add_response( text, "TALK_TRAIN_START", sp );
}
for( auto &style_id : styles ) {
auto &style = style_id.obj();
const int cost = calc_ma_style_training_cost( *p, style.id );
Expand Down Expand Up @@ -1685,14 +1751,18 @@ talk_topic dialogue::opt( dialogue_window &d_win, const talk_topic &topic )
if( chosen.skill ) {
beta->chatbin.skill = chosen.skill;
beta->chatbin.style = matype_id::NULL_ID();
beta->chatbin.dialogue_spell = spell_id();
} else if( chosen.style ) {
beta->chatbin.style = chosen.style;
beta->chatbin.skill = skill_id::NULL_ID();
beta->chatbin.dialogue_spell = spell_id();
} else if( chosen.dialogue_spell != spell_id() ) {
beta->chatbin.style = matype_id::NULL_ID();
beta->chatbin.skill = skill_id::NULL_ID();
beta->chatbin.dialogue_spell = chosen.dialogue_spell;
}

const bool success = chosen.trial.roll( *this );
const auto &effects = success ? chosen.success : chosen.failure;

return effects.apply( *this );
}

Expand Down Expand Up @@ -2637,6 +2707,7 @@ talk_response::talk_response()
mission_selected = nullptr;
skill = skill_id::NULL_ID();
style = matype_id::NULL_ID();
dialogue_spell = spell_id();
}

talk_response::talk_response( JsonObject jo )
Expand Down
Loading