From 67fc2ab63b7f564043e876e4eb3e8011df04831a Mon Sep 17 00:00:00 2001 From: Robert Melton Date: Sun, 30 Aug 2026 20:31:41 -0400 Subject: [PATCH] Add Onward mode: continue after death for half your score A new game type (GAME_TYPE_ONWARD, `-onward`, rc `type = onward`) that plays as a normal game except that death is a choice. When the player would die, they are asked "Continue in the dungeon at the cost of half your points (N points)?"; accepting reuses the Felid extra-life path (pending_revival -> revive()) so they get back up on the next turn at full HP and MP where they fell, unless the tile is lava or deep water. Felid lives are spent first because they are free. Every continue halves whatever the run ends up being worth, so a win without dying keeps the full score and a late death still costs far more than an early one. Continues are counted in you.props (no save-format change) and the scorefile carries onward=N on every Onward entry, and score lines read "points (D deaths)", D counting the death that ended the game. Onward never touches normal games: saves go to saves/onward/, and scores, logfile, milestones and bones get the -onward qualifier, the same way Sprint and Descent are kept apart. The mode is listed in the startup menu, -help, -gametypes-json (webtiles), games.d/base.yaml, the manual and options guide. The Felid revival tail in ouch.cc is factored into _schedule_revival() so both paths share it. catch2-tests/test_onward.cc pins the game-type switches (which all fall through `default:` silently), the halving and the xlog field. --- crawl-ref/docs/crawl.6 | 1 + crawl-ref/docs/crawl_manual.rst | 5 + crawl-ref/docs/options_guide.txt | 2 +- crawl-ref/source/Makefile.obj | 1 + crawl-ref/source/catch2-tests/test_onward.cc | 96 +++++++++++++++++ crawl-ref/source/game-type.h | 1 + crawl-ref/source/hiscores.cc | 54 +++++++++- crawl-ref/source/hiscores.h | 9 ++ crawl-ref/source/initfile.cc | 15 ++- crawl-ref/source/main.cc | 9 +- crawl-ref/source/ng-setup.cc | 1 + crawl-ref/source/ouch.cc | 105 ++++++++++++++++--- crawl-ref/source/player.cc | 6 ++ crawl-ref/source/player.h | 4 + crawl-ref/source/startup.cc | 5 + crawl-ref/source/state.cc | 17 ++- crawl-ref/source/state.h | 1 + crawl-ref/source/tilepick.cc | 1 + crawl-ref/source/webserver/games.d/base.yaml | 5 + 19 files changed, 314 insertions(+), 24 deletions(-) create mode 100644 crawl-ref/source/catch2-tests/test_onward.cc diff --git a/crawl-ref/docs/crawl.6 b/crawl-ref/docs/crawl.6 index 60c8ca13a43..4cf0bce1601 100644 --- a/crawl-ref/docs/crawl.6 +++ b/crawl-ref/docs/crawl.6 @@ -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 diff --git a/crawl-ref/docs/crawl_manual.rst b/crawl-ref/docs/crawl_manual.rst index ba67a0996d6..2bbf657c502 100644 --- a/crawl-ref/docs/crawl_manual.rst +++ b/crawl-ref/docs/crawl_manual.rst @@ -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. diff --git a/crawl-ref/docs/options_guide.txt b/crawl-ref/docs/options_guide.txt index e1eb290a1fe..7242e3c7e90 100644 --- a/crawl-ref/docs/options_guide.txt +++ b/crawl-ref/docs/options_guide.txt @@ -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 diff --git a/crawl-ref/source/Makefile.obj b/crawl-ref/source/Makefile.obj index 3cedca7d9db..b65cf357eb9 100644 --- a/crawl-ref/source/Makefile.obj +++ b/crawl-ref/source/Makefile.obj @@ -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 \ diff --git a/crawl-ref/source/catch2-tests/test_onward.cc b/crawl-ref/source/catch2-tests/test_onward.cc new file mode 100644 index 00000000000..2b3f6d626d7 --- /dev/null +++ b/crawl-ref/source/catch2-tests/test_onward.cc @@ -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 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 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); +} diff --git a/crawl-ref/source/game-type.h b/crawl-ref/source/game-type.h index f30e3eef43f..0b62c2a0817 100644 --- a/crawl-ref/source/game-type.h +++ b/crawl-ref/source/game-type.h @@ -15,5 +15,6 @@ enum game_type GAME_TYPE_HIGH_SCORES, GAME_TYPE_CUSTOM_SEED, GAME_TYPE_DESCENT, + GAME_TYPE_ONWARD, NUM_GAME_TYPE }; diff --git a/crawl-ref/source/hiscores.cc b/crawl-ref/source/hiscores.cc index f06e0413440..0396b1ad211 100644 --- a/crawl-ref/source/hiscores.cc +++ b/crawl-ref/source/hiscores.cc @@ -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; @@ -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")); @@ -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); @@ -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; @@ -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; @@ -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" : ""); } @@ -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); } diff --git a/crawl-ref/source/hiscores.h b/crawl-ref/source/hiscores.h index dc4080b8767..59c75942788 100644 --- a/crawl-ref/source/hiscores.h +++ b/crawl-ref/source/hiscores.h @@ -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); @@ -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 @@ -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; diff --git a/crawl-ref/source/initfile.cc b/crawl-ref/source/initfile.cc index e17b450ad2e..06556bae33e 100644 --- a/crawl-ref/source/initfile.cc +++ b/crawl-ref/source/initfile.cc @@ -226,7 +226,8 @@ static map _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; @@ -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"; } @@ -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 @@ -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 @@ -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: @@ -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; diff --git a/crawl-ref/source/main.cc b/crawl-ref/source/main.cc index 3d0bdddc847..16edfab01fc 100644 --- a/crawl-ref/source/main.cc +++ b/crawl-ref/source/main.cc @@ -525,6 +525,7 @@ static void _show_commandline_options_help() puts(" -save-version Save file version for the given player"); puts(" -sprint select Sprint"); puts(" -sprint-map 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"); @@ -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(); } diff --git a/crawl-ref/source/ng-setup.cc b/crawl-ref/source/ng-setup.cc index bec9d41c86b..4647eee5993 100644 --- a/crawl-ref/source/ng-setup.cc +++ b/crawl-ref/source/ng-setup.cc @@ -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: diff --git a/crawl-ref/source/ouch.cc b/crawl-ref/source/ouch.cc index 45c4ebf9848..9bf6d07f53f 100644 --- a/crawl-ref/source/ouch.cc +++ b/crawl-ref/source/ouch.cc @@ -42,6 +42,7 @@ #include "item-prop.h" #include "items.h" #include "libutil.h" +#include "macro.h" #include "melee-attack.h" #include "message.h" #include "mgen-data.h" @@ -1194,6 +1195,84 @@ static void _print_endgame_messages(scorefile_entry &se) * @param skip_multipliers Whether to ignore harm/vitrify/etc. * @param skip_awaken Whether this damage will skip waking a sleeping player. */ +// "You die..." and Xom's commentary, then wait for a key. Split out so the +// Onward prompt can show the death before asking about it. +static void _announce_death(const scorefile_entry &se) +{ + canned_msg(MSG_YOU_DIE); + xom_death_message((kill_method_type) se.get_death_type()); + more(); +} + +// The player is dead, but gets back up at the start of their next turn (see +// revive()). Shared by Felid extra lives and Onward continues. +static void _schedule_revival(const scorefile_entry &se, + kill_method_type death_type, bool announce) +{ + you.pending_revival = true; + + stop_delay(true); + + // You wouldn't want to lose this accomplishment to a crash, would you? + // Especially if you manage to trigger one via lua somehow... + if (!crawl_state.disables[DIS_SAVE_CHECKPOINTS]) + save_game(false); + + if (announce) + _announce_death(se); + + if (death_type != KILLED_BY_ZOT) + _place_player_corpse(death_type == KILLED_BY_DISINT); +} + +// Quitting, winning and leaving end the game but are not deaths, so Onward +// has nothing to offer for them. +static bool _onward_can_continue(kill_method_type death_type) +{ + return death_type != KILLED_BY_QUITTING + && death_type != KILLED_BY_WINNING + && death_type != KILLED_BY_LEAVING; +} + +// Onward mode: offer to carry on where the player fell for half their score. +// `se` already reflects earlier continues, so the cost shown is what this one +// takes. The death is shown and acknowledged first, and any keys still queued +// from the fight are dropped, so the answer has to be a deliberate Y or N. +static bool _onward_continue(const scorefile_entry &se) +{ + _announce_death(se); + // FLUSH_ON_FAILURE exists in every supported version and is on by + // default; it drops keys queued during the fight. + flush_input_buffer(FLUSH_ON_FAILURE); + + const int score = se.get_score(); + const int cost = score - apply_onward_penalty(score, 1); + // Nothing left to pay with (0 points, or a single point that halving + // would take entirely): this death is final. + if (!onward_can_afford_continue(score)) + { + mprf(MSGCH_WARN, "Sorry, you don't even have the points to come back. " + "Goodbye."); + more(); + return false; + } + const string prompt = make_stringf( + "Continue in the dungeon at the cost of half your points " + "(%d point%s)?", cost, cost == 1 ? "" : "s"); + const bool answer = yesno(prompt.c_str(), false, 0); + // A dropped connection mid-prompt must not decide for the player; keep + // the character and let them choose again if they die again. + if (crawl_state.seen_hups) + return true; + if (!answer) + return false; + + take_note(Note(NOTE_MESSAGE, 0, 0, + make_stringf("Continued onward at the cost of %d point%s.", + cost, cost == 1 ? "" : "s"))); + return true; +} + void ouch(int dam, kill_method_type death_type, mid_t source, const char *aux, bool see_source, const char *death_source_name, bool skip_multipliers, bool skip_awaken) @@ -1479,23 +1558,23 @@ void ouch(int dam, kill_method_type death_type, mid_t source, const char *aux, you.deaths++; you.lives--; - you.pending_revival = true; - take_note(Note(NOTE_LOSE_LIFE, you.lives)); - stop_delay(true); - - // You wouldn't want to lose this accomplishment to a crash, would you? - // Especially if you manage to trigger one via lua somehow... - if (!crawl_state.disables[DIS_SAVE_CHECKPOINTS]) - save_game(false); + _schedule_revival(se, death_type, true); + return; + } + // Onward mode: death is a choice. Felid lives are spent first because + // they are free; this costs half the score, so ask. No max HP means + // revive() would kill us straight back here, so that death is final. + if (crawl_state.game_is_onward() && _onward_can_continue(death_type) + && you.hp_max > 0 && _onward_continue(se)) + { + mark_milestone("death", lowercase_first(se.long_kill_message()).c_str()); - canned_msg(MSG_YOU_DIE); - xom_death_message((kill_method_type) se.get_death_type()); - more(); + you.deaths++; + you.props[ONWARD_CONTINUES_KEY].get_int()++; - if (death_type != KILLED_BY_ZOT) - _place_player_corpse(death_type == KILLED_BY_DISINT); + _schedule_revival(se, death_type, false); return; } diff --git a/crawl-ref/source/player.cc b/crawl-ref/source/player.cc index 8b9458eaed2..5bf19a6960d 100644 --- a/crawl-ref/source/player.cc +++ b/crawl-ref/source/player.cc @@ -9122,3 +9122,9 @@ bool player::allies_forbidden() return get_mutation_level(MUT_NO_LOVE) || have_passive(passive_t::no_allies); } + +int player::onward_continues() const +{ + return props.exists(ONWARD_CONTINUES_KEY) + ? props[ONWARD_CONTINUES_KEY].get_int() : 0; +} diff --git a/crawl-ref/source/player.h b/crawl-ref/source/player.h index b65c5fb9c21..a1d376b423d 100644 --- a/crawl-ref/source/player.h +++ b/crawl-ref/source/player.h @@ -44,6 +44,9 @@ #define POWERED_BY_DEATH_KEY "powered_by_death_strength" #define FUGUE_KEY "fugue_of_the_fallen_bonus" #define FORCE_MAPPABLE_KEY "force_mappable" +// Onward mode: how many times the player has paid half their score to keep +// going after a death. Lives in props so it needs no save-format change. +#define ONWARD_CONTINUES_KEY "onward_continues" #define TEMP_WATERWALK_KEY "temp_waterwalk" #define EMERGENCY_FLIGHT_KEY "emergency_flight" #define DISABLED_BY_KEY "disabled_by" @@ -595,6 +598,7 @@ class player : public actor int get_hit_dice() const override; int get_experience_level() const override; int get_max_xl() const; + int onward_continues() const; bool is_player() const override { #ifndef DEBUG_GLOBALS diff --git a/crawl-ref/source/startup.cc b/crawl-ref/source/startup.cc index 69e10861227..b5fbe62d841 100644 --- a/crawl-ref/source/startup.cc +++ b/crawl-ref/source/startup.cc @@ -429,6 +429,9 @@ static const vector entries = "than the tutorial."}, {GAME_TYPE_DESCENT, "Dungeon Descent", "Mode with a branching, one-way path through the Dungeon." }, + {GAME_TYPE_ONWARD, "Onward", + "A normal game where death offers a way back, at the cost of half " + "your score each time." }, {GAME_TYPE_SPRINT, "Dungeon Sprint", "Hard, fixed single level game mode." }, {GAME_TYPE_INSTRUCTIONS, "Instructions", "Help menu." }, @@ -736,6 +739,7 @@ class UIStartupMenu : public Widget { 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: @@ -916,6 +920,7 @@ void UIStartupMenu::menu_item_activated(int id) { 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: diff --git a/crawl-ref/source/state.cc b/crawl-ref/source/state.cc index ee266502de8..39dab017456 100644 --- a/crawl-ref/source/state.cc +++ b/crawl-ref/source/state.cc @@ -555,12 +555,14 @@ bool game_state::player_is_dead() const bool game_state::game_has_random_floors() const { - return game_is_normal() || game_is_hints() || game_is_descent(); + return game_is_normal() || game_is_hints() || game_is_descent() + || game_is_onward(); } bool game_state::game_saves_prefs() const { - return game_is_normal() || game_is_hints() || game_is_descent(); + return game_is_normal() || game_is_hints() || game_is_descent() + || game_is_onward(); } bool game_state::game_is_valid_type() const @@ -605,6 +607,12 @@ bool game_state::game_is_descent() const return type == GAME_TYPE_DESCENT; } +bool game_state::game_is_onward() const +{ + ASSERT(game_is_valid_type()); + return type == GAME_TYPE_ONWARD; +} + bool game_state::game_is_hints_tutorial() const { return game_is_hints() || game_is_tutorial(); @@ -635,6 +643,8 @@ string game_state::game_type_name_for(game_type _type) return "Dungeon Sprint"; case GAME_TYPE_DESCENT: return "Dungeon Descent"; + case GAME_TYPE_ONWARD: + return "Onward"; case NUM_GAME_TYPE: return "Unknown"; } @@ -664,6 +674,8 @@ string game_state::game_savedir_path() const return "sprint/"; case GAME_TYPE_DESCENT: return "descent/"; + case GAME_TYPE_ONWARD: + return "onward/"; default: return ""; } @@ -678,6 +690,7 @@ string game_state::game_type_qualifier() const case GAME_TYPE_HINTS: case GAME_TYPE_TUTORIAL: case GAME_TYPE_DESCENT: + case GAME_TYPE_ONWARD: return "-" + gametype_to_str(type); default: return ""; diff --git a/crawl-ref/source/state.h b/crawl-ref/source/state.h index 7626e02c587..a705d45588f 100644 --- a/crawl-ref/source/state.h +++ b/crawl-ref/source/state.h @@ -231,6 +231,7 @@ struct game_state bool game_is_hints() const; bool game_is_hints_tutorial() const; bool game_is_descent() const; + bool game_is_onward() const; bool game_has_random_floors() const; bool game_saves_prefs() const; diff --git a/crawl-ref/source/tilepick.cc b/crawl-ref/source/tilepick.cc index 28e83f177f5..47bb4cc6c50 100644 --- a/crawl-ref/source/tilepick.cc +++ b/crawl-ref/source/tilepick.cc @@ -3884,6 +3884,7 @@ tileidx_t tileidx_gametype(const game_type gtype) { case GAME_TYPE_NORMAL: case GAME_TYPE_CUSTOM_SEED: + case GAME_TYPE_ONWARD: return TILEG_STARTUP_STONESOUP; case GAME_TYPE_DESCENT: return TILEG_STARTUP_IRONSOUP; diff --git a/crawl-ref/source/webserver/games.d/base.yaml b/crawl-ref/source/webserver/games.d/base.yaml index e543b224aca..df606678130 100644 --- a/crawl-ref/source/webserver/games.d/base.yaml +++ b/crawl-ref/source/webserver/games.d/base.yaml @@ -92,6 +92,11 @@ games: name: Descent options: - -descent + - id: onward-web-trunk + template: trunk + name: Onward + options: + - -onward - id: tut-web-trunk template: trunk name: Tutorial