mirror of
				https://github.com/zenorogue/hyperrogue.git
				synced 2025-10-31 05:52:59 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			527 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			527 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // Hyperbolic Rogue - basic game routines
 | |
| // Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details
 | |
| 
 | |
| /** \file game.cpp
 | |
|  *  \brief basic game routines
 | |
|  */
 | |
| 
 | |
| #include "hyper.h"
 | |
| namespace hr {
 | |
| 
 | |
| /** \brief the main random number generator for the game.
 | |
|  *  
 | |
|  * All the random calls related to the game mechanics (land generation, AI...) should use hrngen.
 | |
|  *
 | |
|  * Random calls not related to the game mechanics (graphical effects) should not use hrngen.
 | |
|  *
 | |
|  * This ensures that the game should unfold exactly the same if given the same seed and the same input.
 | |
|  */
 | |
| EX std::mt19937 hrngen;
 | |
| 
 | |
| /** \brief initialize \link hrngen \endlink */
 | |
| EX void shrand(int i) {
 | |
|   hrngen.seed(i);
 | |
|   }
 | |
| 
 | |
| /** \brief generate a large number with \link hrngen \endlink */
 | |
| EX int hrandpos() { return hrngen() & HRANDMAX; }
 | |
| 
 | |
| /** \brief A random integer from [0..i), generated from \link hrngen \endlink.
 | |
|  *
 | |
|  *  We are using our own implementations rather than ones from <random>,
 | |
|  *  to make sure that they return the same values on different compilers.
 | |
|  **/
 | |
| 
 | |
| EX int hrand(int i) { 
 | |
|   unsigned d = hrngen() - hrngen.min();
 | |
|   long long m = (long long) (hrngen.max() - hrngen.min()) + 1;
 | |
|   m /= i;
 | |
|   d /= m;
 | |
|   if(d < (unsigned) i) return d;
 | |
|   return hrand(i);
 | |
|   }
 | |
| 
 | |
| #if HDR
 | |
| template<class T, class... U> T pick(T x, U... u) { std::initializer_list<T> i = {x,u...}; return *(i.begin() + hrand(1+sizeof...(u))); }
 | |
| template<class T> void hrandom_shuffle(T* x, int n) { for(int k=1; k<n; k++) swap(x[k], x[hrand(k+1)]); }
 | |
| template<class T> void hrandom_shuffle(T& container) { hrandom_shuffle(container.data(), isize(container)); }
 | |
| template<class U> auto hrand_elt(U& container) -> decltype(container[0]) { return container[hrand(isize(container))]; }
 | |
| template<class T, class U> T hrand_elt(U& container, T default_value) { 
 | |
|   if(container.empty()) return default_value; 
 | |
|   return container[hrand(isize(container))]; 
 | |
|   }
 | |
| #endif
 | |
| 
 | |
| EX vector<int> hrandom_permutation(int qty) {
 | |
|   vector<int> res(qty);
 | |
|   for(int i=0; i<qty; i++) res[i] = i;
 | |
|   hrandom_shuffle(res);
 | |
|   return res;
 | |
|   }
 | |
| 
 | |
| /** Use \link hrngen \endlink to generate a floating point number between 0 and 1.
 | |
|  */
 | |
| 
 | |
| EX ld randf_from(std::mt19937& r) { 
 | |
|   return (r() - r.min()) / (r.max() + 1.0 - r.min());
 | |
|   }
 | |
| 
 | |
| EX ld hrandf() { return randf_from(hrngen); }
 | |
| 
 | |
| /** Returns an integer corresponding to the current state of \link hrngen \endlink.
 | |
|  */
 | |
| EX int hrandstate() {
 | |
|   std::mt19937 r2 = hrngen;
 | |
|   return r2() & HRANDMAX;
 | |
|   }
 | |
| 
 | |
| EX int lastsafety;
 | |
| 
 | |
| EX bool usedSafety = false;
 | |
| EX eLand safetyland;
 | |
| EX int safetyseed;
 | |
| 
 | |
| EX bool childbug = false;
 | |
| 
 | |
| /** Is `w` killed if the part of an ivy `killed` is killed? */
 | |
| EX bool isChild(cell *w, cell *killed) {
 | |
|   if(isAnyIvy(w->monst)) {
 | |
|     int lim = 0;
 | |
|     // printf("w = %p mondir = %d **\n", w, w->mondir);
 | |
|     while(w != killed && w->mondir != NODIR) {
 | |
|       lim++; if(lim == 100000) {
 | |
|         childbug = true;
 | |
|         printf("childbug!\n");
 | |
|         w->item = itBuggy; break; 
 | |
|         }
 | |
|       if(!isAnyIvy(w->monst)) { 
 | |
|         return false;
 | |
|         }
 | |
|       w = w->move(w->mondir);
 | |
|       // printf("w = %p mondir = %d\n", w, w->mondir);
 | |
|       }
 | |
|     
 | |
|     }
 | |
|   return w == killed;
 | |
|   }
 | |
| 
 | |
| EX eMonster active_switch() {
 | |
|   return eMonster(passive_switch ^ moSwitch1 ^ moSwitch2);
 | |
|   }
 | |
| 
 | |
| EX vector<cell*> crush_now, crush_next;
 | |
|   
 | |
| EX void activateFlashFrom(cell *cf, eMonster who, flagtype flags);
 | |
| 
 | |
| EX bool saved_tortoise_on(cell *c) {
 | |
|   return 
 | |
|     (c->monst == moTortoise && c->item == itBabyTortoise &&
 | |
|     !((tortoise::getb(c) ^ tortoise::babymap[c]) & tortoise::mask));
 | |
|   }
 | |
| 
 | |
| EX bool normal_gravity_at(cell *c) {
 | |
|   return !in_gravity_zone(c);
 | |
|   }
 | |
| 
 | |
| EX int countMyGolems(eMonster m) {
 | |
|   int g=0, dcs = isize(dcal);
 | |
|   for(int i=0; i<dcs; i++) {
 | |
|     cell *c = dcal[i];
 | |
|     if(c->monst == m) g++;
 | |
|     }
 | |
|   return g;
 | |
|   }
 | |
| 
 | |
| EX int savePrincesses() {
 | |
|   int g=0, dcs = isize(dcal);
 | |
|   for(int i=0; i<dcs; i++) {
 | |
|     cell *c = dcal[i];
 | |
|     if(isPrincess(c->monst)) princess::save(c);
 | |
|     }
 | |
|   return g;
 | |
|   }
 | |
| 
 | |
| EX int countMyGolemsHP(eMonster m) {
 | |
|   int g=0, dcs = isize(dcal);
 | |
|   for(int i=0; i<dcs; i++) {
 | |
|     cell *c = dcal[i];
 | |
|     if(c->monst == m) g += c->hitpoints;
 | |
|     }
 | |
|   return g;
 | |
|   }
 | |
| 
 | |
| EX void restoreGolems(int qty, eMonster m, int hp IS(0)) {
 | |
|   int dcs = isize(dcal);
 | |
|   for(int i=1; qty && i<dcs; i++) {
 | |
|     cell *c = dcal[i];
 | |
|     if(m == moTameBomberbird ? 
 | |
|         (c->mpdist >= 3 && passable(c, NULL, P_FLYING)) : 
 | |
|         passable(c, NULL, 0)) {
 | |
|       c->hitpoints = hp / qty;
 | |
|       c->monst = m, qty--, hp -= c->hitpoints;
 | |
|       if(m == moPrincess || m == moPrincessArmed)
 | |
|         princess::newFakeInfo(c);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
| EX cellwalker recallCell;
 | |
| EX display_data recallDisplay;
 | |
| 
 | |
| EX bool activateRecall() {
 | |
|   if(!recallCell.at) {
 | |
|     addMessage("Error: no recall");
 | |
|     return false;
 | |
|     }
 | |
|   items[itOrbRecall] = 0; items[itOrbSafety] = 0;
 | |
|   if(!makeEmpty(recallCell.at)) {
 | |
|     addMessage(XLAT("Your Orb of Recall is blocked by something big!"));
 | |
|     recallCell.at = NULL;
 | |
|     return false;
 | |
|     }
 | |
| 
 | |
|   killFriendlyIvy();
 | |
|   movecost(cwt.at, recallCell.at, 3);
 | |
|   playerMoveEffects(movei(cwt.at, recallCell.at, TELEPORT));
 | |
|   mirror::destroyAll();
 | |
|   
 | |
|   sword::reset();
 | |
| 
 | |
|   cwt = recallCell;
 | |
|   recallCell.at = NULL;
 | |
|   flipplayer = true;
 | |
| 
 | |
|   centerover = recallDisplay.precise_center;
 | |
|   View = recallDisplay.view_matrix;
 | |
|   // local_perspective = recallDisplay.local_perspective;
 | |
|   gmatrix = recallDisplay.cellmatrices;
 | |
|   gmatrix0 = recallDisplay.old_cellmatrices;
 | |
|   current_display->which_copy = recallDisplay.which_copy;
 | |
| 
 | |
|   makeEmpty(cwt.at);
 | |
|   forCellEx(c2, cwt.at) 
 | |
|     if(c2->monst != moMutant) 
 | |
|       c2->stuntime = 4;
 | |
|   if(shmup::on) shmup::recall();
 | |
|   if(multi::players > 1) multi::recall();
 | |
|   bfs();
 | |
|   checkmove();
 | |
|   drawSafety();
 | |
|   addMessage(XLAT("You are recalled!"));
 | |
|   return true;
 | |
|   }
 | |
| 
 | |
| EX void saveRecall(cellwalker cw2) {  
 | |
|   if(!recallCell.at) {
 | |
|     changes.value_set(recallCell, cw2);
 | |
|     changes.value_keep(recallDisplay);
 | |
|     recallDisplay = *current_display;
 | |
|     }
 | |
|   }
 | |
| 
 | |
| EX void teleportToLand(eLand l, bool make_it_safe) {
 | |
|   if(recallCell.at && activateRecall()) 
 | |
|     return;
 | |
|   savePrincesses();
 | |
|   int gg = countMyGolems(moGolem);
 | |
|   int gb = countMyGolems(moTameBomberbird);
 | |
|   int gp1 = countMyGolems(moPrincess);
 | |
|   int gp2 = countMyGolems(moPrincessArmed);
 | |
|   int gph1 = countMyGolemsHP(moPrincess);
 | |
|   int gph2 = countMyGolemsHP(moPrincessArmed);
 | |
|   drawSafety();
 | |
|   addMessage(XLAT("You fall into a wormhole!"));
 | |
|   
 | |
|   eLand f = firstland;
 | |
|   if(l == laTemple) l = laRlyeh;
 | |
|   if(l == laClearing) l = laOvergrown;
 | |
|   if(l == laWhirlpool) l = laOcean;
 | |
|   if(l == laCrossroads5) l = laCrossroads2; // could not fit!
 | |
|   if(l == laCamelot && !ls::single())
 | |
|     l = laCrossroads;
 | |
|   firstland = l;
 | |
|   safetyland = l;
 | |
|   safetyseed = hrandpos();
 | |
|   clear_euland(firstland);
 | |
|   safety = make_it_safe; avengers = 0;
 | |
|   clearMemory();
 | |
|   initcells();
 | |
|   initgame();
 | |
|   firstland = f;
 | |
|   safety = false;
 | |
|   restoreGolems(gg, moGolem); 
 | |
|   restoreGolems(gb, moTameBomberbird); 
 | |
|   restoreGolems(gp1, moPrincess, gph1); 
 | |
|   restoreGolems(gp2, moPrincessArmed, gph2); 
 | |
|   restartGraph();  
 | |
|   }
 | |
| 
 | |
| 
 | |
| EX void activateSafety(eLand l) {
 | |
|   teleportToLand(l, true);
 | |
|   #if CAP_SAVE
 | |
|   if(casual) {
 | |
|     saveStats();
 | |
|     savecount++;
 | |
|     save_turns = turncount;
 | |
|     }
 | |
|   #endif
 | |
|   if(items[itOrbChoice]) {
 | |
|     items[itOrbChoice] = 0;
 | |
|     cwt.at->item = itOrbSafety;
 | |
|     }
 | |
|   }
 | |
| 
 | |
| EX void placeGolem(cell *on, cell *moveto, eMonster m) {
 | |
|   if(on->monst == moFriendlyIvy)
 | |
|     killMonster(on, moPlayer);
 | |
|   if(on->monst) {
 | |
|     addMessage(XLAT("There is no room for %the1!", m));
 | |
|     return;
 | |
|     }
 | |
|   if(passable(on, moveto, P_ISFRIEND | (m == moTameBomberbird ? P_FLYING : 0)))
 | |
|     on->monst = m;
 | |
|   else {
 | |
|     on->monst = m;
 | |
|     flagtype f = AF_CRUSH;
 | |
|     if(isFire(on))
 | |
|       addMessage(XLAT("%The1 burns!", m));
 | |
|     else if(on->wall == waChasm)
 | |
|       addMessage(XLAT("%The1 falls!", m)), f = AF_FALL;
 | |
|     else if(isWatery(on) && isNonliving(m))
 | |
|       addMessage(XLAT("%The1 sinks!", m)), f = AF_FALL;
 | |
|     else if(isWatery(on))
 | |
|       addMessage(XLAT("%The1 drowns!", m)), f = AF_FALL;
 | |
|     else if(isWall(on))
 | |
|       addMessage(XLAT("%The1 is crushed!", m));
 | |
|     else if(m == moTameBomberbird && cwt.at->wall == waBoat)
 | |
|       return;
 | |
|     else 
 | |
|       addMessage(XLAT("%The1 is destroyed!", m));
 | |
|     
 | |
|     printf("mondir = %d\n", on->mondir);
 | |
|     fallMonster(cwt.at, f);
 | |
|     }                 
 | |
|   }
 | |
| 
 | |
| EX bool multiRevival(cell *on, cell *moveto) {
 | |
|   int fl = 0;
 | |
|   if(items[itOrbAether]) fl |= P_AETHER;
 | |
|   if(items[itCurseWater]) fl |= P_WATERCURSE;
 | |
|   if(items[itOrbFish]) fl |= P_FISH;
 | |
|   if(items[itOrbWinter]) fl |= P_WINTER;
 | |
|   if(passable(on, moveto, fl)) {
 | |
|     int id = multi::revive_queue[0];
 | |
|     for(int i=1; i<isize(multi::revive_queue); i++)
 | |
|       multi::revive_queue[i-1] = multi::revive_queue[i];
 | |
|     multi::revive_queue.pop_back();
 | |
|     multi::player[id].at = on;
 | |
|     multi::player[id].spin = neighborId(moveto, on);
 | |
|     if(multi::player[id].spin < 0) multi::player[id].spin = 0;
 | |
|     multi::flipped[id] = true;
 | |
|     multi::whereto[id].d = MD_UNDECIDED;
 | |
|     sword::reset();
 | |
|     return true;
 | |
|     }
 | |
|   return false;
 | |
|   }
 | |
| 
 | |
| EX eMonster passive_switch = moSwitch2;
 | |
| 
 | |
| EX void checkSwitch() {
 | |
|   passive_switch = (gold() & 1) ? moSwitch1 : moSwitch2;
 | |
|   }
 | |
| 
 | |
| EX void pushThumper(const movei& mi) {
 | |
|   auto &cto = mi.t;
 | |
|   auto &th = mi.s;
 | |
|   eWall w = th->wall;
 | |
|   if(th->land == laAlchemist)
 | |
|     th->wall = isAlch(cwt.at) ? cwt.at->wall : cto->wall;
 | |
|   else th->wall = waNone;
 | |
|   int explode = 0;
 | |
|   if(cto->wall == waArrowTrap && w == waExplosiveBarrel ) explode = max<int>(cto->wparam, 1);
 | |
|   if(cto->wall == waFireTrap) {
 | |
|     if(w == waExplosiveBarrel)
 | |
|       explode = max<int>(cto->wparam, 1);
 | |
|     if(w == waThumperOn)
 | |
|       explode = 2;
 | |
|     }
 | |
|   if(w == waExplosiveBarrel && cto->wall == waMineMine)
 | |
|     explode = 2;
 | |
|   destroyTrapsOn(cto);
 | |
|   if(cto->wall == waOpenPlate || cto->wall == waClosePlate) {
 | |
|     toggleGates(cto, cto->wall);
 | |
|     addMessage(XLAT("%The1 destroys %the2!", w, cto->wall));
 | |
|     }
 | |
|   if(cellUnstable(cto) && cto->land == laMotion) {
 | |
|     addMessage(XLAT("%The1 falls!", w));
 | |
|     doesFallSound(cto);
 | |
|     }
 | |
|   else if(cellUnstableOrChasm(cto)) {
 | |
|     addMessage(XLAT("%The1 fills the hole!", w));
 | |
|     cto->wall = w == waThumperOn ? waTempFloor : waNone;
 | |
|     cto->wparam = th->wparam;
 | |
|     playSound(cto, "click");
 | |
|     }
 | |
|   else if(isWatery(cto)) {
 | |
|     addMessage(XLAT("%The1 fills the hole!", w));
 | |
|     cto->wall = w == waThumperOn ? waTempBridge : waShallow;
 | |
|     cto->wparam = th->wparam;
 | |
|     playSound(cto, "splash"+pick12());
 | |
|     }
 | |
|   else if(cto->wall == waShallow) {
 | |
|     addMessage(XLAT("%The1 fills the hole!", w));
 | |
|     cto->wall = waNone;
 | |
|     playSound(cto, "splash"+pick12());
 | |
|     }
 | |
|   else if(w == waCrateCrate && cto->wall == waCrateTarget) {
 | |
|     cto->wall = waCrateOnTarget;
 | |
|     th->wall = waNone;
 | |
|     }    
 | |
|   else if(w == waCrateOnTarget && cto->wall == waNone) {
 | |
|     cto->wall = waCrateCrate;
 | |
|     th->wall = waCrateTarget;
 | |
|     }    
 | |
|   else if(w == waCrateOnTarget && cto->wall == waCrateTarget) {
 | |
|     cto->wall = waCrateOnTarget;
 | |
|     th->wall = waCrateTarget;
 | |
|     }
 | |
|   #if CAP_COMPLEX2
 | |
|   else if(isDie(w)) {
 | |
|     th->wall = waNone;
 | |
|     cto->wall = w;    
 | |
|     dice::roll(mi);
 | |
|     if(w == waRichDie && dice::data[cto].happy() > 0) {
 | |
|       cto->wall = waHappyDie;
 | |
|       if(cto->land == laDice && th->land == laDice) {
 | |
|         int q = items[itDice];
 | |
|         gainItem(itDice);
 | |
|         if(vid.bubbles_all || (threshold_met(items[itDice]) > threshold_met(q) && vid.bubbles_threshold)) {
 | |
|           drawBubble(cto, iinf[itDice].color, its(items[itDice]), 0.5);
 | |
|           }
 | |
|         addMessage(XLAT("The die is now happy, and you are rewarded!"));
 | |
|         }
 | |
|       else {
 | |
|         addMessage(XLAT("The die is now happy, but won't reward you outside of the Dice Reserve!"));
 | |
|         }
 | |
|       }          
 | |
|     if(w == waHappyDie && dice::data[cto].happy() <= 0) {
 | |
|       cto->monst = moAngryDie;
 | |
|       cto->wall = waNone;
 | |
|       cto->stuntime = 5;
 | |
|       addMessage(XLAT("You have made a Happy Die angry!"));
 | |
|       animateMovement(mi, LAYER_SMALL);
 | |
|       }          
 | |
|     else
 | |
|       animateMovement(mi, LAYER_BOAT);
 | |
|     }
 | |
|   #endif
 | |
|   else
 | |
|     cto->wall = w;
 | |
|   if(explode) cto->wall = waFireTrap, cto->wparam = explode;
 | |
|   if(cto->wall == waThumperOn)
 | |
|     cto->wparam = th->wparam;
 | |
|   }
 | |
| 
 | |
| EX bool canPushThumperOn(movei mi, cell *player) {
 | |
|   cell *thumper = mi.s;
 | |
|   cell *tgt = mi.t;
 | |
|   #if CAP_COMPLEX2
 | |
|   if(dice::on(thumper) && !dice::can_roll(mi))
 | |
|     return false;
 | |
|   #endif
 | |
|   if(tgt->wall == waBoat || tgt->wall == waStrandedBoat) return false;  
 | |
|   if(isReptile(tgt->wall)) return false;
 | |
|   if(isWatery(tgt) && !tgt->monst)
 | |
|     return true;
 | |
|   if(tgt->wall == waChasm && !tgt->monst)
 | |
|     return true;
 | |
|   return
 | |
|     passable(tgt, thumper, P_MIRROR) &&
 | |
|     passable(tgt, player, P_MIRROR) &&
 | |
|     (!tgt->item || dice::on(thumper));
 | |
|   }
 | |
| 
 | |
| EX void activateActiv(cell *c, bool msg) {
 | |
|   if(msg) addMessage(XLAT("You activate %the1.", c->wall));
 | |
|   if(c->wall == waThumperOff) {
 | |
|     playSound(c, "click");
 | |
|     c->wall = waThumperOn;
 | |
|     }
 | |
|   if(c->wall == waBonfireOff) {
 | |
|     playSound(c, "fire");
 | |
|     c->wall = waFire;
 | |
|     }
 | |
|   c->wparam = 100;
 | |
|   }
 | |
| 
 | |
| /* bool isPsiTarget(cell *dst) {
 | |
|   return 
 | |
|     dst->cpdist > 1 &&
 | |
|     dst->monst && 
 | |
|     !(isWorm(dst) || dst->monst == moShadow);
 | |
|   } */                                                      
 | |
| 
 | |
| EX void fixWormBug(cell *c) {
 | |
|   if(history::includeHistory) return;
 | |
|   printf("worm bug!\n");
 | |
|   if(c->monst == moWormtail) c->monst = moWormwait;
 | |
|   if(c->monst == moTentacletail || c->monst == moTentacleGhost) c->monst = moTentacle;
 | |
|   if(c->monst == moHexSnakeTail) c->monst = moHexSnake;
 | |
|   }
 | |
| 
 | |
| EX bool isWormhead(eMonster m) {
 | |
|   return among(m, 
 | |
|     moTentacle, moWorm, moHexSnake, moWormwait, moTentacleEscaping,
 | |
|     moTentaclewait, moDragonHead);   
 | |
|   }
 | |
| 
 | |
| EX cell *worm_tohead(cell *c) {
 | |
|  for(int i=0; i<c->type; i++)
 | |
|    if(c->move(i) && isWorm(c->move(i)->monst) && c->move(i)->mondir == c->c.spin(i))
 | |
|      return c->move(i);
 | |
|   return nullptr;
 | |
|   }
 | |
| 
 | |
| EX cell *wormhead(cell *c) {
 | |
|   // cell *cor = c;
 | |
|   int cnt = 0;
 | |
|   while(cnt < iteration_limit) {
 | |
|     if(isWormhead(c->monst))
 | |
|       return c;
 | |
|     cell *c1 = worm_tohead(c);
 | |
|     if(!c1) break;
 | |
|     c = c1;
 | |
|     cnt++;
 | |
|     }
 | |
|   fixWormBug(c);
 | |
|   return c;
 | |
|   }  
 | |
|   
 | |
| /** \brief currently works for worms only */
 | |
| EX bool sameMonster(cell *c1, cell *c2) {
 | |
|   if(!c1 || !c2) return false;
 | |
|   if(c1 == c2) return true;
 | |
|   if(isWorm(c1->monst) && isWorm(c2->monst))
 | |
|     return wormhead(c1) == wormhead(c2);
 | |
|   if(isKraken(c1->monst) && isKraken(c2->monst))
 | |
|     return kraken::head(c1) == kraken::head(c2);
 | |
|   return false;
 | |
|   }
 | |
| 
 | |
| EX eMonster haveMount() {
 | |
|   for(cell *pc: player_positions()) {
 | |
|     eMonster m = pc->monst;
 | |
|     if(isWorm(m)) return m;
 | |
|     }
 | |
|   return moNone;
 | |
|   }
 | |
| 
 | |
| EX eMonster otherpole(eMonster m) {
 | |
|   return eMonster(m ^ moNorthPole ^ moSouthPole);
 | |
|   }
 | |
|   
 | |
| 
 | |
| }
 | 
