Make note_skill_levels default to what it used to, make it a simple list option.
[crawl.git] / crawl-ref / source / notes.cc
1 /**
2  * @file
3  * @brief Notetaking stuff
4 **/
5
6 #include "AppHdr.h"
7
8 #include <vector>
9 #include <sstream>
10 #include <iomanip>
11
12 #include "notes.h"
13
14 #include "branch.h"
15 #include "files.h"
16 #include "kills.h"
17 #include "hiscores.h"
18 #include "message.h"
19 #include "mutation.h"
20 #include "options.h"
21 #include "place.h"
22 #include "religion.h"
23 #include "skills2.h"
24 #include "spl-util.h"
25 #include "state.h"
26 #include "tags.h"
27
28 #define NOTES_VERSION_NUMBER 1002
29
30 std::vector<Note> note_list;
31
32 // return the real number of the power (casting out nonexistent powers),
33 // starting from 0, or -1 if the power doesn't exist
34 static int _real_god_power(int religion, int idx)
35 {
36     if (god_gain_power_messages[religion][idx][0] == 0)
37         return -1;
38
39     int count = 0;
40     for (int j = 0; j < idx; ++j)
41         if (god_gain_power_messages[religion][j][0])
42             ++count;
43
44     return count;
45 }
46
47 static bool _is_highest_skill(int skill)
48 {
49     for (int i = 0; i < NUM_SKILLS; ++i)
50     {
51         if (i == skill)
52             continue;
53         if (you.skills[i] >= you.skills[skill])
54             return false;
55     }
56     return true;
57 }
58
59 static bool _is_noteworthy_hp(int hp, int maxhp)
60 {
61     return (hp > 0 && Options.note_hp_percent
62             && hp <= (maxhp * Options.note_hp_percent) / 100);
63 }
64
65 static int _dungeon_branch_depth(uint8_t branch)
66 {
67     if (branch >= NUM_BRANCHES)
68         return -1;
69     return brdepth[branch];
70 }
71
72 static bool _is_noteworthy_dlevel(unsigned short place)
73 {
74     const uint8_t branch = (place >> 8) & 0xFF;
75     const int lev = (place & 0xFF);
76
77     // The Abyss is noted a different way (since we care mostly about the cause).
78     if (branch == BRANCH_ABYSS)
79         return false;
80
81     // Other portal levels are always interesting.
82     if (!is_connected_branch(static_cast<branch_type>(branch)))
83         return true;
84
85     if (lev == _dungeon_branch_depth(branch)
86         || branch == BRANCH_MAIN_DUNGEON && (lev % 5) == 0
87         || branch == BRANCH_MAIN_DUNGEON && lev == 14
88         || branch != BRANCH_MAIN_DUNGEON && lev == 1)
89     {
90         return true;
91     }
92
93     return false;
94 }
95
96 // Is a note worth taking?
97 // This function assumes that game state has not changed since
98 // the note was taken, e.g. you.* is valid.
99 static bool _is_noteworthy(const Note& note)
100 {
101     // Always noteworthy.
102     if (note.type == NOTE_XP_LEVEL_CHANGE
103         || note.type == NOTE_LEARN_SPELL
104         || note.type == NOTE_GET_GOD
105         || note.type == NOTE_GOD_GIFT
106         || note.type == NOTE_GET_MUTATION
107         || note.type == NOTE_LOSE_MUTATION
108         || note.type == NOTE_GET_ITEM
109         || note.type == NOTE_ID_ITEM
110         || note.type == NOTE_BUY_ITEM
111         || note.type == NOTE_DONATE_MONEY
112         || note.type == NOTE_SEEN_MONSTER
113         || note.type == NOTE_DEFEAT_MONSTER
114         || note.type == NOTE_POLY_MONSTER
115         || note.type == NOTE_USER_NOTE
116         || note.type == NOTE_MESSAGE
117         || note.type == NOTE_LOSE_GOD
118         || note.type == NOTE_PENANCE
119         || note.type == NOTE_MOLLIFY_GOD
120         || note.type == NOTE_DEATH
121         || note.type == NOTE_XOM_REVIVAL
122         || note.type == NOTE_SEEN_FEAT
123         || note.type == NOTE_PARALYSIS
124         || note.type == NOTE_NAMED_ALLY
125         || note.type == NOTE_ALLY_DEATH
126         || note.type == NOTE_FEAT_MIMIC)
127     {
128         return true;
129     }
130
131     // Never noteworthy, hooked up for fun or future use.
132     if (note.type == NOTE_MP_CHANGE
133         || note.type == NOTE_MAXHP_CHANGE
134         || note.type == NOTE_MAXMP_CHANGE)
135     {
136         return false;
137     }
138
139     // Xom effects are only noteworthy if the option is true.
140     if (note.type == NOTE_XOM_EFFECT)
141         return Options.note_xom_effects;
142
143     // God powers might be noteworthy if it's an actual power.
144     if (note.type == NOTE_GOD_POWER
145         && _real_god_power(note.first, note.second) == -1)
146     {
147         return false;
148     }
149
150     // HP noteworthiness is handled in its own function.
151     if (note.type == NOTE_HP_CHANGE
152         && !_is_noteworthy_hp(note.first, note.second))
153     {
154         return false;
155     }
156
157     // Skills are noteworthy if in the skill value list or if
158     // it's a new maximal skill (depending on options).
159     if (note.type == NOTE_GAIN_SKILL || note.type == NOTE_LOSE_SKILL)
160     {
161         if (Options.note_all_skill_levels
162             || note.second <= 27 && Options.note_skill_levels[note.second]
163             || Options.note_skill_max && _is_highest_skill(note.first))
164         {
165             return true;
166         }
167         return false;
168     }
169
170     if (note.type == NOTE_DUNGEON_LEVEL_CHANGE)
171         return _is_noteworthy_dlevel(note.packed_place);
172
173     for (unsigned i = 0; i < note_list.size(); ++i)
174     {
175         if (note_list[i].type != note.type)
176             continue;
177
178         const Note& rnote(note_list[i]);
179         switch (note.type)
180         {
181         case NOTE_GOD_POWER:
182             if (rnote.first == note.first && rnote.second == note.second)
183                 return false;
184             break;
185
186         case NOTE_HP_CHANGE:
187             // Not if we have a recent warning
188             // unless we've lost half our HP since then.
189             if (note.turn - rnote.turn < 5
190                 && note.first * 2 >= rnote.first)
191             {
192                 return false;
193             }
194             break;
195
196         default:
197             mpr("Buggy note passed: unknown note type");
198             // Return now, rather than give a "Buggy note passed" message
199             // for each note of the matching type in the note list.
200             return true;
201         }
202     }
203     return true;
204 }
205
206 static const char* _number_to_ordinal(int number)
207 {
208     const char* ordinals[5] = { "first", "second", "third", "fourth", "fifth" };
209
210     if (number < 1)
211         return "[unknown ordinal (too small)]";
212     if (number > 5)
213         return "[unknown ordinal (too big)]";
214     return ordinals[number-1];
215 }
216
217 std::string Note::describe(bool when, bool where, bool what) const
218 {
219     std::ostringstream result;
220
221     if (when)
222         result << std::setw(6) << turn << " ";
223
224     if (where)
225     {
226         result << "| " << chop_string(short_place_name(packed_place),
227                                       MAX_NOTE_PLACE_LEN) << " | ";
228     }
229
230     if (what)
231     {
232         switch (type)
233         {
234         case NOTE_HP_CHANGE:
235             // [ds] Shortened HP change note from "Had X hitpoints" to
236             // accommodate the cause for the loss of hitpoints.
237             result << "HP: " << first << "/" << second
238                    << " [" << name << "]";
239             break;
240         case NOTE_XOM_REVIVAL:
241             result << "Xom revived you";
242             break;
243         case NOTE_MP_CHANGE:
244             result << "Mana: " << first << "/" << second;
245             break;
246         case NOTE_MAXHP_CHANGE:
247             result << "Reached " << first << " max hit points";
248             break;
249         case NOTE_MAXMP_CHANGE:
250             result << "Reached " << first << " max mana";
251             break;
252         case NOTE_XP_LEVEL_CHANGE:
253             result << "Reached XP level " << first << ". " << name;
254             break;
255         case NOTE_DUNGEON_LEVEL_CHANGE:
256             if (!desc.empty())
257                 result << desc;
258             else
259                 result << "Entered " << place_name(packed_place, true, true);
260             break;
261         case NOTE_LEARN_SPELL:
262             result << "Learned a level "
263                    << spell_difficulty(static_cast<spell_type>(first))
264                    << " spell: "
265                    << spell_title(static_cast<spell_type>(first));
266             break;
267         case NOTE_GET_GOD:
268             result << "Became a worshipper of "
269                    << god_name(static_cast<god_type>(first), true);
270             break;
271         case NOTE_LOSE_GOD:
272             result << "Fell from the grace of "
273                    << god_name(static_cast<god_type>(first));
274             break;
275         case NOTE_PENANCE:
276             result << "Was placed under penance by "
277                    << god_name(static_cast<god_type>(first));
278             break;
279         case NOTE_MOLLIFY_GOD:
280             result << "Was forgiven by "
281                    << god_name(static_cast<god_type>(first));
282             break;
283         case NOTE_GOD_GIFT:
284             result << "Received a gift from "
285                    << god_name(static_cast<god_type>(first));
286             break;
287         case NOTE_ID_ITEM:
288             result << "Identified " << name;
289             if (!desc.empty())
290                 result << " (" << desc << ")";
291             break;
292         case NOTE_GET_ITEM:
293             result << "Got " << name;
294             break;
295         case NOTE_BUY_ITEM:
296             result << "Bought " << name << " for " << first << " gold piece"
297                    << (first == 1 ? "" : "s");
298             break;
299         case NOTE_DONATE_MONEY:
300             result << "Donated " << first << " gold piece"
301                    << (first == 1 ? "" : "s") << " to Zin";
302             break;
303         case NOTE_GAIN_SKILL:
304             result << "Reached skill level " << second
305                    << " in " << skill_name(static_cast<skill_type>(first));
306             break;
307         case NOTE_LOSE_SKILL:
308             result << "Reduced skill "
309                    << skill_name(static_cast<skill_type>(first))
310                    << " to level " << second;
311             break;
312         case NOTE_SEEN_MONSTER:
313             result << "Noticed " << name;
314             break;
315         case NOTE_DEFEAT_MONSTER:
316             if (second)
317                 result << name << " (ally) was " << desc;
318             else
319                 result << uppercase_first(desc) << " " << name;
320             break;
321         case NOTE_POLY_MONSTER:
322             result << name << " changed into " << desc;
323             break;
324         case NOTE_GOD_POWER:
325             result << "Acquired "
326                    << god_name(static_cast<god_type>(first)) << "'s "
327                    << _number_to_ordinal(_real_god_power(first, second)+1)
328                    << " power";
329             break;
330         case NOTE_GET_MUTATION:
331             result << "Gained mutation: "
332                    << mutation_name(static_cast<mutation_type>(first),
333                                     second == 0 ? 1 : second);
334             if (!name.empty())
335                 result << " [" << name << "]";
336             break;
337         case NOTE_LOSE_MUTATION:
338             result << "Lost mutation: "
339                    << mutation_name(static_cast<mutation_type>(first),
340                                     second == 3 ? 3 : second+1);
341             if (!name.empty())
342                 result << " [" << name << "]";
343             break;
344         case NOTE_DEATH:
345             result << name;
346             break;
347         case NOTE_USER_NOTE:
348             result << Options.user_note_prefix << name;
349             break;
350         case NOTE_MESSAGE:
351             result << name;
352             break;
353         case NOTE_SEEN_FEAT:
354             result << "Found " << name;
355             break;
356         case NOTE_FEAT_MIMIC:
357             result << name <<" was a mimic.";
358             break;
359         case NOTE_XOM_EFFECT:
360             result << "XOM: " << name;
361 #if defined(DEBUG_XOM) || defined(NOTE_DEBUG_XOM)
362             // If debugging, also take note of piety and tension.
363             result << " (piety: " << first;
364             if (second >= 0)
365                 result << ", tension: " << second;
366             result << ")";
367 #endif
368             break;
369         case NOTE_PARALYSIS:
370             result << "Paralysed by " << name << " for " << first << " turns";
371             break;
372         case NOTE_NAMED_ALLY:
373             result << "Gained " << name << " as an ally";
374             break;
375         case NOTE_ALLY_DEATH:
376             result << "Your ally " << name << " died";
377             break;
378         default:
379             result << "Buggy note description: unknown note type";
380             break;
381         }
382     }
383
384     if (type == NOTE_SEEN_MONSTER || type == NOTE_DEFEAT_MONSTER)
385     {
386         if (what && first == MONS_PANDEMONIUM_LORD)
387             result << " the pandemonium lord";
388     }
389     return result.str();
390 }
391
392 Note::Note()
393 {
394     turn         = you.num_turns;
395     packed_place = get_packed_place();
396 }
397
398 Note::Note(NOTE_TYPES t, int f, int s, const char* n, const char* d) :
399     type(t), first(f), second(s)
400 {
401     if (n)
402         name = std::string(n);
403     if (d)
404         desc = std::string(d);
405
406     turn         = you.num_turns;
407     packed_place = get_packed_place();
408 }
409
410 void Note::check_milestone() const
411 {
412     if (crawl_state.game_is_arena())
413         return;
414
415     if (type == NOTE_DUNGEON_LEVEL_CHANGE)
416     {
417         const int br = place_branch(packed_place),
418                  dep = place_depth(packed_place);
419
420         // Wizlabs report their milestones on their own.
421         if (br != -1 && br != BRANCH_WIZLAB)
422         {
423             ASSERT(br >= 0 && br < NUM_BRANCHES);
424             std::string branch = place_name(packed_place, true, false).c_str();
425             if (branch.find("The ") == 0)
426                 branch[0] = tolower(branch[0]);
427
428             if (dep == 1)
429             {
430                 mark_milestone(br == BRANCH_ZIGGURAT ? "zig.enter" : "br.enter",
431                                "entered " + branch + ".", true);
432             }
433             else if (dep == _dungeon_branch_depth(br) || dep == 14
434                      || br == BRANCH_ZIGGURAT)
435             {
436                 std::string level = place_name(packed_place, true, true);
437                 if (level.find("Level ") == 0)
438                     level[0] = tolower(level[0]);
439
440                 std::ostringstream branch_finale;
441                 branch_finale << "reached " << level << ".";
442                 mark_milestone(br == BRANCH_ZIGGURAT ? "zig" :
443                                dep == 14 ? "br.mid" : "br.end",
444                                branch_finale.str());
445             }
446         }
447     }
448 }
449
450 void Note::save(writer& outf) const
451 {
452     marshallInt(outf, type);
453     marshallInt(outf, turn);
454     marshallShort(outf, packed_place);
455     marshallInt(outf, first);
456     marshallInt(outf, second);
457     marshallString4(outf, name);
458     marshallString4(outf, desc);
459 }
460
461 void Note::load(reader& inf)
462 {
463     type = static_cast<NOTE_TYPES>(unmarshallInt(inf));
464     turn = unmarshallInt(inf);
465     packed_place = unmarshallShort(inf);
466     first  = unmarshallInt(inf);
467     second = unmarshallInt(inf);
468     unmarshallString4(inf, name);
469     unmarshallString4(inf, desc);
470 }
471
472 static bool notes_active = false;
473
474 bool notes_are_active()
475 {
476     return notes_active;
477 }
478
479 void take_note(const Note& note, bool force)
480 {
481     if (notes_active && (force || _is_noteworthy(note)))
482     {
483         note_list.push_back(note);
484         note.check_milestone();
485     }
486 }
487
488 void activate_notes(bool active)
489 {
490     notes_active = active;
491 }
492
493 void save_notes(writer& outf)
494 {
495     marshallInt(outf, NOTES_VERSION_NUMBER);
496     marshallInt(outf, note_list.size());
497     for (unsigned i = 0; i < note_list.size(); ++i)
498         note_list[i].save(outf);
499 }
500
501 void load_notes(reader& inf)
502 {
503     if (unmarshallInt(inf) != NOTES_VERSION_NUMBER)
504         return;
505
506     const int num_notes = unmarshallInt(inf);
507     for (int i = 0; i < num_notes; ++i)
508     {
509         Note new_note;
510         new_note.load(inf);
511         note_list.push_back(new_note);
512     }
513 }
514
515 void make_user_note()
516 {
517     char buf[400];
518     bool validline = !msgwin_get_line("Enter note: ", buf, sizeof(buf));
519     if (!validline || (!*buf))
520         return;
521     Note unote(NOTE_USER_NOTE);
522     unote.name = buf;
523     take_note(unote);
524 }