/*
* File: notes.cc
* Summary: Notetaking stuff
* Written by: Haran Pilpel
*/
#include "AppHdr.h"
#include <vector>
#include <sstream>
#include <iomanip>
#include "notes.h"
#include "branch.h"
#include "cio.h"
#include "describe.h"
#include "files.h"
#include "kills.h"
#include "hiscores.h"
#include "mutation.h"
#include "options.h"
#include "place.h"
#include "religion.h"
#include "skills2.h"
#include "spl-util.h"
#include "tags.h"
#define NOTES_VERSION_NUMBER 1002
std::vector<Note> note_list;
// return the real number of the power (casting out nonexistent powers),
// starting from 0, or -1 if the power doesn't exist
static int _real_god_power( int religion, int idx )
{
if (god_gain_power_messages[religion][idx][0] == 0)
return -1;
int count = 0;
for (int j = 0; j < idx; ++j)
if (god_gain_power_messages[religion][j][0])
++count;
return count;
}
static bool _is_noteworthy_skill_level( int level )
{
for (unsigned int i = 0; i < Options.note_skill_levels.size(); ++i)
if (level == Options.note_skill_levels[i])
return (true);
return (false);
}
static bool _is_highest_skill( int skill )
{
for (int i = 0; i < NUM_SKILLS; ++i)
{
if (i == skill)
continue;
if (you.skills[i] >= you.skills[skill])
return (false);
}
return (true);
}
static bool _is_noteworthy_hp( int hp, int maxhp )
{
return (hp > 0 && Options.note_hp_percent
&& hp <= (maxhp * Options.note_hp_percent) / 100);
}
static int _dungeon_branch_depth( unsigned char branch )
{
if (branch >= NUM_BRANCHES)
return -1;
return branches[branch].depth;
}
static bool _is_noteworthy_dlevel( unsigned short place )
{
const unsigned char branch =
static_cast<unsigned char>((place >> 8) & 0xFF);
const int lev = (place & 0xFF);
// Special levels (Abyss, etc.) are always interesting.
if (lev == 0xFF)
return (true);
if (lev == _dungeon_branch_depth(branch)
|| branch == BRANCH_MAIN_DUNGEON && (lev % 5) == 0
|| branch != BRANCH_MAIN_DUNGEON && lev == 1)
{
return (true);
}
return (false);
}
// Is a note worth taking?
// This function assumes that game state has not changed since
// the note was taken, e.g. you.* is valid.
static bool _is_noteworthy( const Note& note )
{
// Always noteworthy.
if (note.type == NOTE_XP_LEVEL_CHANGE
|| note.type == NOTE_GET_GOD
|| note.type == NOTE_GOD_GIFT
|| note.type == NOTE_GET_MUTATION
|| note.type == NOTE_LOSE_MUTATION
|| note.type == NOTE_GET_ITEM
|| note.type == NOTE_ID_ITEM
|| note.type == NOTE_BUY_ITEM
|| note.type == NOTE_DONATE_MONEY
|| note.type == NOTE_SEEN_MONSTER
|| note.type == NOTE_KILL_MONSTER
|| note.type == NOTE_POLY_MONSTER
|| note.type == NOTE_USER_NOTE
|| note.type == NOTE_MESSAGE
|| note.type == NOTE_LOSE_GOD
|| note.type == NOTE_PENANCE
|| note.type == NOTE_MOLLIFY_GOD
|| note.type == NOTE_DEATH
|| note.type == NOTE_XOM_REVIVAL
|| note.type == NOTE_SEEN_FEAT)
{
return (true);
}
// Never noteworthy, hooked up for fun or future use.
if (note.type == NOTE_MP_CHANGE
|| note.type == NOTE_MAXHP_CHANGE
|| note.type == NOTE_MAXMP_CHANGE)
{
return (false);
}
// Xom effects are only noteworthy if the option is true.
if (note.type == NOTE_XOM_EFFECT)
return (Options.note_xom_effects);
// God powers might be noteworthy if it's an actual power.
if (note.type == NOTE_GOD_POWER
&& _real_god_power(note.first, note.second) == -1)
{
return (false);
}
// HP noteworthiness is handled in its own function.
if (note.type == NOTE_HP_CHANGE
&& !_is_noteworthy_hp(note.first, note.second))
{
return (false);
}
// Skills are noteworthy if in the skill value list or if
// it's a new maximal skill (depending on options).
if (note.type == NOTE_GAIN_SKILL)
{
if (Options.note_all_skill_levels
|| _is_noteworthy_skill_level(note.second)
|| Options.note_skill_max && _is_highest_skill(note.first))
{
return (true);
}
return (false);
}
if (note.type == NOTE_DUNGEON_LEVEL_CHANGE)
{
if (!_is_noteworthy_dlevel(note.packed_place))
return (false);
// Labyrinths and portal vaults are always interesting.
if ((note.packed_place & 0xFF) == 0xFF
&& ((note.packed_place >> 8) == LEVEL_LABYRINTH
|| (note.packed_place >> 8) == LEVEL_PORTAL_VAULT))
{
return (true);
}
}
// Learning a spell is always noteworthy if note_all_spells is set.
if (note.type == NOTE_LEARN_SPELL && Options.note_all_spells)
return (true);
for (unsigned i = 0; i < note_list.size(); ++i)
{
if (note_list[i].type != note.type)
continue;
const Note& rnote( note_list[i] );
switch (note.type)
{
case NOTE_DUNGEON_LEVEL_CHANGE:
if (rnote.packed_place == note.packed_place)
return (false);
break;
case NOTE_LEARN_SPELL:
if (spell_difficulty(static_cast<spell_type>(rnote.first))
>= spell_difficulty(static_cast<spell_type>(note.first)))
{
return (false);
}
break;
case NOTE_GOD_POWER:
if (rnote.first == note.first && rnote.second == note.second)
return (false);
break;
case NOTE_HP_CHANGE:
// Not if we have a recent warning
// unless we've lost half our HP since then.
if (note.turn - rnote.turn < 5
&& note.first * 2 >= rnote.first)
{
return (false);
}
break;
default:
mpr("Buggy note passed: unknown note type");
// Return now, rather than give a "Buggy note passed" message
// for each note of the matching type in the note list.
return (true);
}
}
return (true);
}
static const char* _number_to_ordinal( int number )
{
const char* ordinals[5] = { "first", "second", "third", "fourth", "fifth" };
if (number < 1)
return "[unknown ordinal (too small)]";
if (number > 5)
return "[unknown ordinal (too big)]";
return ordinals[number-1];
}
std::string Note::describe( bool when, bool where, bool what ) const
{
std::ostringstream result;
if (when)
result << std::setw(6) << turn << " ";
if (where)
{
if (!place_abbrev.empty())
result << "| " << std::setw(MAX_NOTE_PLACE_LEN) << std::left
<< place_abbrev << " | ";
else
result << "| " << std::setw(MAX_NOTE_PLACE_LEN) << std::left
<< short_place_name(packed_place) << " | ";
}
if (what)
{
switch ( type )
{
case NOTE_HP_CHANGE:
// [ds] Shortened HP change note from "Had X hitpoints" to
// accommodate the cause for the loss of hitpoints.
result << "HP: " << first << "/" << second
<< " [" << name << "]";
break;
case NOTE_XOM_REVIVAL:
result << "Xom revived you";
break;
case NOTE_MP_CHANGE:
result << "Mana: " << first << "/" << second;
break;
case NOTE_MAXHP_CHANGE:
result << "Reached " << first << " max hit points";
break;
case NOTE_MAXMP_CHANGE:
result << "Reached " << first << " max mana";
break;
case NOTE_XP_LEVEL_CHANGE:
result << "Reached XP level " << first << ". " << name;
break;
case NOTE_DUNGEON_LEVEL_CHANGE:
if ( !desc.empty() )
result << desc;
else
result << "Entered " << place_name(packed_place, true, true);
break;
case NOTE_LEARN_SPELL:
result << "Learned a level "
<< spell_difficulty(static_cast<spell_type>(first))
<< " spell: "
<< spell_title(static_cast<spell_type>(first));
break;
case NOTE_GET_GOD:
result << "Became a worshipper of "
<< god_name(static_cast<god_type>(first), true);
break;
case NOTE_LOSE_GOD:
result << "Fell from the grace of "
<< god_name(static_cast<god_type>(first));
break;
case NOTE_PENANCE:
result << "Was placed under penance by "
<< god_name(static_cast<god_type>(first));
break;
case NOTE_MOLLIFY_GOD:
result << "Was forgiven by "
<< god_name(static_cast<god_type>(first));
break;
case NOTE_GOD_GIFT:
result << "Received a gift from "
<< god_name(static_cast<god_type>(first));
break;
case NOTE_ID_ITEM:
result << "Identified " << name;
if (!desc.empty())
result << " (" << desc << ")";
break;
case NOTE_GET_ITEM:
result << "Got " << name;
break;
case NOTE_BUY_ITEM:
result << "Bought " << name << " for " << first << " gold piece"
<< (first == 1 ? "" : "s");
break;
case NOTE_DONATE_MONEY:
result << "Donated " << first << " gold piece"
<< (first == 1 ? "" : "s") << " to Zin";
break;
case NOTE_GAIN_SKILL:
result << "Reached skill " << second
<< " in " << skill_name(first);
break;
case NOTE_SEEN_MONSTER:
result << "Noticed " << name;
break;
case NOTE_KILL_MONSTER:
if (second)
result << name << " (ally) was defeated";
else
result << "Defeated " << name;
break;
case NOTE_POLY_MONSTER:
result << name << " changed into " << desc;
break;
case NOTE_GOD_POWER:
result << "Acquired "
<< god_name(static_cast<god_type>(first)) << "'s "
<< _number_to_ordinal(_real_god_power(first, second)+1)
<< " power";
break;
case NOTE_GET_MUTATION:
result << "Gained mutation: "
<< mutation_name(static_cast<mutation_type>(first),
second == 0 ? 1 : second);
break;
case NOTE_LOSE_MUTATION:
result << "Lost mutation: "
<< mutation_name(static_cast<mutation_type>(first),
second == 3 ? 3 : second+1);
break;
case NOTE_DEATH:
result << name;
break;
case NOTE_USER_NOTE:
result << Options.user_note_prefix << name;
break;
case NOTE_MESSAGE:
result << name;
break;
case NOTE_SEEN_FEAT:
result << "Found " << name;
break;
case NOTE_XOM_EFFECT:
result << "XOM: " << name;
#if defined(DEBUG_XOM) || defined(NOTE_DEBUG_XOM)
// If debugging, also take note of piety and tension.
result << " (piety: " << first;
if (second >= 0)
result << ", tension: " << second;
result << ")";
#endif
break;
default:
result << "Buggy note description: unknown note type";
break;
}
}
if (type == NOTE_SEEN_MONSTER || type == NOTE_KILL_MONSTER)
{
if (what && first == MONS_PANDEMONIUM_DEMON)
result << " the pandemonium lord";
}
return result.str();
}
Note::Note()
{
turn = you.num_turns;
packed_place = get_packed_place();
if (you.level_type == LEVEL_PORTAL_VAULT)
place_abbrev = you.level_type_name_abbrev;
}
Note::Note( NOTE_TYPES t, int f, int s, const char* n, const char* d ) :
type(t), first(f), second(s), place_abbrev("")
{
if (n)
name = std::string(n);
if (d)
desc = std::string(d);
turn = you.num_turns;
packed_place = get_packed_place();
if (you.level_type == LEVEL_PORTAL_VAULT)
place_abbrev = you.level_type_name_abbrev;
}
void Note::check_milestone() const
{
#ifdef DGL_MILESTONES
if (crawl_state.arena)
return;
if (type == NOTE_DUNGEON_LEVEL_CHANGE)
{
const int br = place_branch(packed_place),
dep = place_depth(packed_place);
if (br != -1)
{
std::string branch = place_name(packed_place, true, false).c_str();
if (branch.find("The ") == 0)
branch[0] = tolower(branch[0]);
if (dep == 1)
mark_milestone("enter", "entered " + branch + ".");
else if (dep == _dungeon_branch_depth(br) && br != BRANCH_HIVE)
{
std::string level = place_name(packed_place, true, true);
if (level.find("Level ") == 0)
level[0] = tolower(level[0]);
std::ostringstream branch_finale;
branch_finale << "reached " << level << ".";
mark_milestone("branch-finale", branch_finale.str());
}
}
}
#endif
}
void Note::save(writer& outf) const
{
marshallLong( outf, type );
marshallLong( outf, turn );
marshallShort( outf, packed_place );
marshallLong( outf, first );
marshallLong( outf, second );
marshallString4( outf, name );
marshallString4( outf, place_abbrev );
marshallString4( outf, desc );
}
void Note::load(reader& inf)
{
type = static_cast<NOTE_TYPES>(unmarshallLong( inf ));
turn = unmarshallLong( inf );
packed_place = unmarshallShort( inf );
first = unmarshallLong( inf );
second = unmarshallLong( inf );
unmarshallString4( inf, name );
unmarshallString4( inf, place_abbrev );
unmarshallString4( inf, desc );
}
bool notes_active = false;
bool notes_are_active()
{
return (notes_active);
}
void take_note( const Note& note, bool force )
{
if (notes_active && (force || _is_noteworthy(note)))
{
note_list.push_back( note );
note.check_milestone();
}
}
void activate_notes( bool active )
{
notes_active = active;
}
void save_notes(writer& outf)
{
marshallLong( outf, NOTES_VERSION_NUMBER );
marshallLong( outf, note_list.size() );
for (unsigned i = 0; i < note_list.size(); ++i)
note_list[i].save(outf);
}
void load_notes(reader& inf)
{
if ( unmarshallLong(inf) != NOTES_VERSION_NUMBER )
return;
const long num_notes = unmarshallLong(inf);
for ( long i = 0; i < num_notes; ++i )
{
Note new_note;
new_note.load(inf);
note_list.push_back(new_note);
}
}
void make_user_note()
{
mpr("Enter note: ", MSGCH_PROMPT);
char buf[400];
bool validline = !cancelable_get_line(buf, sizeof(buf));
if (!validline || (!*buf))
return;
Note unote(NOTE_USER_NOTE);
unote.name = buf;
take_note(unote);
}