Skip to content
Merged
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
1 change: 1 addition & 0 deletions crawl-ref/docs/crawl.6
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
.Op Fl test
.Op Fl sprint-map Ar map
.Op Fl sprint
.Op Fl onward
.Op Fl species Ar species
.Op Fl seed Ar num
.Op Fl script Ar file
Expand Down
5 changes: 5 additions & 0 deletions crawl-ref/docs/crawl_manual.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ Hints Mode for Dungeon Crawl
Dungeon Sprint
Start one of several single-map challenge mode games of Crawl.

Onward
Start a standard game of Crawl in which dying offers a choice: continue in
the dungeon where you fell, with full health and magic, at the cost of half
your score. Onward games keep their own saves and score lists.

Instructions
View the instructional help text you are currently reading.

Expand Down
2 changes: 1 addition & 1 deletion crawl-ref/docs/options_guide.txt
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ name = Delilah

type = normal
Choose a game type. Valid values are "normal", "seeded", "tutorial",
"arena", "sprint", "descent", and "hints". If explicitly set (not just
"arena", "sprint", "descent", "onward", and "hints". If explicitly set (not just
left to the default), this sets the default game type at the main menu.
If the main menu is bypassed (e.g. by setting `name`), this option can
be used to determine the game type. "descent" is only valid in alpha
Expand Down
1 change: 1 addition & 0 deletions crawl-ref/source/Makefile.obj
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ catch2-tests/test_files.o \
catch2-tests/test_items.o \
catch2-tests/test_mon-util.o \
catch2-tests/test_ng-init-branches.o \
catch2-tests/test_onward.o \
catch2-tests/test_player.o \
catch2-tests/test_player_fixture.o \
catch2-tests/test_randbook.o \
Expand Down
96 changes: 96 additions & 0 deletions crawl-ref/source/catch2-tests/test_onward.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#include "catch_amalgamated.hpp"

#include "AppHdr.h"
#include "game-type.h"
#include "hiscores.h"
#include "initfile.h"
#include "player.h"
#include "state.h"
#include "unwind.h"

#include "test_player_fixture.h"

// Every game-type switch in the codebase has a `default:` label, so a new
// mode that is missing from one compiles fine and silently behaves like a
// normal game (shared save directory, shared scorefile). These pin each one.
TEST_CASE("Onward mode is a distinct game type everywhere", "[single-file]")
{
unwind_var<game_type> gt(crawl_state.type, GAME_TYPE_ONWARD);

REQUIRE(crawl_state.game_is_onward());
REQUIRE_FALSE(crawl_state.game_is_normal());
REQUIRE_FALSE(crawl_state.game_is_sprint());
REQUIRE_FALSE(crawl_state.game_is_descent());

// Behaves like a normal dungeon game apart from death.
REQUIRE(crawl_state.game_has_random_floors());
REQUIRE(crawl_state.game_saves_prefs());

// Never shares files with real games.
REQUIRE(gametype_to_str(GAME_TYPE_ONWARD) == "onward");
REQUIRE(crawl_state.game_type_name() == "Onward");
REQUIRE(crawl_state.game_savedir_path() == "onward/");
REQUIRE(crawl_state.game_type_qualifier() == "-onward");
}

TEST_CASE("Onward continues halve the score each time", "[single-file]")
{
REQUIRE(apply_onward_penalty(1000, 0) == 1000);
REQUIRE(apply_onward_penalty(1000, 1) == 500);
REQUIRE(apply_onward_penalty(1000, 3) == 125);
REQUIRE(apply_onward_penalty(3, 2) == 0);
// Far more continues than bits in an int must not be undefined behaviour.
REQUIRE(apply_onward_penalty(1000000, 100) == 0);
}

TEST_CASE("Onward continue is refused when the score cannot pay",
"[single-file]")
{
// Need at least 2 points: half rounds down to >= 1 spent and >= 1 kept.
REQUIRE_FALSE(onward_can_afford_continue(0));
REQUIRE_FALSE(onward_can_afford_continue(1));
REQUIRE(onward_can_afford_continue(2));
// Cost rounds up (remainder rounds down): 3 -> keep 1, spend 2.
REQUIRE(apply_onward_penalty(3, 1) == 1);
REQUIRE(onward_can_afford_continue(3));
}

TEST_CASE_METHOD(MockPlayerYouTestsFixture,
"Onward continues are counted on the player", "[single-file]")
{
REQUIRE(you.onward_continues() == 0);
you.props[ONWARD_CONTINUES_KEY].get_int()++;
you.props[ONWARD_CONTINUES_KEY].get_int()++;
REQUIRE(you.onward_continues() == 2);
}

TEST_CASE("Onward continues survive an xlog round trip and are described",
"[single-file]")
{
unwind_var<game_type> gt(crawl_state.type, GAME_TYPE_ONWARD);

scorefile_entry se;
REQUIRE(se.parse("v=0.34.1:lv=0.1:name=Test:race=Minotaur:cls=Fighter"
":xl=5:sc=250:ktyp=mon:killer=an ogre:onward=2"
":start=20260830000000S:end=20260830000100S"));
REQUIRE(se.raw_string().find(":onward=2") != string::npos);
// Two paid continues plus the death that ended the game.
REQUIRE(hiscores_format_single_long(se, true).find("250 (3 deaths) Test")
!= string::npos);
REQUIRE(hiscores_format_single(se).find("250 (3 deaths) Test")
!= string::npos);

scorefile_entry win;
REQUIRE(win.parse("v=0.34.1:lv=0.1:name=Win:race=Minotaur:cls=Fighter"
":xl=27:sc=900000:ktyp=winning:onward=1"
":start=20260830000000S:end=20260830000100S"));
REQUIRE(hiscores_format_single(win).find("900000 (1 death) Win")
!= string::npos);

scorefile_entry clean;
REQUIRE(clean.parse("v=0.34.1:lv=0.1:name=Clean:race=Minotaur:cls=Fighter"
":xl=27:sc=900000:ktyp=winning"
":start=20260830000000S:end=20260830000100S"));
REQUIRE(hiscores_format_single(clean).find("900000 (0 deaths) Clean")
!= string::npos);
}
1 change: 1 addition & 0 deletions crawl-ref/source/game-type.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ enum game_type
GAME_TYPE_HIGH_SCORES,
GAME_TYPE_CUSTOM_SEED,
GAME_TYPE_DESCENT,
GAME_TYPE_ONWARD,
NUM_GAME_TYPE
};
54 changes: 49 additions & 5 deletions crawl-ref/source/hiscores.cc
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,7 @@ void scorefile_entry::init_from(const scorefile_entry &se)
penance = se.penance;
wiz_mode = se.wiz_mode;
explore_mode = se.explore_mode;
onward_continues = se.onward_continues;
birth_time = se.birth_time;
death_time = se.death_time;
real_time = se.real_time;
Expand Down Expand Up @@ -1087,6 +1088,7 @@ void scorefile_entry::init_with_fields()
penance = fields->int_field("pen");
wiz_mode = fields->int_field("wiz");
explore_mode = fields->int_field("explore");
onward_continues = fields->int_field("onward");

birth_time = _parse_time(fields->str_field("start"));
death_time = _parse_time(fields->str_field("end"));
Expand Down Expand Up @@ -1184,6 +1186,10 @@ void scorefile_entry::set_base_xlog_fields() const
fields->add_field("wiz", "%d", wiz_mode);
if (explore_mode)
fields->add_field("explore", "%d", explore_mode);
// Onward games always carry the field, so a clean win is visibly onward=0
// rather than indistinguishable from a normal game's entry.
if (crawl_state.game_is_onward() || onward_continues)
fields->add_field("onward", "%d", onward_continues);

fields->add_field("start", "%s", make_date_string(birth_time).c_str());
fields->add_field("dur", "%d", (int)real_time);
Expand Down Expand Up @@ -1550,6 +1556,7 @@ void scorefile_entry::reset()
penance = -1;
wiz_mode = 0;
explore_mode = 0;
onward_continues = 0;
birth_time = 0;
death_time = 0;
real_time = -1;
Expand Down Expand Up @@ -1702,6 +1709,12 @@ void scorefile_entry::init(time_t dt)
ASSERT(crawl_state.game_is_sprint());
// only sprint should use custom scores

if (crawl_state.game_is_onward())
{
onward_continues = you.onward_continues();
points = apply_onward_penalty(points, onward_continues);
}

race = you.species;
job = you.char_class;

Expand Down Expand Up @@ -1947,12 +1960,42 @@ void scorefile_entry::fixup_char_name()
}
}

// Onward mode: every continue halves whatever the run would otherwise be
// worth, so a win that never died keeps the full score and a late death still
// costs far more than an early one.
int apply_onward_penalty(int points, int continues)
{
for (int i = 0; i < continues && points > 0; ++i)
points /= 2;
return points;
}

bool onward_can_afford_continue(int points)
{
// Need >= 2 so half rounds down to >= 1 spent and >= 1 kept.
return points >= 2;
}

string scorefile_entry::onward_deaths_desc() const
{
if (!crawl_state.game_is_onward() && !onward_continues)
return "";
// Continues are the deaths that were paid for; a game that ended in
// death has one more.
const bool died = death_type != KILLED_BY_WINNING
&& death_type != KILLED_BY_QUITTING
&& death_type != KILLED_BY_LEAVING;
const int deaths = onward_continues + (died ? 1 : 0);
return make_stringf(" (%d death%s)", deaths, deaths == 1 ? "" : "s");
}

string scorefile_entry::single_cdesc() const
{
string scname;
scname = chop_string(name, 10);

return make_stringf("%8d %s %s-%02d%s", points, scname.c_str(),
return make_stringf("%8d%s %s %s-%02d%s", points,
onward_deaths_desc().c_str(), scname.c_str(),
race_class_name.c_str(), lvl,
(wiz_mode == 1) ? "W" : (explore_mode == 1) ? "E" : "");
}
Expand Down Expand Up @@ -2034,13 +2077,14 @@ scorefile_entry::character_description(death_desc_verbosity verbosity) const
// Please excuse the following bit of mess in the name of flavour ;)
if (verbose)
{
desc = make_stringf("%8d %s the %s (level %d",
points, name.c_str(), title.c_str(), lvl);
desc = make_stringf("%8d%s %s the %s (level %d",
points, onward_deaths_desc().c_str(), name.c_str(),
title.c_str(), lvl);
}
else
{
desc = make_stringf("%8d %s the %s %s (level %d",
points, name.c_str(),
desc = make_stringf("%8d%s %s the %s %s (level %d",
points, onward_deaths_desc().c_str(), name.c_str(),
_species_name(race).c_str(),
_job_name(job), lvl);
}
Expand Down
9 changes: 9 additions & 0 deletions crawl-ref/source/hiscores.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ string hiscores_print_list(int display_count, int format, int newest_entry, int&
void hiscores_print_all(int display_count = -1, int format = SCORE_TERSE);
void show_hiscore_table();

int apply_onward_penalty(int points, int continues);
// Onward mode: a run can only be continued while it can pay the halving cost
// and keep at least one point; below that, death is final.
bool onward_can_afford_continue(int points);
string hiscores_format_single(const scorefile_entry &se);
string hiscores_format_single_long(const scorefile_entry &se,
bool verbose = false);
Expand Down Expand Up @@ -122,6 +126,7 @@ class scorefile_entry
int penance; // penance
uint8_t wiz_mode; // character used wiz mode
uint8_t explore_mode; // character used explore mode
int onward_continues; // Onward mode: times the player paid to continue
time_t birth_time; // start time of character
time_t death_time; // end time of character
time_t real_time; // real playing time in seconds
Expand Down Expand Up @@ -178,6 +183,10 @@ class scorefile_entry
string raw_string() const;
bool parse(const string &line);

// Onward mode: how many times this character died, counting the death
// that ended the game. Empty for other modes.
string onward_deaths_desc() const;

string hiscore_line(death_desc_verbosity verbosity) const;

string character_description(death_desc_verbosity) const;
Expand Down
15 changes: 13 additions & 2 deletions crawl-ref/source/initfile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ static map<string, game_type> _game_modes()
{"arena", GAME_TYPE_ARENA},
{"sprint", GAME_TYPE_SPRINT},
{"tutorial", GAME_TYPE_TUTORIAL},
{"hints", GAME_TYPE_HINTS}
{"hints", GAME_TYPE_HINTS},
{"onward", GAME_TYPE_ONWARD}
};
if (Version::ReleaseType == VER_ALPHA)
modes["descent"] = GAME_TYPE_DESCENT;
Expand Down Expand Up @@ -1248,6 +1249,8 @@ string gametype_to_str(game_type type)
return "hints";
case GAME_TYPE_DESCENT:
return "descent";
case GAME_TYPE_ONWARD:
return "onward";
default:
return "none";
}
Expand Down Expand Up @@ -4591,6 +4594,7 @@ enum commandline_option_type
CLO_GAMETYPES_JSON,
CLO_EDIT_BONES,
CLO_DESCENT,
CLO_ONWARD,
#if defined(UNIX) || defined(USE_TILE_LOCAL)
CLO_HEADLESS,
#endif
Expand Down Expand Up @@ -4644,7 +4648,7 @@ static const char *cmd_ops[] =
"print-charset", "tutorial", "wizard", "explore", "no-save",
"no-player-bones", "gdb", "no-gdb", "nogdb", "throttle", "no-throttle",
"lua-max-memory", "playable-json", "branches-json", "save-json",
"gametypes-json", "bones", "descent",
"gametypes-json", "bones", "descent", "onward",
#if defined(UNIX) || defined(USE_TILE_LOCAL)
"headless",
#endif
Expand Down Expand Up @@ -5379,6 +5383,8 @@ static string _gametype_to_clo(game_type g)
return cmd_ops[CLO_SPRINT];
case GAME_TYPE_DESCENT: // no CLO?
return cmd_ops[CLO_DESCENT];
case GAME_TYPE_ONWARD:
return cmd_ops[CLO_ONWARD];
case GAME_TYPE_HINTS: // no CLO?
case GAME_TYPE_NORMAL:
default:
Expand Down Expand Up @@ -5908,6 +5914,11 @@ bool parse_args(int argc, char **argv, bool rc_only)
Options.game.type = GAME_TYPE_DESCENT;
break;

case CLO_ONWARD:
if (!rc_only)
Options.game.type = GAME_TYPE_ONWARD;
break;

case CLO_SPRINT_MAP:
if (!next_is_param)
return false;
Expand Down
9 changes: 8 additions & 1 deletion crawl-ref/source/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ static void _show_commandline_options_help()
puts(" -save-version <name> Save file version for the given player");
puts(" -sprint select Sprint");
puts(" -sprint-map <name> preselect a Sprint map");
puts(" -onward select Onward (continue after death for half your score)");
puts(" -tutorial select the Tutorial");
#ifdef WIZARD
puts(" -wizard allow access to wizard mode");
Expand Down Expand Up @@ -1077,7 +1078,13 @@ static void _input()
if (you.pending_revival)
{
revive();
bring_to_safety();
// Onward continues put you back where you fell, unless the floor
// itself would just kill you again.
if (!crawl_state.game_is_onward()
|| is_feat_dangerous(env.grid(you.pos()), true))
{
bring_to_safety();
}
redraw_screen();
update_screen();
}
Expand Down
1 change: 1 addition & 0 deletions crawl-ref/source/ng-setup.cc
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ void setup_game(const newgame_def& ng,
{
case GAME_TYPE_NORMAL:
case GAME_TYPE_DESCENT:
case GAME_TYPE_ONWARD:
case GAME_TYPE_CUSTOM_SEED:
case GAME_TYPE_TUTORIAL:
case GAME_TYPE_SPRINT:
Expand Down
Loading
Loading