// Hyperbolic Rogue -- heads-up display // Copyright (C) 2011-2018 Zeno Rogue, see 'hyper.cpp' for details namespace hr { vector radarpoints; vector radarlines; purehookset hooks_stats; int monsterclass(eMonster m) { if(isFriendly(m) || m == moTortoise) return 1; else if(isMonsterPart(m)) return 2; else return 0; } int glyphclass(int i) { if(i < ittypes) { eItem it = eItem(i); return itemclass(it) == IC_TREASURE ? 0 : 1; } else { eMonster m = eMonster(i-ittypes); return monsterclass(m) == 0 ? 2 : 3; } } int subclass(int i) { if(i < ittypes) return itemclass(eItem(i)); else return monsterclass(eMonster(i-ittypes)); } #define GLYPH_MARKTODO 1 #define GLYPH_MARKOVER 2 #define GLYPH_LOCAL 4 #define GLYPH_IMPORTANT 8 #define GLYPH_NONUMBER 16 #define GLYPH_DEMON 32 #define GLYPH_RUNOUT 64 #define GLYPH_INPORTRAIT 128 #define GLYPH_LOCAL2 256 #define GLYPH_TARGET 512 #define GLYPH_INSQUARE 1024 eGlyphsortorder glyphsortorder; int zero = 0; int& ikmerge(int i) { if(i < ittypes) return items[i]; else if(i == ittypes) return zero; else return kills[i-ittypes]; } bool ikappear(int i) { if(i == itInventory && inv::on) return true; return ikmerge(i); } const int glyphs = ittypes + motypes; int gfirsttime[glyphs], glasttime[glyphs], gcopy[glyphs], ikland[glyphs]; int glyphorder[glyphs]; int glyphphase[glyphs]; int glyph_lastticks; void updatesort() { for(int i=0; i= ittypes) || i == itTerra)) { int a = gp; gp += (ticks - glyph_lastticks); if(a/500 != gp/500) gp = gp/500*500; } } glyph_lastticks = ticks; } void preparesort() { for(int i=0; i gfirsttime[j]; if(glyphsortorder == gsoLastTop) return glasttime[i] > glasttime[j]; if(glyphsortorder == gsoLastBottom) return glasttime[i] < glasttime[j]; if(glyphsortorder == gsoValue) return ikmerge(i) > ikmerge(j); if(glyphsortorder == gsoLand) return ikland[i] < ikland[j]; return 0; } int glyphflags(int gid) { int f = 0; if(gid < ittypes) { eItem i = eItem(gid); if(itemclass(i) == IC_NAI) f |= GLYPH_NONUMBER; if(isElementalShard(i)) { f |= GLYPH_LOCAL | GLYPH_INSQUARE; if(i == localshardof(cwt.at->land)) f |= GLYPH_LOCAL2; } if(i == treasureType(cwt.at->land) || daily::on) f |= GLYPH_LOCAL | GLYPH_LOCAL2 | GLYPH_IMPORTANT | GLYPH_INSQUARE; if(i == itHolyGrail) { if(items[i] >= 3 && !inv::on) f |= GLYPH_MARKOVER; } else if(itemclass(i) == IC_TREASURE) { if(items[i] >= 25 && items[i] < 100 && !inv::on) f |= GLYPH_MARKOVER; else if(items[i] < 10) f |= GLYPH_MARKTODO; } else { f |= GLYPH_IMPORTANT | GLYPH_INSQUARE; if(itemclass(i) == IC_ORB && items[i] < 10) f |= GLYPH_RUNOUT; } if(i == orbToTarget) f |= GLYPH_TARGET; f |= GLYPH_INPORTRAIT; } else { eMonster m = eMonster(gid-ittypes); if(m == moLesser) f |= GLYPH_IMPORTANT | GLYPH_DEMON | GLYPH_INPORTRAIT | GLYPH_INSQUARE; int isnat = isNative(cwt.at->land, m); if(isnat) f |= GLYPH_LOCAL | GLYPH_IMPORTANT | GLYPH_INPORTRAIT | GLYPH_INSQUARE; if(isnat == 2) f |= GLYPH_LOCAL2; if(m == monsterToSummon) f |= GLYPH_TARGET; } return f; } bool graphglyph() { // if(DIM == 3) return false; return vid.graphglyph == 2 || (vid.graphglyph == 1 && vid.monmode); } bool displayglyph(int cx, int cy, int buttonsize, char glyph, color_t color, int qty, int flags, int id) { bool b = mousex >= cx && mousex < cx+buttonsize && mousey >= cy-buttonsize/2 && mousey <= cy-buttonsize/2+buttonsize; int glsize = buttonsize; if(glyph == '%' || glyph == 'M' || glyph == 'W') glsize = glsize*4/5; int d = ticks - glasttime[id]; double zoom = (d <= 250 && d >= 0) ? 1.25 - .001 * d : 1; glsize = int(glsize * zoom); if(graphglyph()) { initquickqueue(); if(id >= ittypes) { eMonster m = eMonster(id - ittypes); double bsize = buttonsize * 2/3; if(m == moKrakenH) bsize /= 3; if(m == moKrakenT || m == moDragonTail) bsize /= 2; if(m == moSlime) bsize = (2*bsize+1)/3; transmatrix V = atscreenpos(cx+buttonsize/2, cy, bsize*zoom); if(isWorm(m) && cgi.wormscale != 1) for(int i=0; i> 2; drawMonsterType(m, NULL, V, mcol, glyphphase[id]/500.0, NOCOLOR); } else { eItem it = eItem(id); double bsize = buttonsize / 2; if(glyph =='*') bsize *= 2; if(glyph == '$') bsize = (bsize*5+2)/3; if(glyph == 'o') bsize = (bsize*3+1)/2; if(glyph == 't') bsize = bsize*5/2; if(glyph == '(') bsize = bsize*2.5; if(it == itWarning) bsize *= 2; if(it == itBombEgg || it == itTrollEgg || it == itDodeca) bsize = bsize*3/2; int icol = color; icol -= (color & 0xFCFCFC) >> 2; int ic = itemclass(it); bsize = bsize * zoom; transmatrix V = atscreenpos(cx+buttonsize/2, cy, bsize); double t = (ic == IC_ORB || ic == IC_NAI) ? ticks*2 : ((glyph == 't' && qty%5) || it == itOrbYendor) ? ticks/2 : it == itTerra ? glyphphase[id] * 3 * M_PI + 900 * M_PI: glyphphase[id] * 2; drawItemType(it, NULL, V, icol, t, false); } quickqueue(); } else if(glyph == '*') displaychr(cx + buttonsize/2, cy+buttonsize/4, 0, glsize*3/2, glyph, darkenedby(color, b?0:1)); else displaychr(cx + buttonsize/2, cy, 0, glsize, glyph, darkenedby(color, b?0:1)); string fl = ""; string str = its(qty); if(flags & GLYPH_TARGET) fl += "!"; if(flags & GLYPH_LOCAL2) fl += "+"; else if(flags & GLYPH_LOCAL) fl += "-"; if(flags & GLYPH_DEMON) fl += "X"; if(flags & GLYPH_MARKOVER) str += "!"; if(fl != "") displaystr(cx + buttonsize, cy-buttonsize/2 + buttonsize/4, 0, buttonsize/2, fl, darkenedby(color, 0), 16); if(flags & GLYPH_NONUMBER) str = ""; int bsize = (qty < 10 && (flags & (GLYPH_MARKTODO | GLYPH_RUNOUT))) ? buttonsize*3/4 : qty < 100 ? buttonsize / 2 : buttonsize / 3; if(str != "") displayfr(cx + buttonsize, cy + buttonsize/2 - bsize/2, 1, bsize, str, color, 16); return b; } void displayglyph2(int cx, int cy, int buttonsize, int i) { char glyph = i < ittypes ? iinf[i].glyph : minf[i-ittypes].glyph; color_t color = i < ittypes ? iinf[i].color : minf[i-ittypes].color; int imp = glyphflags(i); if(displayglyph(cx, cy, buttonsize, glyph, color, ikmerge(i), imp, i)) { instat = true; getcstat = SDLK_F1; if(i < ittypes) { eItem it = eItem(i); int t = itemclass(it); if(t == IC_TREASURE) mouseovers = XLAT("treasure collected: %1", it); if(t == IC_OTHER) mouseovers = XLAT("objects found: %1", it); if(t == IC_NAI) mouseovers = XLATT1(it); if(t == IC_ORB) mouseovers = XLAT("orb power: %1", eItem(i)); if(it == itGreenStone) { mouseovers += XLAT(" (click to drop)"); getcstat = 'g'; } if(it == itStrongWind) { mouseovers += XLAT(" (click to use)"); getcstat = 't'; } if(it == itInventory) { mouseovers += XLAT(" (click to use)"); getcstat = 'i'; } if(imp & GLYPH_LOCAL) mouseovers += XLAT(" (local treasure)"); help = generateHelpForItem(it); } else { eMonster m = eMonster(i-ittypes); if(isMonsterPart(m)) mouseovers = s0 + XLAT("parts destroyed: %1", m); else if(isFriendly(m) && isNonliving(m)) mouseovers = s0 + XLAT("friends destroyed: %1", m); else if(isFriendly(m)) mouseovers = s0 + XLAT("friends killed: %1", m); else if(isNonliving(m)) mouseovers = s0 + XLAT("monsters destroyed: %1", m); else if(m == moTortoise) mouseovers = s0 + XLAT("animals killed: %1", m); else mouseovers = s0 + XLAT("monsters killed: %1", m); if(imp & GLYPH_LOCAL2) mouseovers += XLAT(" (killing increases treasure spawn)"); else if(imp & GLYPH_LOCAL) mouseovers += XLAT(" (appears here)"); help = generateHelpForMonster(m); } } } bool nohud, nomenukey; hookset *hooks_prestats; #if CAP_SHAPES void drawMobileArrow(int i) { int dir = i; cell *c = cwt.at->move(i); if(!c) return; transmatrix T; if(!compute_relamatrix(c, cwt.at, i, T)) return; // color_t col = getcs().uicolor; // col -= (col & 0xFF) >> 1; bool invalid = !legalmoves[dir]; color_t col = cellcolor(c); if(col == OUTLINE_NONE) col = 0xC0C0C0FF; col -= (col & 0xFF) >> 1; if(invalid) col -= (col & 0xFF) >> 1; if(invalid) col -= (col & 0xFF) >> 1; poly_outline = OUTLINE_DEFAULT; // transmatrix m2 = Id; ld scale = vid.mobilecompasssize * (sphere ? 7 : euclid ? 6 : 5); // m2[0][0] = scale; m2[1][1] = scale; m2[2][2] = 1; transmatrix U = ggmatrix(cwt.at); hyperpoint H = sphereflip * tC0(U); transmatrix Centered = sphereflip * rgpushxto0(H); hyperpoint P = inverse(Centered) * U * T * C0; double alpha = atan2(P[1], P[0]); using namespace shmupballs; double dx = xmove + rad*(1+SKIPFAC-.2)/2 * cos(alpha); double dy = yb + rad*(1+SKIPFAC-.2)/2 * sin(alpha); queuepolyat(atscreenpos(dx, dy, scale) * spin(-alpha), cgi.shArrow, col, PPR::MOBILE_ARROW); } #endif bool nofps = false; void draw_radar(bool cornermode) { if(dual::split([] { dual::in_subscreen([] { calcparam(); draw_radar(false); }); })) return; bool d3 = WDIM == 3; bool hyp = hyperbolic; bool sph = sphere; bool scompass = nonisotropic; dynamicval g(geometry, gEuclid); dynamicval pm(pmodel, mdUnchanged); dynamicval ga(vid.always3, false); initquickqueue(); int rad = vid.radarsize; if(dual::state) rad /= 2; ld cx = dual::state ? (dual::currently_loaded ? vid.xres/2+rad+2 : vid.xres/2-rad-2) : cornermode ? rad+2 : vid.xres-rad-2; ld cy = vid.yres-rad-2 - vid.fsize; for(int i=0; i<360; i++) curvepoint(atscreenpos(cx-cos(i * degree)*rad, cy-sin(i*degree)*rad, 1) * C0); queuecurve(0xFFFFFFFF, 0x000000FF, PPR::ZERO); ld alpha = 15 * degree; ld co = cos(alpha); ld si = sin(alpha); if(sph && !d3) { for(int i=0; i<360; i++) curvepoint(atscreenpos(cx-cos(i * degree)*rad, cy-sin(i*degree)*rad*si, 1) * C0); queuecurve(0, 0x200000FF, PPR::ZERO); } if(d3) { for(int i=0; i<360; i++) curvepoint(atscreenpos(cx-cos(i * degree)*rad, cy-sin(i*degree)*rad*si, 1) * C0); queuecurve(0xFF0000FF, 0x200000FF, PPR::ZERO); curvepoint(atscreenpos(cx-sin(vid.fov*degree/2)*rad, cy-sin(vid.fov*degree/2)*rad*si, 1) * C0); curvepoint(atscreenpos(cx, cy, 1) * C0); curvepoint(atscreenpos(cx+sin(vid.fov*degree/2)*rad, cy-sin(vid.fov*degree/2)*rad*si, 1) * C0); queuecurve(0xFF8000FF, 0, PPR::ZERO); } if(d3) for(auto& r: radarpoints) { queueline(atscreenpos(cx+rad * r.h[0], cy - rad * r.h[2] * si + rad * r.h[1] * co, 0)*C0, atscreenpos(cx+rad*r.h[0], cy - rad*r.h[2] * si, 0)*C0, r.line, -1); } if(scompass) { auto compassdir = [&] (char dirname, hyperpoint h) { using namespace hyperpoint_vec; h = nisot::local_perspective * h * .8; queueline(atscreenpos(cx+rad * h[0], cy - rad * h[2] * si + rad * h[1] * co, 0)*C0, atscreenpos(cx+rad*h[0], cy - rad*h[2] * si, 0)*C0, 0x80400020, -1); displaychr(int(cx+rad * h[0]), int(cy - rad * h[2] * si + rad * h[1] * co), 0, 8, dirname, 0x804000); }; compassdir('E', point3(+1, 0, 0)); compassdir('N', point3(0, +1, 0)); compassdir('W', point3(-1, 0, 0)); compassdir('S', point3(0, -1, 0)); compassdir('U', point3(0, 0,+1)); compassdir('D', point3(0, 0,-1)); } auto locate = [&] (hyperpoint h) { if(sph) return point3(cx + (rad-10) * h[0], cy + (rad-10) * h[2] * si + (rad-10) * h[1] * co, +h[1] * si > h[2] * co ? 8 : 16); else if(hyp) return point3(cx + rad * h[0], cy + rad * h[1], 1/(1+h[3]) * cgi.scalefactor * current_display->radius / (inHighQual ? 10 : 6)); else return point3(cx + rad * h[0], cy + rad * h[1], rad * cgi.scalefactor / (vid.radarrange + cgi.scalefactor/4) * 0.8); }; for(auto& r: radarlines) { hyperpoint h1 = locate(r.h1); hyperpoint h2 = locate(r.h2); h1 = tC0(atscreenpos(h1[0], h1[1], 1)); h2 = tC0(atscreenpos(h2[0], h2[1], 1)); queueline(h1, h2, r.line, -1); } quickqueue(); glflush(); for(auto& r: radarpoints) { if(d3) displaychr(int(cx + rad * r.h[0]), int(cy - rad * r.h[2] * si + rad * r.h[1] * co), 0, 8, r.glyph, r.color); else { hyperpoint h = locate(r.h); displaychr(int(h[0]), int(h[1]), 0, int(h[2]), r.glyph, r.color); } } } void drawStats() { if(nohud || vid.stereo_mode == sLR) return; if(callhandlers(false, hooks_prestats)) return; if(viewdists && show_distance_lists) expansion.view_distances_dialog(); if(current_display->sidescreen) return; first_cell_to_draw = true; bool h = hide_player(); { dynamicval pm(pmodel, DIM == 3 ? mdFlatten : mdDisk); // dynamicval v(vid, vid); // vid.alpha = vid.scale = 1; dynamicval va(vid.alpha, 1); dynamicval vs(vid.scale, 1); dynamicval vc(vid.camera_angle, 0); calcparam(); bool cornermode = (vid.xres > vid.yres * 85/100 && vid.yres > vid.xres * 85/100); if(vid.radarsize > 0 && h) #if CAP_RACING if(!racing::on) #endif if(!peace::on) if(!(cmode & sm::MISSION)) draw_radar(cornermode); if(haveMobileCompass()) { initquickqueue(); using namespace shmupballs; calc(); #if CAP_QUEUE queuecircle(xmove, yb, rad, 0xFF0000FF); queuecircle(xmove, yb, rad*SKIPFAC, legalmoves[MAX_EDGE] ? 0xFF0000FF : 0xFF000080 ); #endif #if CAP_SHAPES for(int i=0; itype; i++) drawMobileArrow(i); #endif if(hypot(mousex-xmove, mousey-yb) <= rad) getcstat = '-'; quickqueue(); } if(racing::on) #if CAP_RACING racing::drawStats(); #else {} #endif else if(cornermode) { int bycorner[4]; for(int u=0; u<4; u++) bycorner[u] = 0; for(int i=0; i rad) { spots++; } if(spots >= bycorner[cor] && spots >= 3) { int next = 0; vector glyphstoshow; for(int i=0; i rad) { if(next >= isize(glyphstoshow)) break; int cx = u; int cy = v + s/2; if(cor&1) cx = vid.xres-1-s-cx; if(cor&2) cy = vid.yres-1-cy; displayglyph2(cx, cy, s, glyphstoshow[next++]); } break; } } } } else { instat = false; bool portrait = vid.xres < vid.yres; int colspace = portrait ? (vid.yres - vid.xres - vid.fsize*3) : (vid.xres - vid.yres - 16) / 2; int rowspace = portrait ? vid.xres - 16 : vid.yres - vid.fsize * (vid.msgleft ? 9 : 4); int colid[4], rowid[4]; int maxbyclass[4]; for(int z=0; z<4; z++) maxbyclass[z] = 0; for(int i=0; i columns) { vid.killreduction++; continue; } coltaken = 0; } colid[z] = coltaken, rowid[z] = 0, coltaken += (maxbyclass[z] + rows-1) / rows; } if(coltaken > columns) { vid.killreduction++; continue; } break; } if(buttonsize <= vid.fsize*3/4) { imponly = true; buttonsize = minsize; rows = rowspace / buttonsize; if(!rows) return; colid[0] = 0; colid[2] = portrait ? 1 : 0; } updatesort(); stable_sort(glyphorder, glyphorder+glyphs, glyphsort); for(int i0=0; i0= rows) rowid[z] = 0, colid[z]++; displayglyph2(cx, cy, buttonsize, i); } } } glflush(); calcparam(); string s0; if(racing::on) { #if CAP_RACING using namespace racing; color_t col; if(ticks >= race_start_tick) col = 0x00FF00; else if(ticks >= race_start_tick - 2000) col = 0xFFFF00; else col = 0xFF0000; for(int i=0; i x(vid.fsize, vid.fsize*2); if(displayButtonS(vid.xres - 8, vid.fsize, racetimeformat(ticks - race_start_tick), col, 16, vid.fsize)) getcstat = 'o'; for(int i=0; i> 8), 16, vid.fsize)) getcstat = 'o'; } else { int comp = get_percentage(i); if(displayButtonS(vid.xres - 8, vid.fsize * (3+2*i), its(comp) + "%", (getcs().uicolor >> 8), 16, vid.fsize)) getcstat = 'o'; } if(displayButtonS(vid.xres - 8, vid.fsize * (4+2*i), fts_fixed(shmup::pc[i]->vel * SCALE * 1000/600, 2), (getcs().uicolor >> 8), 16, vid.fsize)) getcstat = 'o'; } #endif } else if(!peace::on) { if(displayButtonS(vid.xres - 8, vid.fsize, XLAT("score: %1", its(gold())), forecolor, 16, vid.fsize)) { mouseovers = XLAT("Your total wealth"), instat = true, getcstat = SDLK_F1, help = helptitle(XLAT("Your total wealth"), 0xFFD500) + XLAT( "The total value of the treasure you have collected.\n\n" "Every world type contains a specific type of treasure, worth 1 $$$; " "your goal is to collect as much treasure as possible, but every treasure you find " "causes more enemies to hunt you in its native land.\n\n" "Orbs of Yendor are worth 50 $$$ each.\n\n" ); } if(displayButtonS(8, vid.fsize, XLAT("kills: %1", its(tkills())), forecolor, 0, vid.fsize)) { instat = true, getcstat = SDLK_F1, mouseovers = XLAT("Your total kills")+": " + its(tkills()), help = helptitle(XLAT("Your total kills") + ": " + its(tkills()), 0x404040) + XLAT( "In most lands, more treasures are generated with each enemy native to this land you kill. " "Moreover, 100 kills is a requirement to enter the Graveyard and the Hive.\n\n" "Friendly creatures and parts of monsters (such as the Ivy) do appear in the list, " "but are not counted in the total kill count."); } } string vers = VER; if(!nofps) vers += XLAT(" fps: ") + its(calcfps()); #if CAP_MEMORY_RESERVE if(reserve_limit && reserve_count < reserve_limit) { vers += " " + its(reserve_count) + "/" + its(reserve_limit) + " MB"; if(displayButtonS(4, vid.yres - 4 - vid.fsize/2, vers, 0xFF2020, 0, vid.fsize/2)) getcstat = PSEUDOKEY_MEMORY, instat = true; } else #endif if(displayButtonS(4, vid.yres - 4 - vid.fsize/2, vers, 0x202020, 0, vid.fsize/2)) { mouseovers = XLAT("frames per second"), getcstat = SDLK_F1, instat = true, help = helptitle(XLAT("frames per second"), 0xFF4040) + XLAT( "The higher the number, the smoother the animations in the game. " "If you find that animations are not smooth enough, you can try " "to change the options " ) + #if ISIOS XLAT( "(in the MENU). You can reduce the sight range, this should make " "the animations smoother."); #else XLAT( "(press v) and change the wall/monster mode to ASCII, or change " "the resolution."); #endif } glflush(); achievement_display(); callhooks(hooks_stats); } }