1
0
mirror of https://github.com/zenorogue/hyperrogue.git synced 2024-11-23 13:07:16 +00:00
hyperrogue/tour.cpp

1069 lines
36 KiB
C++
Raw Normal View History

// Hyperbolic Rogue -- the Tutorial/presentation
// Copyright (C) 2011-2018 Zeno Rogue, see 'hyper.cpp' for details
/** \file tour.cpp
* \brief the Tutorial and presentation system
*/
#include "hyper.h"
2019-08-10 08:57:14 +00:00
namespace hr {
2020-10-15 14:33:52 +00:00
#if !CAP_TOUR
EX namespace tour {
EX always_false on;
EX }
#endif
2019-09-06 07:17:45 +00:00
#if CAP_TOUR
2020-03-27 19:54:21 +00:00
2020-03-27 20:47:09 +00:00
/** \brief Variables and function related to Guided Tour and other presentations. */
2019-08-10 08:57:14 +00:00
EX namespace tour {
2020-03-27 20:47:09 +00:00
/** \brief are we currently in a presentation */
2019-08-10 08:57:14 +00:00
EX bool on;
2020-03-27 20:47:09 +00:00
/** \brief should the presentation texts be shown */
EX bool texts = true;
2019-08-10 08:57:14 +00:00
EX string tourhelp;
2020-03-27 20:47:09 +00:00
/** \brief index of the current slide */
2019-08-10 08:57:14 +00:00
EX int currentslide;
#if HDR
2020-03-27 20:47:09 +00:00
/** \brief a parameter for the slides' action function */
2019-08-10 08:57:14 +00:00
enum presmode {
pmStartAll = 0,
pmStart = 1, pmFrame = 2, pmStop = 3, pmKey = 4, pmRestart = 5,
pmAfterFrame = 6, pmHelpEx = 7,
2020-04-29 13:14:36 +00:00
pmGeometry = 11, pmGeometryReset = 13, pmGeometryStart = 15,
pmGeometrySpecial = 16
2019-08-10 08:57:14 +00:00
};
2020-03-27 20:47:09 +00:00
/** \brief slide definition */
2019-08-10 08:57:14 +00:00
struct slide {
2020-03-27 20:47:09 +00:00
/** \brief title of this slide */
string name;
2020-03-27 20:47:09 +00:00
/** \brief ID (currently unused */
2020-03-27 19:54:21 +00:00
int unused_id;
2020-03-27 20:47:09 +00:00
/** \brief various flags */
2020-03-27 19:54:21 +00:00
flagtype flags;
2020-03-27 20:47:09 +00:00
/** \brief the helptext */
2020-04-07 12:05:47 +00:00
string help;
2020-03-27 20:47:09 +00:00
/** \brief This function is called while this slide is displayed. Parameter hr::tour::presmode mode says what should be done */
2019-08-10 08:57:14 +00:00
function<void(presmode mode)> action;
};
2020-03-27 20:47:09 +00:00
/** \brief in which geometries does this slide work */
2020-03-27 19:54:21 +00:00
namespace LEGAL {
2020-04-29 13:14:36 +00:00
enum flagtype { NONE, UNLIMITED, HYPERBOLIC, ANY, NONEUC, SPECIAL };
2020-03-27 19:54:21 +00:00
}
2020-03-27 20:47:09 +00:00
/** \brief when Enter pressed while showing the text, skip to the next slide immediately */
static constexpr flagtype QUICKSKIP=8;
2020-03-27 20:47:09 +00:00
/** \brief The final slide. Shows where the presentation ends */
static constexpr flagtype FINALSLIDE=16;
2020-03-27 20:47:09 +00:00
/** \brief Pressing Enter while in another geometry should change slides immediately */
static constexpr flagtype QUICKGEO=32;
2020-03-27 20:47:09 +00:00
/** \brief This slide should be displayed in sidescreen mode */
static constexpr flagtype SIDESCREEN = 64;
2020-03-27 20:47:09 +00:00
/** \brief When changing geometries, show the name of the slide, instead of the current land */
static constexpr flagtype USE_SLIDE_NAME = 128;
2020-09-15 17:11:29 +00:00
/** \brief do not display any help line */
static constexpr flagtype NOTITLE = 256;
2022-08-13 20:44:44 +00:00
/** \brief always display the text, even if going back or texts are disabled */
static constexpr flagtype ALWAYS_TEXT = 256;
2019-08-10 08:57:14 +00:00
#endif
EX vector<reaction_t> restorers;
#if HDR
2020-09-15 17:11:29 +00:00
template<class T, class U> void slide_backup(T& what, U value) {
T backup = what;
restorers.push_back([&what, backup] { what = backup; });
what = value;
}
2021-03-31 13:23:40 +00:00
template<class T> void slide_backup(T& what) { slide_backup(what, what); }
#endif
EX void on_restore(const reaction_t& t) {
restorers.push_back(t);
}
EX void slide_restore_all() {
while(!restorers.empty()) {
restorers.back()();
restorers.pop_back();
}
}
EX void slide_url(presmode mode, char key, string text, string url) {
if(mode == pmHelpEx)
help_extensions.push_back(help_extension{key, text, [url] () {
open_url(url);
}});
}
2022-02-27 13:51:02 +00:00
EX void slide_action(presmode mode, char key, string text, reaction_t act) {
if(mode == pmHelpEx)
help_extensions.push_back(help_extension{key, text, act});
}
2024-05-05 18:15:53 +00:00
EX void enable_canvas_backup(ccolor::data *canv) {
slide_backup(ccolor::which, canv);
2022-07-23 14:33:11 +00:00
slide_backup(firstland, laCanvas);
slide_backup(specialland, laCanvas);
slide_backup(land_structure);
slide_backup(randomPatternsMode);
enable_canvas();
}
2020-03-27 20:47:09 +00:00
/** \brief an auxiliary function to enable a visualization in the Canvas land */
2024-06-16 14:13:22 +00:00
EX void setCanvas(presmode mode, ccolor::data *canv, reaction_t f) {
if(mode == pmStart) {
2019-05-28 23:09:38 +00:00
gamestack::push();
2022-07-23 14:33:11 +00:00
enable_canvas_backup(canv);
2024-06-16 14:13:22 +00:00
f();
start_game();
2019-05-30 15:47:54 +00:00
resetview();
}
if(mode == pmStop) {
2019-05-28 23:09:38 +00:00
gamestack::pop();
slide_restore_all();
}
}
2024-06-16 14:13:22 +00:00
EX void setCanvas(presmode mode, ccolor::data *canv) { setCanvas(mode, canv, [] {}); }
EX void setCanvasColor(presmode mode, color_t col, reaction_t f) {
2024-06-22 09:27:30 +00:00
setCanvas(mode, &ccolor::plain, [f, col] { slide_backup(ccolor::rwalls, 0); slide_backup(ccolor::plain.ctab, colortable{col}); f(); });
2024-06-16 14:13:22 +00:00
}
EX void setWhiteCanvas(presmode mode, reaction_t f) {
setCanvasColor(mode, 0xFFFFFF, f);
}
EX void setWhiteCanvas(presmode mode) { setWhiteCanvas(mode, [] {}); }
EX void setPlainCanvas(presmode mode, reaction_t f) { setCanvas(mode, &ccolor::plain, [f] {slide_backup(ccolor::rwalls, 0); f(); }); }
EX void setPlainCanvas(presmode mode) { setCanvas(mode, &ccolor::plain, [] {slide_backup(ccolor::rwalls, 0); }); }
2020-03-27 20:47:09 +00:00
/** \brief static mode: we get Orbs of Teleport to use them instead of movement */
bool sickmode;
2019-08-10 08:57:14 +00:00
EX function<eLand(eLand)> getNext;
EX function<bool(eLand)> quickfind;
EX function<bool(eLand)> showland;
#define GETNEXT getNext = [](eLand old)
#define QUICKFIND quickfind = [](eLand l)
#define SHOWLAND(f) showland = [](eLand l) { return f; }
2020-03-27 20:47:09 +00:00
/** \brief the caption of the special command (executed by pressing '5') in the current slide */
2019-08-10 08:57:14 +00:00
EX string slidecommand;
2020-03-27 20:47:09 +00:00
/** \brief hooks to execute after calling presentation */
EX hookset<void(int)> hooks_slide;
2020-03-27 20:47:09 +00:00
/** \brief call action(mode) for the current slide. Also sets up some default stuff */
2019-08-10 08:57:14 +00:00
EX void presentation(presmode mode) {
cheater = 0;
if(mode == pmStart) tourhelp = XLAT(slides[currentslide].name);
if(sickmode && !items[itOrbTeleport]) items[itOrbTeleport] = 1;
if(mode == pmStart) slidecommand = "";
GETNEXT { return laNone; };
QUICKFIND { return false; };
SHOWLAND(true);
slides[currentslide].action(mode);
callhooks(hooks_slide, mode);
2020-09-11 09:19:19 +00:00
if(mode == pmStop) slide_restore_all();
}
string parent_folder(const string& s) {
for(int k=isize(s)-2; k>=0; k--) if(s[k] == '/') return s.substr(0, k+1);
return "";
}
string get_slidename(const string& s) {
int i = 0;
for(int k=0; k<isize(s); k++) if(s[k] == '/') i = k+1;
return s.substr(i);
}
EX string get_foldername(const string& s) {
int i = 0;
for(int k=0; k<isize(s); k++) if(s[k] == '/') i = k+1;
return s.substr(0, i);
}
bool in_folder(const string& s, const string& folder) {
return s.substr(0, isize(folder)) == folder;
}
string get_subname(const string& s, const string& folder) {
for(int k=isize(folder); k<isize(s); k++) if(s[k] == '/')
return s.substr(isize(folder), k+1 - isize(folder));
return s.substr(isize(folder));
}
2020-03-27 20:47:09 +00:00
/** \brief display the help text for the current slide if texts enabled */
2019-08-10 08:57:14 +00:00
EX void slidehelp() {
2022-08-13 20:44:44 +00:00
if(!slides[currentslide].help[0]) return;
string slidename = get_slidename(slides[currentslide].name);
gotoHelp(
help =
helptitle(XLAT(slidename), 0xFF8000) +
XLAT(slides[currentslide].help)
);
presentation(pmHelpEx);
}
2020-03-27 20:47:09 +00:00
/** \brief return from a subgame launched while in presentation */
2018-04-22 09:10:19 +00:00
void return_geometry() {
2019-05-28 23:09:38 +00:00
gamestack::pop();
pconf.scale = 1; pconf.alpha = 1;
2018-04-22 09:10:19 +00:00
presentation(pmGeometryReset);
addMessage(XLAT("Returned to your game."));
}
2022-02-26 12:33:11 +00:00
EX bool next_slide() {
flagtype flags = slides[currentslide].flags;
popScreenAll();
if(gamestack::pushed()) {
return_geometry();
if(!(flags & QUICKGEO)) return true;
}
if(flags & FINALSLIDE) return true;
presentation(pmStop);
slide_restore_all();
currentslide++;
presentation(pmStart);
2022-08-13 20:44:44 +00:00
if(texts) slidehelp();
2022-02-26 12:33:11 +00:00
return true;
}
bool handleKeyTour(int sym, int uni) {
2017-07-10 18:47:38 +00:00
if(!tour::on) return false;
2017-07-12 17:50:39 +00:00
if(!(cmode & sm::DOTOUR)) return false;
bool inhelp = cmode & sm::HELP;
flagtype flags = slides[currentslide].flags;
2022-02-26 12:33:11 +00:00
if((sym == SDLK_RETURN || sym == SDLK_KP_ENTER) && (!inhelp || (flags & QUICKSKIP)))
return next_slide();
if(sym == SDLK_BACKSPACE) {
if(gamestack::pushed()) {
2019-05-28 23:09:38 +00:00
gamestack::pop();
2017-07-04 13:38:33 +00:00
if(!(flags & QUICKGEO)) return true;
}
if(currentslide == 0) { slidehelp(); return true; }
presentation(pmStop);
currentslide--;
presentation(pmStart);
popScreenAll();
2022-08-13 20:44:44 +00:00
if(inhelp || (flags & ALWAYS_TEXT)) slidehelp();
return true;
}
int legal = slides[currentslide].flags & 7;
if((NUMBERKEY == '1' || NUMBERKEY == '2') && (legal != LEGAL::NONE)) {
2020-04-29 13:14:36 +00:00
if(legal == LEGAL::SPECIAL) {
presentation(pmGeometrySpecial);
return true;
}
2020-03-27 19:54:21 +00:00
if(legal == LEGAL::NONE || legal == LEGAL::HYPERBOLIC) {
addMessage(XLAT("You cannot change geometry in this slide."));
return true;
}
2020-03-27 19:54:21 +00:00
if(legal == LEGAL::UNLIMITED && NUMBERKEY == '1') {
addMessage(XLAT("This does not work in bounded geometries."));
return true;
}
2020-03-27 19:54:21 +00:00
if(legal == LEGAL::NONEUC && NUMBERKEY == '2') {
addMessage(XLAT("This does not work in Euclidean geometry."));
return true;
}
2020-03-27 19:54:21 +00:00
if(legal == LEGAL::HYPERBOLIC && NUMBERKEY != '3') {
addMessage(XLAT("This works only in hyperbolic geometry."));
return true;
}
if(NUMBERKEY == '2') {
2017-11-03 18:31:42 +00:00
dynamicval<eGeometry> g(geometry, gEuclid);
if(cwt.at->land != laCanvas && !land_validity(cwt.at->land).quality_level) {
addMessage(XLAT("This land has no Euclidean version."));
return true;
}
}
if(NUMBERKEY == '1') {
2017-11-03 18:31:42 +00:00
dynamicval<eGeometry> g(geometry, gSphere);
if(cwt.at->land != laCanvas && !land_validity(cwt.at->land).quality_level) {
addMessage(XLAT("This land has no spherical version."));
return true;
}
}
if(geometry || CHANGED_VARIATION) {
2018-04-22 09:10:19 +00:00
return_geometry();
return true;
}
presentation(pmGeometry);
firstland = specialland = cwt.at->land;
2019-05-28 23:09:38 +00:00
gamestack::push();
switch(NUMBERKEY) {
case '3':
set_variation(eVariation::pure);
break;
case '1':
set_geometry(gSphere);
pconf.alpha = 1, pconf.scale = .5;
break;
case '2':
set_geometry(gEuclid);
pconf.alpha = 1, pconf.scale = .5;
break;
}
start_game();
2019-05-30 15:47:54 +00:00
resetview();
presentation(pmGeometryStart);
2018-04-22 09:10:19 +00:00
string x;
if(slides[currentslide].flags & USE_SLIDE_NAME) {
if(NUMBERKEY == '1') x = XLAT("Spherical version of %the1. ", s0 + "'" + get_slidename(slides[currentslide].name) + "'");
if(NUMBERKEY == '2') x = XLAT("Euclidean version of %the1. ", s0 + "'" + get_slidename(slides[currentslide].name) + "'");
2018-04-22 09:10:19 +00:00
}
else {
if(NUMBERKEY == '1') x = XLAT("Spherical version of %the1. ", cwt.at->land);
if(NUMBERKEY == '2') x = XLAT("Euclidean version of %the1. ", cwt.at->land);
2018-04-22 09:10:19 +00:00
}
if(mousing)
addMessage(x + XLAT("Click again to go back to your game."));
else
addMessage(x + XLAT("Press %1 again to go back to your game.", dialog::keyname(sym)));
return true;
}
if(NUMBERKEY == '3' && sphere) {
if(pconf.alpha < 2) pconf.scale = 400, pconf.alpha = 400; else pconf.scale = .5, pconf.alpha = 1;
2018-04-22 09:10:19 +00:00
addMessage(XLAT("Changed the projection."));
return true;
}
if(NUMBERKEY == '4') {
2017-07-10 18:47:38 +00:00
popScreenAll();
if(items[itOrbTeleport]) goto give_aether;
items[itOrbTeleport] = 1;
checkmove();
if(!canmove) {
if(items[itOrbAether]) goto give_flash;
give_aether:
items[itOrbAether] = 10;
checkmove();
if(!canmove) {
give_flash:
activateFlash();
canmove = true;
}
}
else {
bool shift = vid.shifttarget & 1;
addMessage(
shift ?
XLAT("Shift-click a location to teleport there.")
: XLAT("Click a location to teleport there.")
);
}
return true;
}
if(NUMBERKEY == '5') {
presentation(pmKey);
return true;
}
if(NUMBERKEY == '6') {
sickmode = !sickmode;
static ld spd;
if(sickmode == true) {
spd = vid.sspeed, vid.sspeed = 5;
addMessage(XLAT("Static mode enabled."));
}
else {
vid.sspeed = spd;
addMessage(XLAT("Static mode disabled."));
}
return true;
}
if(NUMBERKEY == '7') {
texts = !texts;
if(texts) slidehelp();
else addMessage("Help texts disabled.");
return true;
}
if(NUMBERKEY == '8') {
history::includeHistory = !history::includeHistory;
return true;
}
if(NUMBERKEY == '9') {
ss::current_folder = get_foldername(slides[currentslide].name);
2017-07-10 18:47:38 +00:00
pushScreen(ss::showMenu);
2017-07-04 13:38:33 +00:00
return true;
}
2019-01-02 15:22:06 +00:00
if(NUMBERKEY == '0') {
tour::start();
pushScreen(showStartMenu);
return true;
}
return false;
}
2019-08-10 08:57:14 +00:00
EX void checkGoodLand(eLand l) {
2017-07-10 18:47:38 +00:00
if(!showland(l) && texts)
gotoHelp(XLAT(
2019-09-13 16:01:28 +00:00
"This guided tour is different than most other game tutorials -- "
"you are not forced to do anything, and you can go wherever you want.\n\n"
"However, %the1 is not what we are talking about now. "
2018-04-22 09:10:54 +00:00
"We will not explain this land at the moment, and you could potentially "
"get lost there.\n\n"
"Remember that you can get to the next slide by pressing Enter.",
l
) +
2019-09-13 16:01:28 +00:00
XLAT(" This tour will not advance on its own -- you have to press Enter (not while reading help text).")
);
}
2019-08-10 08:57:14 +00:00
EX namespace ss {
EX slide *wts;
EX string current_folder;
2017-07-04 13:38:33 +00:00
string slidechars = "abcdefghijklmnopqrsvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ23456789!@#$%^&*(";
2020-09-11 09:21:52 +00:00
#if HDR
using slideshow_callback = function<void(string, slide*, char)>;
#endif
EX hookset<void(slideshow_callback)> hooks_extra_slideshows;
EX void for_all_slideshows(const slideshow_callback& cb) {
cb(XLAT("HyperRogue Guided Tour"), default_slides, 't');
2020-09-11 09:21:52 +00:00
callhooks(hooks_extra_slideshows, cb);
}
EX void slideshow_menu() {
dialog::init(XLAT("slideshows"), forecolor, 150, 100);
2020-09-11 09:21:52 +00:00
for_all_slideshows([] (string title, slide *sl, char ch) {
dialog::addBoolItem(title, wts == sl, ch);
dialog::add_action([sl] { wts = sl; popScreen(); });
});
dialog::addBack();
dialog::display();
}
2019-08-10 08:57:14 +00:00
EX void showMenu() {
2017-07-04 13:38:33 +00:00
if(!wts) wts = slides;
dialog::init(XLAT("slides"), forecolor, 150, 100);
2021-05-30 10:06:36 +00:00
string cftrans, cftransbuild;
for(char c: current_folder)
if(c == '/') {
cftrans += XLAT(cftransbuild);
cftrans += "/";
cftransbuild = "";
}
else cftransbuild += c;
if(current_folder != "") {
2021-05-30 10:06:36 +00:00
dialog::addTitle(cftrans, 0xFFFFFFFF, 120);
dialog::addItem(XLAT("go up"), 'u');
dialog::add_action([] { current_folder = parent_folder(current_folder); });
dialog::addBreak(100);
}
string last = "";
int key = 0;
for(int i=0; (i==0 || !(wts[i-1].flags & FINALSLIDE)); i++) {
if(!in_folder(wts[i].name, current_folder)) continue;
string sf = get_subname(wts[i].name, current_folder);
if(sf == last) continue;
last = sf;
string sfd;
if(sf.back() == '/') {
sfd = sf.substr(0, isize(sf)-1);
sfd = XLAT(sfd) + "/ ";
}
else sfd = XLAT(sf);
2021-05-30 10:06:36 +00:00
dialog::addBoolItem(sfd, wts == slides && in_folder(slides[currentslide].name, current_folder+sf), slidechars[key++]);
dialog::add_action([i, sf] {
if(sf.back() == '/') {
current_folder += sf;
return;
}
2019-05-28 23:09:38 +00:00
if(gamestack::pushed()) {
gamestack::pop();
2017-07-10 18:47:38 +00:00
presentation(pmGeometryReset);
}
if(slides != wts) {
while(tour::on) restart_game(rg::tour);
2017-07-10 18:47:38 +00:00
slides = wts;
tour::start();
}
presentation(pmStop);
currentslide = i;
2017-07-10 18:47:38 +00:00
popScreenAll();
presentation(pmStart);
2022-08-13 20:44:44 +00:00
if(texts) slidehelp();
});
}
dialog::addBreak(50);
2020-09-11 09:21:52 +00:00
int cnt = 0;
for_all_slideshows([&] (string title, slide *sl, char ch) { cnt++; });
if(cnt > 1) {
dialog::addItem(XLAT("change slideshow"), '1');
dialog::add_action_push(slideshow_menu);
}
dialog::addBack();
dialog::display();
2017-07-04 13:38:33 +00:00
}
2019-08-10 08:57:14 +00:00
EX }
EX void initialize_slides() {
dynamicval<int> cs(currentslide, 0);
for(currentslide=0;; currentslide++) {
presentation(pmStartAll);
if(slides[currentslide].flags & FINALSLIDE) break;
}
}
2020-09-15 17:11:19 +00:00
EX void print() {
dynamicval<int> cs(currentslide, 0);
for(currentslide=0;; currentslide++) {
auto& sl = slides[currentslide];
println(hlog, sl.name);
string str = sl.name;
for(char& c: str) c = '=';
println(hlog, str);
println(hlog);
println(hlog, sl.help);
println(hlog);
if(slides[currentslide].flags & FINALSLIDE) break;
}
}
2017-07-04 13:38:33 +00:00
2019-08-10 08:57:14 +00:00
EX void start() {
currentslide = 0;
pconf.scale = 1;
pconf.alpha = 1;
pmodel = mdDisk;
if(!tour::on) {
initialize_slides();
}
2017-07-04 13:38:33 +00:00
else {
presentation(pmStop);
stop_game();
2017-08-06 12:50:16 +00:00
firstland = specialland = laIce;
2017-07-04 13:38:33 +00:00
}
restart_game(rg::tour);
if(tour::on) {
2022-08-13 20:44:44 +00:00
if(texts) slidehelp();
presentation(pmStart);
}
}
2021-05-30 10:06:36 +00:00
string bog = "Basics of gameplay/";
string shapes = "Hyperbolic shapes/";
string models = "Projections of hyperbolic space/";
string pcg = "Procedural generation/";
2020-03-27 20:47:09 +00:00
/** \brief the default presentation (the Guided Tour) */
2019-08-10 08:57:14 +00:00
EX slide default_slides[] = {
2017-08-06 12:50:16 +00:00
#if ISMOBILE
2020-03-27 19:54:21 +00:00
{"Note for mobiles", 10, LEGAL::NONE | QUICKSKIP,
2019-09-13 16:01:28 +00:00
"This tour is designed for computers, "
2017-08-06 12:50:16 +00:00
"and keys are given for all actions. It will "
"work without a keyboard though, although less "
"comfortably -- just ignore the keys "
"given and select options from MENU.\n\n"
"Select 'next slide' from MENU.",
[] (presmode mode) {
if(mode == pmStartAll) firstland = specialland = laIce;
if(mode == 1) {
2019-09-13 16:01:28 +00:00
if(tour::texts) addMessage(XLAT("Welcome to the HyperRogue Guided Tour!"));
2017-08-06 12:50:16 +00:00
else clearMessages();
}
SHOWLAND( l == laIce );
}
},
#endif
2020-03-27 19:54:21 +00:00
{"Introduction", 10, LEGAL::NONE | QUICKSKIP,
2019-09-13 16:01:28 +00:00
"This tour is mostly aimed to show what is "
"special about the geometry used by HyperRogue. "
"It also shows the basics of gameplay, and "
"how is it affected by geometry.\n\n"
"You decide when you want to stop playing with the "
"current \"slide\" and go to the next one, by pressing Enter. You can also "
"press ESC to see a "
"menu with other options.",
[] (presmode mode) {
2017-08-06 12:50:16 +00:00
if(mode == pmStartAll) firstland = specialland = laIce;
if(mode == 1) {
2019-09-13 16:01:28 +00:00
if(tour::texts) addMessage(XLAT("Welcome to the HyperRogue Guided Tour!"));
else clearMessages();
}
SHOWLAND( l == laIce );
}
},
{bog+"Basics of gameplay", 11, LEGAL::ANY,
"The game starts in the Icy Lands. Collect the Ice Diamonds "
2020-07-12 22:10:16 +00:00
"(press F1 if you do not know how to move or the end goal). "
"After you collect many of them, monsters will start to pose a challenge.\n"
"As is typical in roguelikes and other games based on tactical skill rather "
"than story, if you lose, you have to start a new one from the start. "
"However, in this tutorial, you can simply press '4' "
"to teleport away from a bad situation."
"In general, the tutorial is rigged to show you what it "
"wants -- for example, in this slide, you can press '5' to get "
"lots of Ice Diamonds quickly.",
[] (presmode mode) {
slidecommand = "gain Ice Diamonds";
if(mode == 4)
forCellEx(c2, cwt.at)
forCellEx(c3, c2)
if(c3->wall == waNone && c3->item == itNone && c3->monst == moNone && c3 != cwt.at)
c3->item = itDiamond;
SHOWLAND( l == laIce );
}
},
2020-07-12 22:10:16 +00:00
{bog+"Hyperbolic Soccerball", 23, LEGAL::ANY | USE_SLIDE_NAME,
"New players think that the action of HyperRogue takes place on a sphere. "
2020-07-12 22:10:16 +00:00
"This is not true -- in fact, just the opposite is true!\n\n"
"A sphere can be tiled with hexagons and pentagons (the classic soccer ball); "
"pentagons cause the world to close.\n\n"
"The world of HyperRogue is tiled with hexagons "
"and heptagons; heptagons give extra space!\n\n",
[] (presmode mode) {
2024-05-05 18:15:53 +00:00
setCanvas(mode, &ccolor::football);
2020-07-12 22:10:16 +00:00
if(mode == 5) {
cwt.at->move(0)->monst = moRunDog;
cwt.at->move(1)->monst = moGoblin;
}
SHOWLAND( l == laCanvas );
}
},
{bog+"Hypersian Rug model", 21, LEGAL::HYPERBOLIC,
2017-07-22 23:33:27 +00:00
#if CAP_RUG
2020-07-12 22:10:16 +00:00
"The next slide will show the rendering of the surface HyperRogue "
"actually takes place on.\n\n"
"Use arrow keys to rotate the model, and Page Up/Down to zoom.\n\n"
"If you do not see anything, press '5' to try a safer renderer.",
2017-10-29 22:11:51 +00:00
#else
"This is not true -- the Tutorial in the native desktop version shows "
"the surface HyperRogue actually takes place on.",
#endif
[] (presmode mode) {
2017-07-22 23:33:27 +00:00
#if CAP_RUG
if(mode == 1)
rug::init();
if(mode == 3)
rug::close();
slidecommand = "switch renderer";
if(mode == 4) {
rug::close();
rug::rendernogl = !rug::rendernogl;
rug::init();
}
#endif
SHOWLAND( l == laIce );
}
},
{bog+"Expansion", 22, LEGAL::ANY | USE_SLIDE_NAME,
"The next slide shows the number of cells in distance 1, 2, 3, ... from you. "
"It grows exponentially: there are more than 10^100 cells "
"in radius 1000 around you, and you will move further away during the game!\n\n"
"This is extremely important in the design of HyperRogue. "
"HyperRogue has many navigational puzzles -- what would be simple in Euclidean world "
"is extremely tricky "
"in hyperbolic geometry (you want to reach a specific location 20 cells away, "
"which of the thousands of possible directions should you take?); however, other things virtually impossible in Euclidean "
"world become easy in HyperRogue. "
"HyperRogue had to be specially designed so that it is impossible to grind the "
"infinite world. There are almost no permanent upgrades; collecting treasures "
"brings you benefits, but trying to get too many of the same kind is extremely dangerous.",
[] (presmode mode) {
if(mode == 1) viewdists = true;
if(mode == 2 && !canmove) viewdists = false; // disable when killed
if(mode == 3) viewdists = false;
SHOWLAND( l == laIce );
}
},
{bog+"Tiling and Tactics", 23, LEGAL::ANY | USE_SLIDE_NAME,
"The tactics of fighting simple monsters, such as the Yetis from the Icy Lands, "
"might appear shallow, but hyperbolic geometry is essential even there. "
"In the next slide, you are attacked by two monsters at once. "
"You can make them line up simply by "
"running away in a straight line. "
"Press '2' to try the same in the Euclidean world -- it is impossible.",
[] (presmode mode) {
2024-05-05 18:15:53 +00:00
setCanvas(mode, &ccolor::football);
if(mode == 5) {
cwt.at->move(0)->monst = moRunDog;
cwt.at->move(1)->monst = moGoblin;
}
SHOWLAND( l == laCanvas );
}
},
{shapes+"Straight Lines", 24, LEGAL::ANY,
"Hyperbolic geometry has been discovered by the 19th century mathematicians who "
"wondered about the nature of paralellness. Take a line L and a point A. "
"Can a world exist where there is more than one line passing through A "
"which does not cross L?\n\n"
"The Icy Land will be very dangerous if you have lots of Ice Diamonds -- "
"lots of Yetis and Ice Wolves hunting you! But the other lands, where "
"you have no treasures yet, will still be (relatively) safe.\n\n"
"Wander further, and you should find Crossroads quickly -- "
"the Great Walls are straight lines, and indeed, they work differently than in "
"Euclidean. On the other side of Great Walls, you see other lands -- "
2020-03-25 12:42:38 +00:00
"there are about 60 lands in HyperRogue, based "
"on different mechanics and aspects of hyperbolic geometry.",
[] (presmode mode) {
GETNEXT {
if(isCrossroads(old))
return pick(
pick(laRedRock, laWarpCoast, laMirror),
pick(laLivefjord, laAlchemist, laHell),
pick(laJungle, laDesert, laRose),
pick(laGraveyard, laMotion, laDryForest)
);
else return laCrossroads;
};
SHOWLAND( l == laCrossroads || l == laIce );
}
},
{shapes+"Running Dogs", 25, LEGAL::ANY,
"To learn more about straight lines, "
"wander further, and you should find the Land of Eternal Motion. "
"Try to run in a straight line, with a Running Dog next to you. "
"Even though the Running Dog runs at the same speed as you, "
"it will appear to go slower -- this is because you are running "
"in a straight line, and the Running Dog has to run in a curve "
"called an equidistant.\n\n"
2017-07-22 23:33:27 +00:00
#if ISMAC
"Remember that you can click with right Shift on anything to get more information.",
#else
"Remember that you can right click on anything to get more information.",
#endif
[] (presmode mode) {
GETNEXT {
if(isCrossroads(old)) return pick(laMotion, laNone);
else if(old == laMotion) return laCrossroads;
else return laMotion;
};
SHOWLAND( l == laCrossroads || l == laMotion );
}
},
{shapes+"Equidistants", 27, LEGAL::ANY,
"Equidistants are curves which are at some fixed distance from a "
"straight line. Some lands in HyperRogue are based on equidistants; "
"you should see them after wandering a bit more.\n\n"
"This tutorial gives you freedom to go wherever you choose, "
"but we do not recommend going deep into the Dungeon or the Ocean -- "
"getting back might be difficult.",
[] (presmode mode) {
GETNEXT {
if(isCrossroads(old))
return hrand(100) < 20 ? laNone :
pick(laOcean, laIvoryTower, laDungeon, laEndorian);
else return laCrossroads;
};
SHOWLAND( l == laCrossroads || l == laDungeon || l == laOcean || l == laIvoryTower || l == laEndorian );
}
},
{shapes+"Circles", 26, LEGAL::ANY,
"Circles are strange in hyperbolic geometry too. "
"Look for the Castle of Camelot in the Crossroads; "
"the Round Table inside is a circle of radius 28. "
"Finding its center is a difficult challenge.\n\n"
"Press '5' to cheat by seeing the smaller circles too.\n\n"
"Note: Camelot and some other lands are unlocked earlier in the Tutorial than in a real game.",
[] (presmode mode) {
slidecommand =
camelotcheat ? XLAT("enable the Camelot cheat")
: XLAT("disable the Camelot cheat");
if(mode == 4)
camelotcheat = !camelotcheat;
GETNEXT {
if(!isCrossroads(old)) return laCrossroads;
return laNone;
};
QUICKFIND {
return (l == laCamelot);
};
SHOWLAND( l == laCrossroads || l == laCamelot );
}
},
{shapes+"Horocycles", 28, LEGAL::ANY,
"Horocycles are similar to circles, but you cannot reach their center at all -- "
"they can be understood as limit circles of infinite radius centered in some point "
"in infinity (also called an ideal point).\n\n"
"Go to R'Lyeh, and you should quickly find a Temple of Cthulhu there. "
"Each circle of columns is actually a horocycle. Horocycles in a given "
"temple are concentric, and there is an infinite number of them.",
[] (presmode mode) {
GETNEXT {
if(isCrossroads(old))
return pick(laRlyeh, laNone, laNone);
else return pick(laCrossroads, old == laRlyeh ? laNone : laRlyeh);
};
QUICKFIND {
return (l == laTemple);
};
SHOWLAND ( l == laCrossroads || l == laRlyeh || l == laTemple );
}
},
{shapes+"Half-plane model", 47, LEGAL::HYPERBOLIC,
2017-05-29 10:00:59 +00:00
"The game is normally displayed in the so called Poincaré disk model, "
"which is a kind of a map of the infinite hyperbolic world. "
"There are many projections of Earth, but since Earth is curved, "
"all of them have to distort distances or angles in some way -- "
"the same is true in hyperbolic geometry. "
2017-05-29 10:00:59 +00:00
"The next slide shows another model, called the Poincaré upper half-plane model. In this model, "
"horocycles centered at one specific ideal point are drawn as straight lines.",
[] (presmode mode) {
2018-11-01 17:59:25 +00:00
static int smart;
if(mode == 1)
2018-11-01 17:59:25 +00:00
pmodel = mdHalfplane, smart = vid.use_smart_range, vid.use_smart_range = 2;
if(mode == 2)
models::rotation = cwt.at->land == laDungeon ? Id : spin(M_PI);
if(mode == 3) pmodel = mdDisk, models::rotation = Id, vid.use_smart_range = smart;
}
},
2020-03-27 19:54:21 +00:00
{"Curvature", 29, LEGAL::ANY,
"Now, go to the Burial Grounds and find an Orb of the Sword. The Sword appears to "
"always be facing in the same direction whatever you do, and it appears that "
"you have to rotate the sword to excavate the treasures; "
2019-04-11 20:17:56 +00:00
"yet, it is possible to excavate them! You might have already noticed "
"that the world rotates after you move around a loop and return to an old "
"place.\n\n"
"This is related to the fact that the world of HyperRogue is curved, and "
"the sum of angles in a triangle is not equal to 180 degrees.",
[] (presmode mode) {
slidecommand = XLAT("gain Orb of the Sword");
if(mode == 4)
items[itOrbSword] = 90;
GETNEXT {
if(isCrossroads(old))
return pick(laBurial, laNone, laNone);
else return pick(laCrossroads, old == laBurial ? laNone : laBurial);
};
QUICKFIND {
return (l == laBurial && !items[itOrbSword]);
};
SHOWLAND ( l == laRlyeh || l == laCrossroads || l == laBurial );
}
},
{pcg+"Periodic patterns", 30, LEGAL::UNLIMITED | USE_SLIDE_NAME,
"Hyperbolic geometry yields much more interesting periodic patterns "
"than Euclidean.",
[] (presmode mode) {
2024-05-05 18:15:53 +00:00
setCanvas(mode, &ccolor::zebra_stripes);
if(mode == 1)
patterns::displaycodes = true,
patterns::whichPattern = patterns::PAT_ZEBRA;
if(mode == 3)
patterns::displaycodes = false,
patterns::whichPattern = patterns::PAT_NONE;
SHOWLAND ( l == laCanvas );
}
},
{pcg+"Periodic patterns: application", 31, LEGAL::ANY,
"Many lands in HyperRogue are based around periodic patterns. "
"For example, both Zebra and Windy Plains are based on the pattern "
"shown in the previous slide. "
"Such lands often have tree-like nature.",
[] (presmode mode) {
GETNEXT {
if(isCrossroads(old))
return pick(
pick(laWineyard, laEmerald, laPower),
pick(laZebra, laWhirlwind),
laPalace, laNone
);
else return laCrossroads;
};
SHOWLAND ( l == laCrossroads ||
l == laZebra || l == laWhirlwind || l == laPalace || l == laPrairie ||
l == laEmerald || l == laWineyard || l == laPower );
}
},
{pcg+"Fractal landscapes", 32, LEGAL::UNLIMITED | USE_SLIDE_NAME,
"On the following slide, the colors change smoothly in the whole infinite world. "
"Again, this works better than in Euclidean geometry.",
[] (presmode mode) {
2024-05-05 18:15:53 +00:00
setCanvas(mode, &ccolor::landscape);
SHOWLAND ( l == laCanvas );
}
},
{pcg+"Fractal landscapes: application", 33, LEGAL::ANY,
"This is applied in HyperRogue to create landscapes, such as the chasms in the "
"land of Reptiles or the Dragon Chasms, which you should find quickly. "
"Also in the Dragon Chasms, you can find a Baby Tortoise, and try to find "
2017-05-29 10:00:59 +00:00
"a matching adult tortoise in the Galápagos. "
"There are over two millions of species, but since there is so much space in "
"hyperbolic geometry, finding a matching tortoise is possible. The brighter "
2017-05-29 10:00:59 +00:00
"the color in Galápagos is, the more aspects of the tortoises in the given "
"area are matching.",
[] (presmode mode) {
slidecommand = "create a baby tortoise";
if(mode == 4) {
cell *c = cwt.at->move(0);
c->item = itBabyTortoise;
2020-03-16 20:33:37 +00:00
tortoise::babymap[c] = tortoise::getb(c) ^ tortoise::getRandomBits();
}
GETNEXT {
if(old == laDragon) return pick(laTortoise, laTortoise, laCrossroads);
else if(isCrossroads(old))
return pick(laDragon, laReptile, laNone);
return laNone;
};
QUICKFIND {
return l == laTortoise && !items[itBabyTortoise];
};
SHOWLAND ( l == laCrossroads || l == laReptile || l == laDragon || l == laTortoise );
}
},
{models+"Poincaré Ball model", 41, LEGAL::HYPERBOLIC,
2017-05-29 10:00:59 +00:00
"The Poincaré disk model is a model of a hyperbolic *plane* -- you "
"might wonder why are the walls rendered in 3D then.\n\n"
"HyperRogue actually assumes that the floor level is an equidistant surface "
"in a three-dimensional hyperbolic world, and the camera is placed above the "
"plane that the surface is equidistant to (which boils down to showing "
2017-05-29 10:00:59 +00:00
"the floor level in Poincaré disk model).\n\n"
"This is shown on the next slide, in the Poincaré ball model, which is "
"the 3D analog of the Poincaré disk model.",
[] (presmode mode) {
if(mode == 1) pmodel = mdBall;
if(mode == 3) pmodel = mdDisk;
}
},
{models+"Hyperboloid model", 42, LEGAL::ANY,
"Let's see more models of the hyperbolic plane. "
"This model uses a hyperboloid in the Minkowski geometry; "
"it is used internally by HyperRogue.",
[] (presmode mode) {
if(mode == 1) pmodel = mdHyperboloid;
if(mode == 3) pmodel = mdDisk;
}
},
{models+"Beltrami-Klein model", 43, LEGAL::ANY | USE_SLIDE_NAME,
"This model renders straight lines as straight, but it distorts angles.",
[] (presmode mode) {
if(mode == 1 || mode == pmGeometryReset || mode == pmGeometry) pconf.alpha = 0;
if(mode == 3) pconf.alpha = 1;
}
},
{models+"Gans model", 44, LEGAL::ANY | USE_SLIDE_NAME,
"Yet another model, which corresponds to orthographic projection of the "
2017-05-29 10:00:59 +00:00
"sphere. Poincaré disk model, Beltrami-Klein model, and the Gans "
"model are all obtained by looking at either the hyperboloid model or an "
"equidistant surface from various distances.",
[] (presmode mode) {
if(mode == 1 || mode == pmGeometryReset || mode == pmGeometry) pconf.alpha = 400, pconf.scale = 150;
if(mode == 3) pconf.alpha = pconf.scale = 1;
}
},
{models+"Band model", 45, LEGAL::NONEUC | USE_SLIDE_NAME,
"The band model is the hyperbolic analog of the Mercator projection of the sphere: "
"a given straight line is rendered as a straight line, and the rest of the "
"world is mapped conformally, that is, angles are not distorted. "
"Here, we take the straight line connecting your starting point and your "
"current position -- usually the path taken by the player is surprisingly "
"close to a straight line. Press '8' to see this path.\n\n"
"If you want, press '5' to see it rendered as a spiral, although it takes lots of time and "
"memory.",
[] (presmode mode) {
2018-11-01 17:59:25 +00:00
static int smart;
if(mode == 1) pmodel = mdBand, history::create_playerpath(), models::rotation = Id,
2018-11-01 17:59:25 +00:00
smart = vid.use_smart_range, vid.use_smart_range = 2;
if(mode == 3) {
history::clear(), pmodel = mdDisk;
resetview();
drawthemap();
centerpc(INF);
history::includeHistory = false;
2018-11-01 17:59:25 +00:00
vid.use_smart_range = smart;
}
2017-07-22 23:33:27 +00:00
#if CAP_SDL
slidecommand = "render spiral";
if(mode == 4) history::open_filedialog_to_create_image(true);
if(mode == 11) history::create_playerpath();
if(mode == 13) history::clear();
2017-07-04 13:38:33 +00:00
#endif
}
},
2020-03-27 19:54:21 +00:00
/*{"Conformal square model", 46, LEGAL::HYPERBOLIC,
"The world can be mapped conformally to a square too.",
[] (presmode mode) {
if(mode == 1) pmodel = mdPolygonal, polygonal::solve();
if(mode == 3) pmodel = mdDisk;
}
}, */
2017-07-22 23:33:27 +00:00
#if !ISWEB
2020-03-27 19:54:21 +00:00
{"Shoot'em up mode", 52, LEGAL::NONE | USE_SLIDE_NAME,
"In the shoot'em up mode, space and time is continuous. "
"You attack by throwing knives. "
"Very fun with two players!\n\n"
"There are other special modes too which change the gameplay or "
"focus on a particular challenge.",
[] (presmode mode) {
if(mode == 1) {
firstland = cwt.at->land;
2019-05-28 23:09:38 +00:00
gamestack::push();
switch_game_mode(rg::shmup);
start_game();
2019-05-30 15:47:54 +00:00
resetview();
}
if(mode == 3) {
shmup::clearMonsters();
2019-05-28 23:09:38 +00:00
gamestack::pop();
}
}
},
#endif
2020-03-27 19:54:21 +00:00
{"THE END", 99, LEGAL::ANY | FINALSLIDE,
"This tour shows just a small part of what you can see in the world of HyperRogue. "
"For example, "
"hyperbolic mazes are much nicer than their Euclidean counterparts. "
"Have fun exploring!\n\n"
2019-09-13 16:01:28 +00:00
"Press '5' to leave the tour mode.",
[] (presmode mode) {
2019-09-13 16:01:28 +00:00
slidecommand = XLAT("leave the tour mode");
if(mode == 4) restart_game(rg::tour);
}
}
};
2020-03-27 20:47:09 +00:00
/** \brief currently used set of slides */
2019-08-10 08:57:14 +00:00
EX slide *slides = default_slides;
2017-07-10 18:47:38 +00:00
auto a1 = addHook(hooks_frame, 100, [] () { if(tour::on) tour::presentation(tour::pmFrame); });
auto a2 = addHook(hooks_handleKey, 100, handleKeyTour);
auto a3 = addHook(hooks_nextland, 100, [] (eLand l) { return tour::on ? getNext(l) : laNone; });
2019-09-06 07:17:45 +00:00
EX }
#endif
}