mirror of
https://github.com/zenorogue/hyperrogue.git
synced 2024-12-24 17:10:36 +00:00
572 lines
18 KiB
C++
572 lines
18 KiB
C++
// work in progress
|
|
|
|
namespace tour {
|
|
|
|
bool on;
|
|
|
|
bool texts = true;
|
|
|
|
string tourhelp;
|
|
|
|
int currentslide;
|
|
|
|
static struct { const char *name; int id; const char *help; } slides[] = {
|
|
{"Introduction", 10,
|
|
"This tutorial 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"
|
|
"Press Enter to go to the next slide, or ESC to see a "
|
|
"menu with other options."
|
|
},
|
|
{"Basics of gameplay", 11,
|
|
"The game starts in the Icy Lands. Collect the Ice Diamonds "
|
|
"(press F1 if you do not know how to move). "
|
|
"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."
|
|
},
|
|
{"Hypersian Rug model", 21,
|
|
"New players think that the action of HyperRogue takes place on a sphere. "
|
|
"This is not true -- the next slide will show 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."
|
|
},
|
|
{"Expansion", 22,
|
|
"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."
|
|
},
|
|
{"Tiling and Tactics", 23,
|
|
"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."
|
|
},
|
|
{"Straight Lines", 24,
|
|
"Hyperbolic geometry has been created by 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"
|
|
"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 -- "
|
|
"there are about 50 lands in HyperRogue, based "
|
|
"on different mechanics and aspects of hyperbolic geometry."
|
|
},
|
|
{"Running Dogs", 25,
|
|
"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."
|
|
},
|
|
{"Equidistants", 27,
|
|
"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."
|
|
},
|
|
{"Circles", 26,
|
|
"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."
|
|
},
|
|
{"Horocycles", 28,
|
|
"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."
|
|
},
|
|
{"Half-plane model", 47,
|
|
"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. "
|
|
"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."
|
|
},
|
|
{"Curvature", 29,
|
|
"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; "
|
|
"yet, it is possible to excavate them! You migth 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."
|
|
},
|
|
{"Periodic patterns", 30,
|
|
"Hyperbolic geometry yields much more interesting periodic patterns "
|
|
"than Euclidean."
|
|
},
|
|
{"Periodic patterns: application", 31,
|
|
"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."
|
|
},
|
|
{"Fractal landscapes", 32,
|
|
"On the following slide, the colors change smoothly in the whole infinite world. "
|
|
"Again, this works better than in Euclidean geometry."
|
|
},
|
|
{"Fractal landscapes: application", 33,
|
|
"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 "
|
|
"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 "
|
|
"the color in Galápagos is, the more aspects of the tortoises in the given "
|
|
"area are matching."
|
|
},
|
|
{"Poincaré Ball model", 41,
|
|
"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 "
|
|
"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."
|
|
},
|
|
{"Hyperboloid model", 42,
|
|
"Let's see more models of the hyperbolic plane. "
|
|
"This model uses a hyperboloid in the Minkowski geometry; "
|
|
"it is used internally by HyperRogue."
|
|
},
|
|
{"Beltrami-Klein model", 43,
|
|
"This model renders straight lines as straight, but it distorts angles."
|
|
},
|
|
{"Gans model", 44,
|
|
"Yet another model, which corresponds to orthographic projection of the "
|
|
"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."
|
|
},
|
|
{"Band model", 45,
|
|
"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."
|
|
},
|
|
{"Conformal square model", 46,
|
|
"The world can be mapped conformally to a square too."
|
|
},
|
|
#ifdef ROGUEVIZ
|
|
{"Collatz conjecture", 51,
|
|
"Your version of HyperRogue includes RogueViz, which "
|
|
"is an adaptation of HyperRogue as a visualization tool "
|
|
"rather than a game. Hyperbolic space is great "
|
|
"for visualizing some kinds of data because of the vast amount "
|
|
"of space.\n\n"
|
|
"The following slide is a visualization of the Collatz conjecture. "
|
|
"Press '5' for a spiral rendering of the Collatz conjecture visualization."},
|
|
#endif
|
|
{"THE END", 99,
|
|
"This is not everything you can see in HyperRogue. For example, "
|
|
"hyperbolic mazes are much fun than their Euclidean counterparts. "
|
|
"Have fun exploring!\n\n"
|
|
"Press '5' to leave the tutorial mode."
|
|
}
|
|
};
|
|
|
|
int getid() {
|
|
if(!on) return 0;
|
|
return slides[currentslide].id;
|
|
}
|
|
|
|
// modes:
|
|
// 1 - enter the slide
|
|
// 2 - each frame
|
|
// 3 - leave the slide
|
|
// 4 - quicken or modify the slide
|
|
// 5 - on initgame
|
|
|
|
void setCanvas(int mode, char canv) {
|
|
static char wc;
|
|
static eLand ld;
|
|
if(mode == 1) {
|
|
wc = mapeditor::whichCanvas;
|
|
mapeditor::whichCanvas = canv;
|
|
ld = firstland;
|
|
firstland = laCanvas;
|
|
restartGame(0, true);
|
|
}
|
|
if(mode == 3) {
|
|
mapeditor::whichCanvas = wc;
|
|
firstland = ld;
|
|
restartGame(0, false);
|
|
}
|
|
}
|
|
|
|
bool sickmode;
|
|
|
|
void presentation(int mode) {
|
|
|
|
int id = getid();
|
|
cheater = 0;
|
|
|
|
if(id && mode == 1) tourhelp = slides[currentslide].name;
|
|
|
|
if(sickmode && !items[itOrbTeleport]) items[itOrbTeleport] = 1;
|
|
|
|
if(id == 10 && mode == 1) {
|
|
if(tour::texts) addMessage(XLAT("Welcome to the HyperRogue tutorial!"));
|
|
else clearMessages();
|
|
}
|
|
|
|
if(id == 11 && mode == 4)
|
|
forCellEx(c2, cwt.c)
|
|
forCellEx(c3, c2)
|
|
if(c3->wall == waNone && c3->item == itNone && c3->monst == moNone && c3 != cwt.c)
|
|
c3->item = itDiamond;
|
|
|
|
// Hypersian Rug
|
|
if(id == 21) {
|
|
static int wm, mm;
|
|
if(mode == 1) {
|
|
rug::init();
|
|
wm = vid.wallmode;
|
|
mm = vid.monmode;
|
|
vid.wallmode = 3;
|
|
vid.monmode = 2;
|
|
}
|
|
if(mode == 3) {
|
|
rug::close();
|
|
vid.wallmode = wm;
|
|
vid.monmode = mm;
|
|
}
|
|
if(mode == 4) {
|
|
rug::close();
|
|
rug::rendernogl = !rug::rendernogl;
|
|
rug::init();
|
|
}
|
|
}
|
|
|
|
// Expansion
|
|
|
|
if(id == 22) {
|
|
if(mode == 1) viewdists = true;
|
|
if(mode == 3) viewdists = false;
|
|
}
|
|
|
|
// Tiling and Tactics
|
|
|
|
if(id == 23) {
|
|
setCanvas(mode, 'F');
|
|
if(mode == 5) {
|
|
cwt.c->mov[0]->monst = moRunDog;
|
|
cwt.c->mov[1]->monst = moGoblin;
|
|
}
|
|
}
|
|
|
|
if(id == 26 && mode == 4)
|
|
camelotcheat = !camelotcheat;
|
|
|
|
// Curvature
|
|
if(id == 29) {
|
|
if(mode == 4)
|
|
items[itOrbSword] = 90;
|
|
}
|
|
|
|
if(id == 30) {
|
|
setCanvas(mode, 't');
|
|
if(mode == 1)
|
|
mapeditor::displaycodes = 2,
|
|
mapeditor::whichPattern = 'z';
|
|
if(mode == 3)
|
|
mapeditor::displaycodes = 0,
|
|
mapeditor::whichPattern = 0;
|
|
}
|
|
|
|
if(id == 32)
|
|
setCanvas(mode, 'l');
|
|
|
|
if(id == 33) {
|
|
if(mode == 4) {
|
|
cell *c = cwt.c->mov[0];
|
|
c->item = itBabyTortoise;
|
|
tortoise::babymap[c] = getBits(c) ^ tortoise::getRandomBits();
|
|
}
|
|
}
|
|
|
|
if(id == 41) {
|
|
if(mode == 1) pmodel = mdBall;
|
|
if(mode == 3) pmodel = mdDisk;
|
|
}
|
|
|
|
if(id == 42) {
|
|
if(mode == 1) pmodel = mdHyperboloid;
|
|
if(mode == 3) pmodel = mdDisk;
|
|
}
|
|
|
|
if(id == 43) {
|
|
if(mode == 1) vid.alpha = 0;
|
|
if(mode == 3) vid.alpha = 1;
|
|
}
|
|
|
|
if(id == 44) {
|
|
if(mode == 1) vid.alpha = 400, vid.scale = 150;
|
|
if(mode == 3) vid.alpha = vid.scale = 1;
|
|
}
|
|
|
|
if(id == 45) {
|
|
if(mode == 1) pmodel = mdBand, conformal::create(), conformal::rotation = 0;
|
|
if(mode == 3) {
|
|
conformal::clear(), pmodel = mdDisk;
|
|
resetview();
|
|
drawthemap();
|
|
centerpc(INF);
|
|
}
|
|
if(mode == 4) conformal::createImage(true);
|
|
}
|
|
|
|
if(id == 46) {
|
|
if(mode == 1) pmodel = mdPolygonal, polygonal::solve();
|
|
if(mode == 3) pmodel = mdDisk;
|
|
}
|
|
|
|
if(id == 47) {
|
|
if(mode == 1)
|
|
pmodel = mdHalfplane;
|
|
if(mode == 2)
|
|
conformal::rotation = cwt.c->land == laDungeon ? 0 : 2;
|
|
if(mode == 3) pmodel = mdDisk, conformal::rotation = 0;
|
|
}
|
|
|
|
#ifdef ROGUEVIZ
|
|
if(id == 51) {
|
|
setCanvas(mode, 'd');
|
|
if(mode == 1) {
|
|
rogueviz::dftcolor = 0x206020FF;
|
|
|
|
rogueviz::collatz::s2 = .3;
|
|
rogueviz::collatz::p2 = .5;
|
|
rogueviz::collatz::s3 = -.4;
|
|
rogueviz::collatz::p3 = .4;
|
|
|
|
rogueviz::showlabels = true;
|
|
|
|
rogueviz::on = true;
|
|
gmatrix.clear();
|
|
drawthemap();
|
|
gmatrix0 = gmatrix;
|
|
|
|
rogueviz::collatz::start();
|
|
}
|
|
|
|
if(mode == 4)
|
|
pmodel = mdBand, conformal::create(), conformal::rotation = 0,
|
|
conformal::createImage(true),
|
|
conformal::clear(), pmodel = mdDisk;
|
|
}
|
|
|
|
#endif
|
|
|
|
if(id == 99 && mode == 4)
|
|
restartGame('T');
|
|
}
|
|
|
|
eLand getNext(eLand old) {
|
|
|
|
// Straight Lines
|
|
|
|
int id = getid();
|
|
if(id == 24) {
|
|
if(isCrossroads(old))
|
|
return pick(
|
|
pick(laRedRock, laWarpCoast, laMirror),
|
|
pick(laLivefjord, laAlchemist, laHell),
|
|
pick(laJungle, laDesert, laRose),
|
|
pick(laGraveyard, laMotion, laDryForest)
|
|
);
|
|
else return laCrossroads;
|
|
}
|
|
|
|
// Running Dogs
|
|
|
|
if(id == 25) {
|
|
if(isCrossroads(old)) return pick(laMotion, laNone);
|
|
else if(old == laMotion) return laCrossroads;
|
|
else return laMotion;
|
|
}
|
|
|
|
// Circles
|
|
|
|
if(id == 26) {
|
|
if(!isCrossroads(old)) return laCrossroads;
|
|
// Camelot is a circle
|
|
}
|
|
|
|
// Equidistants
|
|
|
|
if(id == 27) {
|
|
if(isCrossroads(old))
|
|
return hrand(100) < 20 ? laNone :
|
|
pick(laOcean, laIvoryTower, laDungeon, laEndorian);
|
|
else return laCrossroads;
|
|
}
|
|
|
|
// Horocycles
|
|
if(id == 28) {
|
|
if(isCrossroads(old))
|
|
return pick(laRlyeh, laNone, laNone);
|
|
else return pick(laCrossroads, old == laRlyeh ? laNone : laRlyeh);
|
|
}
|
|
|
|
// Curvature
|
|
if(id == 29) {
|
|
if(isCrossroads(old))
|
|
return pick(laBurial, laNone, laNone);
|
|
else return pick(laCrossroads, old == laBurial ? laNone : laBurial);
|
|
}
|
|
|
|
// periodic patterns application
|
|
if(id == 31) {
|
|
if(isCrossroads(old))
|
|
return pick(
|
|
pick(laWineyard, laEmerald, laPower),
|
|
pick(laZebra, laWhirlwind),
|
|
laPalace, laNone
|
|
);
|
|
else return laCrossroads;
|
|
}
|
|
|
|
// fractal landscapes application
|
|
if(id == 33) {
|
|
if(old == laDragon) return pick(laTortoise, laTortoise, laCrossroads);
|
|
else if(isCrossroads(old))
|
|
return pick(laDragon, laReptile, laNone);
|
|
}
|
|
|
|
return laNone;
|
|
|
|
}
|
|
|
|
void slidehelp() {
|
|
if(texts) {
|
|
help =
|
|
helptitle(slides[currentslide].name, 0xFF8000) +
|
|
slides[currentslide].help;
|
|
if(cmode != emHelp)
|
|
lastmode = cmode;
|
|
cmode = emHelp;
|
|
}
|
|
}
|
|
|
|
bool handleKeyTour(int sym, int uni) {
|
|
if(sym == SDLK_RETURN && (cmode != emHelp || getid() == 10)) {
|
|
if(geometry) { restartGame(0, false); return true; }
|
|
if(getid() == 99) return true;
|
|
presentation(3);
|
|
currentslide++;
|
|
slidehelp();
|
|
presentation(1);
|
|
return true;
|
|
}
|
|
if(sym == SDLK_BACKSPACE) {
|
|
if(geometry) { restartGame(0, false); return true; }
|
|
if(currentslide == 0) { slidehelp(); return true; }
|
|
presentation(3);
|
|
currentslide--;
|
|
presentation(1);
|
|
if(cmode == emHelp) slidehelp();
|
|
return true;
|
|
}
|
|
if(sym == '1' || sym == '2' || sym == '3') {
|
|
if(geometry) {
|
|
restartGame(0, false); return true;
|
|
}
|
|
if(sym == '1') targetgeometry = gSphere;
|
|
if(sym == '2') targetgeometry = gEuclid;
|
|
firstland = euclidland = cwt.c->land;
|
|
restartGame(sym == '3' ? '7' : 'g', true);
|
|
return true;
|
|
}
|
|
if(sym == '4') {
|
|
items[itOrbTeleport] = 1;
|
|
canmove = true;
|
|
return true;
|
|
}
|
|
if(sym == '5') {
|
|
presentation(4);
|
|
return true;
|
|
}
|
|
if(sym == '6') {
|
|
sickmode = !sickmode;
|
|
static ld spd;
|
|
if(sickmode == true) {
|
|
spd = vid.sspeed, vid.sspeed = 5;
|
|
addMessage("Static mode enabled.");
|
|
}
|
|
else {
|
|
vid.sspeed = spd;
|
|
addMessage("Static mode disabled.");
|
|
}
|
|
return true;
|
|
}
|
|
if(sym == '7') {
|
|
texts = !texts;
|
|
if(texts) slidehelp();
|
|
else addMessage("Help texts disabled.");
|
|
return true;
|
|
}
|
|
if(sym == '8')
|
|
conformal::includeHistory = !conformal::includeHistory;
|
|
return false;
|
|
}
|
|
|
|
bool quickfind(eLand l) {
|
|
int id = getid();
|
|
if(id == 26 && l == laCamelot) return true;
|
|
if(id == 28 && l == laTemple) return true;
|
|
if(id == 29 && l == laBurial && !items[itOrbSword]) return true;
|
|
if(id == 33 && l == laTortoise && !items[itBabyTortoise]) return true;
|
|
return false;
|
|
}
|
|
|
|
void start() {
|
|
currentslide = 0;
|
|
vid.scale = 1;
|
|
vid.alpha = 1;
|
|
pmodel = mdDisk;
|
|
restartGame('T');
|
|
if(tour::on) {
|
|
presentation(1);
|
|
slidehelp();
|
|
}
|
|
}
|
|
|
|
}
|