Invoke the malevolent chronology of Zot pull/1493 zot_branch_bank 1493/head
authorNicholas Feinberg <pleasingfung@gmail.com>
Thu, 16 Jul 2020 15:23:20 +0000 (08:23 -0700)
committerNicholas Feinberg <pleasingfung@gmail.com>
Thu, 30 Jul 2020 15:29:35 +0000 (08:29 -0700)
The natural tendency for roguelikes is to encourage cautious behavior.
It's over once you die once, after all, so you should take your time.
That's not a bad thing, but to balance things out and discourage
extremely time-consuming tactics (which tend not to be very fun!),
it's good to have a counterpressure encouraging players to move quickly.

Food was intended to be such a clock, but for a variety of reasons, it
never worked as a clock for anything but the most egregious abuses.
(And not even for that for foodless races, of course.) Scores is a
compelling motivation for some players, but not many - most are just
playing to win. How can we make the most viable way to win also the
most fun?

This commit adds a new 'Zot Clock'. It ticks up over time, and jumps
down whenever you enter a new level - roughly 6000 turns of clock per
level. Once you hit turn 11,500 or so on the clock, the malevolent and
unexplained entity Zot strikes, draining you immediately and then again
once every 100 turns or so. At turn 12,000, you die. Each branch tracks
the 'clock' separately.

6000 turns per level allows for a 300,000 turn 3-rune game, which
seems very generous. The effective cap of 12,000 turns 'stored' at any
given time means that there's somewhat less slack than that implies,
but a slow character can usually do levels in under 2500 turns and
inter-level travel in under 500, so I'm hopeful that normal characters
will rarely if ever see any of this system. The very slowest game we saw
(an extreme outlier) averaged slightly over 5k turns per level in one
branch and hit 7.5k turns in one level, which this clock would cover.

We use 'levels seen' here as a proxy for progress because that's
simple to conceptualize and relatively difficult to abuse - it's hard
to "stash" entire levels as a clock reserve in the same way you could
stash "unexplored map tiles" or "weak monsters". The separate clock
per branch allows players to dip into branches and then bail without
being penalized - the per-branch clock is paused while they're away.

The clock is paused while you're in Abyss, since that doesn't fit the
exploration model well and isn't usually that wise to linger in, and
turned off entirely when you get the ORB, since (a) there's a new
clock then and (b) it'd feel awful to die of time-out on the orb run.
It's possible to scum Pan for time, but I mean, if you're scumming
Pan, just win already!

Chei gives you some extra time, in keeping with the theme of the
god. It's possible that races need different clocks based on their
movement speeds, but I'm hopeful that the difference there is fairly
small relative to the slack in the clock.

19 files changed:
crawl-ref/source/dat/defaults/messages.txt
crawl-ref/source/dat/descript/status.txt
crawl-ref/source/files.cc
crawl-ref/source/god-passive.cc
crawl-ref/source/god-passive.h
crawl-ref/source/hiscores.cc
crawl-ref/source/items.cc
crawl-ref/source/nearby-danger.cc
crawl-ref/source/ouch.cc
crawl-ref/source/ouch.h
crawl-ref/source/output.cc
crawl-ref/source/player-reacts.cc
crawl-ref/source/stairs.cc
crawl-ref/source/status.cc
crawl-ref/source/status.h
crawl-ref/source/timed-effects.cc
crawl-ref/source/timed-effects.h
crawl-ref/source/wiz-you.cc
crawl-ref/source/xom.cc

index a7f0f61..5bbe5bf 100644 (file)
@@ -104,6 +104,11 @@ force_more_message += You don't have the energy to cast that spell
 force_more_message += You don't have the energy to cast any spells
 force_more_message += This is a scroll of acquirement
 
+## Zot is coming!
+force_more_message += You have lingered too long in familiar places
+force_more_message += Zot's attention fixes on you again
+force_more_message += Zot already knows this place too well
+
 ## Reduce chance of draining because flight or form runs out:
 force_more_message += ^Careful!
 
index d52d53e..5c39d12 100644 (file)
@@ -755,3 +755,9 @@ You are better at attacking unaware enemies, giving attacks against distracted,
 confused or otherwise unaware creatures a chance to deal increased damage as if
 the target were completely helpless.
 %%%%
+Zot status
+
+You have spent too much time in one branch, allowing the malevolent entity Zot to
+notice you. You will be repeatedly drained as Zot brings its full attention to bear,
+after which it will kill you instantly. Travel to new floors first!
+%%%%
index 56a16da..58224fb 100644 (file)
@@ -2180,6 +2180,13 @@ bool load_level(dungeon_feature_type stair_taken, load_mode_type load_mode,
         if (just_created_level)
             xom_new_level_noise_or_stealth();
     }
+
+    if (just_created_level && (load_mode == LOAD_ENTER_LEVEL
+                               || load_mode == LOAD_START_GAME))
+    {
+        decr_zot_clock();
+    }
+
     // Initialize halos, etc.
     invalidate_agrid(true);
 
index 17abc10..5e9c8cd 100644 (file)
@@ -324,6 +324,10 @@ static const vector<god_passive> god_passives[] =
         {  0, passive_t::slow_abyss,
               "GOD will NOW slow the Abyss"
         },
+        // TODO: this one should work regardless of penance, maybe?
+        {  0, passive_t::slow_zot,
+              "GOD will NOW slow Zot's hunt for you"
+        },
         {  1, passive_t::slow_poison, "process poison slowly" },
     },
 
index d261a96..d09ff4b 100644 (file)
@@ -77,6 +77,9 @@ enum class passive_t
     /// Fewer creatures spawn in the Abyss, and it morphs less quickly.
     slow_abyss,
 
+    /// The Zot clock runs more slowly.
+    slow_zot,
+
     /// Your attributes are boosted.
     stat_boost,
 
index 2ea975e..b4ec15a 100644 (file)
@@ -661,7 +661,7 @@ static const char *kill_method_names[] =
     "beogh_smiting", "divine_wrath", "bounce", "reflect", "self_aimed",
     "falling_through_gate", "disintegration", "headbutt", "rolling",
     "mirror_damage", "spines", "frailty", "barbs", "being_thrown",
-    "collision",
+    "collision", "zot",
 };
 
 static const char *_kill_method_name(kill_method_type kmt)
@@ -2602,6 +2602,10 @@ string scorefile_entry::death_description(death_desc_verbosity verbosity) const
         needs_damage = true;
         break;
 
+    case KILLED_BY_ZOT:
+        desc += terse ? "Zot" : "Tarried too long and was consumed by Zot";
+        break;
+
     default:
         desc += terse? "program bug" : "Nibbled to death by software bugs";
         break;
index 4d6d27d..ffba853 100644 (file)
@@ -76,6 +76,7 @@
 #include "terrain.h"
 #include "throw.h"
 #include "tilepick.h"
+#include "timed-effects.h" // bezotted
 #include "travel.h"
 #include "viewchar.h"
 #include "view.h"
@@ -1866,6 +1867,9 @@ static void _get_orb()
 
     mprf(MSGCH_ORB, "You pick up the Orb of Zot!");
 
+    if (bezotted())
+        mpr("Zot can harm you no longer.");
+
     env.orb_pos = you.pos(); // can be wrong in wizmode
     orb_pickup_noise(you.pos(), 30);
 
index 6a49207..dcacff9 100644 (file)
@@ -29,6 +29,7 @@
 #include "stringutil.h"
 #include "state.h"
 #include "terrain.h"
+#include "timed-effects.h" // decr_zot_clock
 #include "transform.h"
 #include "traps.h"
 #include "travel.h"
@@ -461,6 +462,7 @@ void revive()
     you.attribute[ATTR_FLIGHT_UNCANCELLABLE] = 0;
     you.attribute[ATTR_XP_DRAIN] = 0;
     you.attribute[ATTR_SERPENTS_LASH] = 0;
+    decr_zot_clock();
     you.los_noise_level = 0;
     you.los_noise_last_turn = 0; // silence in death
 
index e83b0b6..e5aa9e5 100644 (file)
@@ -825,9 +825,9 @@ void ouch(int dam, kill_method_type death_type, mid_t source, const char *aux,
                                || death_type == KILLED_BY_WATER);
 
     // death's door protects against everything but falling into water/lava,
-    // excessive rot, leaving the dungeon, or quitting.
+    // Zot, excessive rot, leaving the dungeon, or quitting.
     if (you.duration[DUR_DEATHS_DOOR] && !env_death && !non_death
-        && you.hp_max > 0)
+        && death_type != KILLED_BY_ZOT && you.hp_max > 0)
     {
         return;
     }
index d740589..5ff091e 100644 (file)
@@ -66,6 +66,7 @@ enum kill_method_type
     KILLED_BY_BARBS,
     KILLED_BY_BEING_THROWN,
     KILLED_BY_COLLISION,
+    KILLED_BY_ZOT,
     NUM_KILLBY
 };
 
index 13d7fd7..f819074 100644 (file)
@@ -1047,6 +1047,7 @@ static void _get_status_lights(vector<status_light>& out)
     const unsigned int important_statuses[] =
     {
         STATUS_ORB,
+        STATUS_BEZOTTED,
         STATUS_STR_ZERO, STATUS_INT_ZERO, STATUS_DEX_ZERO,
         STATUS_ALIVE_STATE,
         DUR_PARALYSIS,
index 10f7208..f590f58 100644 (file)
@@ -91,6 +91,7 @@
 #include "rltiles/tiledef-dngn.h"
 #include "tilepick.h"
 #endif
+#include "timed-effects.h" // bezotting
 #include "transform.h"
 #include "traps.h"
 #include "travel.h"
@@ -1033,6 +1034,8 @@ void player_reacts()
 
     if (you.props[EMERGENCY_FLIGHT_KEY].get_bool())
         _handle_emergency_flight();
+
+    incr_zot_clock();
 }
 
 void extract_manticore_spikes(const char* endmsg)
index b8bb4d3..37dbc6b 100644 (file)
@@ -44,6 +44,7 @@
  #include "tilepick.h"
 #endif
 #include "tiles-build-specific.h"
+#include "timed-effects.h" // bezotted
 #include "traps.h"
 #include "travel.h"
 #include "view.h"
@@ -789,6 +790,20 @@ void floor_transition(dungeon_feature_type how,
             else if (branch != BRANCH_ABYSS) // too many messages...
                 mprf("Welcome to %s!", branches[branch].longname);
         }
+        const bool was_bezotted = bezotted_in(old_level.branch);
+        if (bezotted())
+        {
+            if (was_bezotted)
+                mpr("Zot already knows this place too well. Flee this branch!");
+            else
+                mpr("Zot's attention fixes on you again. Flee this branch!");
+        } else if (was_bezotted)
+        {
+            if (branch == BRANCH_ABYSS)
+                mpr("Zot has no power in the Abyss.");
+            else
+                mpr("You feel Zot lose track of you.");
+        }
 
         if (branch == BRANCH_GAUNTLET)
             _gauntlet_effect();
index 0f76a77..dfcad9c 100644 (file)
@@ -24,6 +24,7 @@
 #include "spl-transloc.h" // for you_teleport_now() in duration-data
 #include "spl-wpnench.h" // for _end_weapon_brand() in duration-data
 #include "stringutil.h"
+#include "timed-effects.h" // bezotted
 #include "throw.h"
 #include "transform.h"
 #include "traps.h"
@@ -685,6 +686,15 @@ bool fill_status_info(int status, status_info& inf)
         }
         break;
 
+    case STATUS_BEZOTTED:
+        if (bezotted()) {
+            inf.light_colour = MAGENTA;
+            inf.light_text = "Zot";
+            inf.short_text = "bezotted";
+            inf.long_text = "You are being drained by Zot!";
+        }
+        break;
+
     default:
         if (!found)
         {
index 0a6139b..e9ee57f 100644 (file)
@@ -44,7 +44,8 @@ enum status_type
     STATUS_MISSILES,
     STATUS_SERPENTS_LASH,
     STATUS_HEAVENLY_STORM,
-    STATUS_LAST_STATUS = STATUS_HEAVENLY_STORM
+    STATUS_BEZOTTED,
+    STATUS_LAST_STATUS = STATUS_BEZOTTED
 };
 
 struct status_info
index 2181eee..da26918 100644 (file)
@@ -11,6 +11,7 @@
 #include "act-iter.h"
 #include "areas.h"
 #include "beam.h"
+#include "branch.h" // for zot clock key
 #include "cloud.h"
 #include "coordit.h"
 #include "corpse.h"
@@ -34,6 +35,7 @@
 #include "mon-place.h"
 #include "mon-project.h"
 #include "mutation.h"
+#include "notes.h"
 #include "player.h"
 #include "player-stats.h"
 #include "random.h"
@@ -424,7 +426,7 @@ struct timed_effect
 };
 
 // If you add an entry to this list, remember to add a matching entry
-// to timed_effect_type in timef-effect-type.h!
+// to timed_effect_type in timed-effect-type.h!
 static struct timed_effect timed_effects[] =
 {
     { rot_corpses,               200,   200, true  },
@@ -1258,3 +1260,121 @@ int speed_to_duration(int speed)
 
     return div_rand_round(100, speed);
 }
+
+// Returns -1 if the player hasn't been in this branch before.
+static int& _zot_clock_for(branch_type br)
+{
+    CrawlHashTable &branch_clock = you.props["ZOT_CLOCK"];
+    const string branch_name = branches[br].abbrevname;
+    // When entering a new branch, start with an empty clock.
+    // (You'll get the usual time when you finish entering.)
+    if (!branch_clock.exists(branch_name))
+        branch_clock[branch_name].get_int() = -1;
+    return branch_clock[branch_name].get_int();
+}
+
+static int& _zot_clock()
+{
+    return _zot_clock_for(you.where_are_you);
+}
+
+static bool _zot_clock_active_in(branch_type br)
+{
+    return br != BRANCH_ABYSS && !player_has_orb();
+}
+
+// Is the zot clock running, or is it paused or stopped altogether?
+bool zot_clock_active()
+{
+    return _zot_clock_active_in(you.where_are_you);
+}
+
+static bool _over_zot_threshold(branch_type br)
+{
+    return _zot_clock_for(br) >= MAX_ZOT_CLOCK - BEZOTTING_THRESHOLD;
+}
+
+// If the player was in the given branch, would they suffer penalties for
+// nearing the end of the zot clock?
+bool bezotted_in(branch_type br)
+{
+    return _zot_clock_active_in(br) && _over_zot_threshold(br);
+}
+
+// Is the player suffering penalties from nearing the end of the zot clock?
+bool bezotted()
+{
+    return bezotted_in(you.where_are_you);
+}
+
+// How many times should the player have been drained by Zot?
+int bezotting_level()
+{
+    if (!bezotted())
+        return 0;
+    const int MAX_ZOTS = 5;
+    const int TURNS_PER_ZOT = (MAX_ZOT_CLOCK - BEZOTTING_THRESHOLD) / MAX_ZOTS;
+    const int over_thresh = _zot_clock() - (MAX_ZOT_CLOCK - BEZOTTING_THRESHOLD);
+    return over_thresh / TURNS_PER_ZOT + 1;
+}
+
+// Decrease the zot clock when the player enters a new level.
+void decr_zot_clock()
+{
+    if (!zot_clock_active())
+        return;
+    int &zot = _zot_clock();
+    if (zot == -1)
+    {
+        // new branch
+        zot = MAX_ZOT_CLOCK - ZOT_CLOCK_PER_FLOOR;
+    } else {
+        // old branch, new floor
+        if (bezotted())
+            mpr("As you enter the new level, Zot loses track of you.");
+        zot = max(0, zot - ZOT_CLOCK_PER_FLOOR);
+    }
+}
+
+// Odds of the zot clock incrementing every aut, expressed as odds
+// out of 1000 (aka 10x a percent chance).
+static unsigned _zot_clock_odds()
+{
+    const int base_odds = 100; // 10% per aut, aka on average 1/turn
+    if (have_passive(passive_t::slow_zot))
+    {
+        // down to 6.7% at full piety, aka once every 1.5 turns. (only movement
+        // (is slowed, not all actions, so we shouldn't give double clock!)
+        return base_odds - div_rand_round(you.piety, 6);
+    }
+    return base_odds;
+}
+
+void incr_zot_clock()
+{
+    const int clock_incr = binomial(you.time_taken, _zot_clock_odds(), 1000);
+    const int old_lvl = bezotting_level();
+    _zot_clock() += clock_incr;
+    if (!bezotted())
+        return;
+
+    if (_zot_clock() >= MAX_ZOT_CLOCK)
+    {
+        mpr("Zot has found you!");
+        ouch(INSTANT_DEATH, KILLED_BY_ZOT);
+        return;
+    }
+
+    if (!old_lvl)
+    {
+        mpr("You have lingered too long in familiar places. Zot approaches. Travel to new levels before it's too late!");
+        drain_player(150, true, true);
+        take_note(Note(NOTE_MESSAGE, 0, 0, "Touched by the power of Zot."));
+    }
+    else if (bezotting_level() > old_lvl)
+    {
+        mpr("Zot draws near...");
+        drain_player(75, true, true);
+        take_note(Note(NOTE_MESSAGE, 0, 0, "Touched by the power of Zot."));
+    }
+}
index e1023e1..c1503e0 100644 (file)
@@ -5,6 +5,16 @@
 
 #pragma once
 
+// Roughly how many turns does the clock roll back every time the player enters
+// a new floor?
+static const int ZOT_CLOCK_PER_FLOOR = 6000;
+// After roughly how many turns without visiting new floors does the player
+// die instantly?
+static const int MAX_ZOT_CLOCK = ZOT_CLOCK_PER_FLOOR * 2;
+// Roughly how many turns before the end of the clock does the player become
+// bezotted?
+static const int BEZOTTING_THRESHOLD = 500;
+
 void update_level(int elapsedTime);
 monster* update_monster(monster& mon, int turns);
 void handle_time();
@@ -21,3 +31,11 @@ void setup_environment_effects();
 // Lava smokes, swamp water mists.
 void run_environment_effects();
 int speed_to_duration(int speed);
+
+bool zot_clock_active();
+bool bezotted();
+bool bezotted_in(branch_type br);
+int bezotting_level();
+void decr_zot_clock();
+void incr_zot_clock();
+void set_initial_zot_clock();
index 02fb35f..0e0544e 100644 (file)
@@ -33,6 +33,7 @@
 #include "state.h"
 #include "status.h"
 #include "stringutil.h"
+#include "timed-effects.h" // decr_zot_clock
 #include "transform.h"
 #include "unicode.h"
 #include "view.h"
@@ -212,6 +213,7 @@ void wizard_heal(bool super_heal)
         delete_all_temp_mutations("Super heal");
         you.stat_loss.init(0);
         you.attribute[ATTR_STAT_LOSS_XP] = 0;
+        decr_zot_clock();
         you.redraw_stats = true;
     }
     else
index 39645e0..ce822f9 100644 (file)
@@ -3307,6 +3307,7 @@ static int _death_is_worth_saving(const kill_method_type killed_by)
     case KILLED_BY_WATER:
     case KILLED_BY_DRAINING:
     case KILLED_BY_STARVATION:
+    case KILLED_BY_ZOT:
     case KILLED_BY_ROTTING:
 
     // Don't protect the player from these.