1 // Hyperbolic Rogue - Racing
2 // Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details
3 
4 /** \file racing.cpp
5  *  \brief racing mode
6  */
7 
8 #include "hyper.h"
9 namespace hr {
10 
11 /** \brief Racing mode */
12 EX namespace racing {
13 
14 #if CAP_RACING
15 void set_race_configurer();
16 
17 EX bool guiding = false;
18 
19 EX bool on;
20 EX bool player_relative = false;
21 EX bool standard_centering = false;
22 EX bool track_ready;
23 
24 bool official_race = false;
25 
26 int TWIDTH;
27 
28 EX ld race_advance = 0;
29 
30 static const int LENGTH = 250;
31 static const int DROP = 1;
32 
33 EX int ghosts_to_show = 5;
34 EX int ghosts_to_save = 10;
35 
36 struct race_cellinfo {
37   cell *c;
38   int from_track;
39   int completion;
40   int from_start, from_goal;
41   };
42 
43 vector<race_cellinfo> rti;
44 EX vector<cell*> track;
45 map<cell*, int> rti_id;
46 
47 EX int trophy[MAXPLAYER];
48 
49 EX string track_code = "OFFICIAL";
50 
51 transmatrix straight;
52 
53 int race_try;
54 
apply_seed()55 EX void apply_seed() {
56   int s = race_try;
57   for(char c: track_code) s = 713 * s + c;
58   shrand(s);
59   }
60 
61 EX int race_start_tick, race_finish_tick[MAXPLAYER];
62 
63 typedef unsigned char uchar;
64 
frac_to_uchar(ld x)65 uchar frac_to_uchar(ld x) { return uchar(x * 256); }
angle_to_uchar(ld x)66 uchar angle_to_uchar(ld x) { return frac_to_uchar(x / 2 / M_PI); }
67 
uchar_to_frac(uchar x)68 ld uchar_to_frac(uchar x) { return x / 256.; }
spin_uchar(uchar x)69 transmatrix spin_uchar(uchar x) { return spin(uchar_to_frac(x) * 2 * M_PI); }
70 
71 static const ld distance_multiplier = 4;
72 
73 struct ghostmoment {
74   int step, where_id;
75   uchar alpha, distance, beta, footphase;
76   };
77 
78 struct ghost {
79   charstyle cs;
80   int result;
81   int checksum;
82   long long timestamp;
83   vector<ghostmoment> history;
84   };
85 
86 typedef map<eLand, vector<ghost>> raceset;
87 map<pair<string, modecode_t>, raceset> race_ghosts;
88 
89 map<pair<string, modecode_t>, raceset> official_race_ghosts;
90 
ghostset()91 raceset& ghostset() { return race_ghosts[make_pair(track_code, modecode())]; }
oghostset()92 raceset& oghostset() { return official_race_ghosts[make_pair(track_code, modecode())]; }
93 
get_score_in_land(eLand l)94 int get_score_in_land(eLand l) {
95   auto& gh = ghostset();
96   if(!gh.count(l)) return 0;
97   auto& v = gh[l];
98   if(!isize(v)) return 0;
99   return v[0].result;
100   }
101 
102 array<vector<ghostmoment>, MAXPLAYER> current_history;
103 
104 string ghost_prefix = "default";
105 
106 #if CAP_FILES
ghost_filename(string seed,modecode_t mcode)107 string ghost_filename(string seed, modecode_t mcode) {
108   if(ghost_prefix == "default") {
109     #ifdef FHS
110     if(getenv("HOME")) {
111       string s = getenv("HOME");
112       mkdir((s + "/.hyperrogue").c_str(), 0755);
113       mkdir((s + "/.hyperrogue/racing").c_str(), 0755);
114       ghost_prefix = s + "/.hyperrogue/racing/";
115       }
116     #else
117     #if WINDOWS
118     mkdir("racing");
119     #else
120     mkdir("racing", 0755);
121     #endif
122     ghost_prefix = "racing/";
123     #endif
124     }
125   return ghost_prefix + seed + "-" + itsh(mcode) + ".data";
126   }
127 
hread(hstream & hs,ghostmoment & m)128 void hread(hstream& hs, ghostmoment& m) {
129   hread(hs, m.step, m.where_id, m.alpha, m.distance, m.beta, m.footphase);
130   }
131 
hwrite(hstream & hs,const ghostmoment & m)132 void hwrite(hstream& hs, const ghostmoment& m) {
133   hwrite(hs, m.step, m.where_id, m.alpha, m.distance, m.beta, m.footphase);
134   }
135 
hread(hstream & hs,ghost & gh)136 void hread(hstream& hs, ghost& gh) {
137   hread(hs, gh.cs, gh.result, gh.timestamp, gh.checksum, gh.history);
138   }
139 
hwrite(hstream & hs,const ghost & gh)140 void hwrite(hstream& hs, const ghost& gh) {
141   hwrite(hs, gh.cs, gh.result, gh.timestamp, gh.checksum, gh.history);
142   }
143 
read_ghosts(string seed,modecode_t mcode)144 bool read_ghosts(string seed, modecode_t mcode) {
145 
146   if(seed == "OFFICIAL" && mcode == 2) {
147     fhstream f("officials.data", "rb");
148     if(f.f) {
149       hread(f, f.vernum);
150       hread(f, oghostset());
151       }
152     }
153 
154   string fname = ghost_filename(seed, mcode);
155   println(hlog, "trying to read ghosts from: ", fname);
156   fhstream f(fname, "rb");
157   if(!f.f) return false;
158   hread(f, f.vernum);
159   if(f.vernum <= 0xA600) return true; // scores removed due to the possibility of cheating
160   hread(f, ghostset());
161   return true;
162   }
163 
write_ghosts(string seed,int mcode)164 void write_ghosts(string seed, int mcode) {
165   fhstream f;
166   f.f = fopen(ghost_filename(seed, mcode).c_str(), "wb");
167   if(!f.f) throw hstream_exception(); // ("failed to write the ghost file");
168   hwrite(f, f.vernum);
169   hwrite(f, ghostset());
170   }
171 #endif
172 
get_ghostmoment_matrix(ghostmoment & p)173 shiftmatrix get_ghostmoment_matrix(ghostmoment& p) {
174   cell *w = rti[p.where_id].c;
175   transmatrix T = spin_uchar(p.alpha) * xpush(uchar_to_frac(p.distance) * distance_multiplier) * spin_uchar(p.beta);
176   return gmatrix[w] * T;
177   }
178 
fix_cave(cell * c)179 void fix_cave(cell *c) {
180   int v = 0;
181   // if(c->wall == waCavewall) v++;
182   // if(c->wall == waCavefloor) v--;
183   forCellEx(c2, c) {
184     if(c2->wall == waCavewall) v++;
185     if(c2->wall == waCavefloor) v--;
186     }
187   else v--;
188   if(v>0 && c->wall == waCavefloor) c->wall = waCavewall;
189   if(v<0 && c->wall == waCavewall) c->wall = waCavefloor;
190   }
191 
keep_to_crossroads()192 bool keep_to_crossroads() {
193   return specialland == laCrossroads && !(nonisotropic || hybri);
194   }
195 
bad(cell * c2,cell * c)196 bool bad(cell *c2, cell *c) {
197   if(c2->land == laCaves) {
198     forCellEx(c3, c2) fix_cave(c3);
199     fix_cave(c2);
200     }
201   if(!passable(c2, c, P_ISPLAYER)) return true;
202   if((c2->land == laCrossroads) ^ (c->land == laCrossroads) && keep_to_crossroads()) return true;
203   return false;
204   }
205 
rcelldist(cell * c)206 int rcelldist(cell *c) {
207   #if CAP_CRYSTAL
208   if(cryst) return crystal::space_distance(c, currentmap->gamestart());
209   #endif
210   return celldist(c);
211   }
212 
pcelldist(cell * c)213 int pcelldist(cell *c) {
214   #if CAP_CRYSTAL
215   if(cryst) return crystal::precise_distance(c, currentmap->gamestart());
216   #endif
217   if(sl2) return PIU(celldist(c));
218   return celldist(c);
219   }
220 
trackval(cell * c)221 int trackval(cell *c) {
222   int v = rcelldist(c);
223   int bonus = 0;
224   if(c->land != laCrossroads || !keep_to_crossroads())
225   forCellEx(c2, c) {
226     int d = rcelldist(c2) - v;
227     if(d < 0 && bad(c2, c))
228       bonus += 2;
229     if(d == 0 && bad(c2, c))
230       bonus ++;
231     }
232   return v + bonus;
233   }
234 
tie_info(cell * c,int from_track,int comp)235 void tie_info(cell *c, int from_track, int comp) {
236   rti_id[c] = isize(rti);
237   rti.emplace_back(race_cellinfo{c, from_track, comp, -1, -1});
238   }
239 
get_info(cell * c)240 race_cellinfo& get_info(cell *c) {
241   return rti[rti_id.at(c)];
242   }
243 
244 int race_checksum;
245 
246 ld start_line_width;
247 
248 struct hr_track_failure : hr_exception {};
249 
250 int length;
251 
252 bool use_exhaustive_distance;
253 
find_track(cell * start,int sign,int len)254 void find_track(cell *start, int sign, int len) {
255   int dl = 7 - getDistLimit() - genrange_bonus;
256   dl = (8 + dl) / 2;
257   if(WDIM == 3 && dl < 6) dl = 6;
258   cell *goal;
259   map<cell*, cell*> parent;
260   map<int, vector<cell*> > cellbydist;
261   cellbydist[0].push_back(start);
262 
263   int traversed = 0;
264 
265   while(true) {
266     traversed++;
267     if(cellbydist.empty()) {
268       println(hlog, "reset after traversing ", traversed, " width = ", TWIDTH, " length = ", length);
269       throw hr_track_failure();
270       }
271     auto it = cellbydist.end();
272     it--;
273     // if(hrand(100) < 85 && it != cellbydist.begin()) it--;
274     auto& v = it->second;
275     if(v.empty()) { cellbydist.erase(it); continue; }
276     int id = hrand(isize(v));
277     cell *c = v[id];
278     v[id] = v.back(); v.pop_back();
279     if(it->first >= length) {
280       goal = c;
281       break;
282       }
283     setdist(c, dl, parent[c]);
284     forCellEx(c1, c) if(!bad(c1, c) && !parent.count(c1)) {
285       parent[c1] = c;
286       int id;
287       /* if(nil && !nilv::nilperiod[0]) {
288         switch(sign) {
289           case 1: id = c1->master->zebraval - start->master->zebraval; break;
290           case 2: id = start->master->emeraldval - c1->master->emeraldval; break;
291           case 3: id = start->master->zebraval - c1->master->zebraval; break;
292           case 4: id = start->master->emeraldval - c1->master->emeraldval; break;
293           }
294         } */
295       if(use_exhaustive_distance)
296         id = trackval(c1);
297       else if(nil && !quotient) {
298         switch(sign) {
299           case 1: id = c1->master->emeraldval - start->master->emeraldval; break;
300           case 2: case 3: id = start->master->fieldval - c1->master->fieldval; break;
301           case 4: id = start->master->emeraldval - c1->master->emeraldval; break;
302           }
303         }
304       #if CAP_SOLV
305       else if(asonov::in() && asonov::period_z) {
306         auto co = asonov::get_coord(c->master);
307         ld x = szgmod(co[0], asonov::period_xy);
308         ld y = szgmod(co[1], asonov::period_xy);
309         ld z = hypot(x, y);
310         ld maxz = asonov::period_xy ? asonov::period_xy/3 : 1000000000;
311         id = int((log(z) / log(maxz)) * length);
312         }
313       else if(sn::in())
314         id = (start->master->distance - c1->master->distance) * sign;
315       #endif
316       else
317         id = trackval(c1);
318       cellbydist[id].push_back(c1);
319       }
320     }
321 
322   if(use_exhaustive_distance) permanent_long_distances(goal);
323 
324   if(nonisotropic) {
325     vector<cell*> p;
326     while(goal != start) p.push_back(goal), goal = parent[goal];
327     while(!p.empty()) track.push_back(p.back()), p.pop_back();
328     }
329   else
330     track = build_shortest_path(start, goal);
331   }
332 
block_cells(vector<cell * > to_block,function<bool (cell *)> blockbound)333 EX void block_cells(vector<cell*> to_block, function<bool(cell*)> blockbound) {
334   hrandom_shuffle(to_block);
335 
336   for(cell *c: to_block) switch(specialland) {
337     case laIce:
338       c->wall = waIcewall;
339       break;
340 
341     case laHell:
342       c->wall = waSulphur;
343       break;
344 
345     case laJungle: {
346       vector<int> dirs;
347       forCellIdEx(c2, i, c) if(among(c2->monst, moIvyRoot, moIvyWait)) dirs.push_back(i);
348       if(dirs.empty()) c->monst = moIvyRoot;
349       else c->monst = moIvyWait, c->mondir = dirs[hrand(isize(dirs))];
350       break;
351       }
352 
353     case laDeadCaves:
354       if(blockbound(c)) c->wall = waDeadwall;
355       break;
356 
357     case laRedRock:
358       if(blockbound(c)) c->wall = waRed3;
359       break;
360 
361     case laDragon:
362       c->wall = waChasm;
363       break;
364 
365     case laDryForest:
366       if(blockbound(c)) c->wall = waBigTree;
367       break;
368 
369     case laDesert:
370       if(blockbound(c)) c->wall = waDune;
371       break;
372 
373     case laRuins:
374       if(blockbound(c)) c->wall = waRuinWall;
375       break;
376 
377     case laElementalWall:
378       if(blockbound(c)) {
379         if(c->land == laEFire) c->wall = waEternalFire;
380         else if(c->land == laEWater) c->wall = waSea;
381         else if(c->land == laEAir) c->wall = waChasm;
382         else if(c->land == laEEarth) c->wall = waStone;
383         }
384       break;
385 
386     default: break;
387     }
388   }
389 
make_bounded_track(cell * s)390 EX void make_bounded_track(cell *s) {
391 
392   celllister cl(s, 999, 1000000, NULL);
393   for(cell *c: cl.lst)
394     setdist(c, 0, NULL);
395   println(hlog, "cls = ", isize(cl.lst));
396 
397   for(cell *c: cl.lst) {
398     c->item = itNone; c->monst = moNone;
399     if(c->land != laAsteroids) c->wall = waNone;
400     }
401 
402   map<cell*, int> mazetype;
403   track.clear();
404   track.push_back(s);
405   while(true) {
406     cell *last = track.back();
407     mazetype[last] = 1;
408     vector<cell*> choices;
409     forCellCM(c1, last) {
410       if(mazetype[c1] != 0) continue;
411       choices.push_back(c1);
412       }
413     if(choices.empty()) break;
414     cell *nxt = choices[hrand(isize(choices))];
415     for(cell *cc: choices) mazetype[cc] = 2;
416     track.push_back(nxt);
417     }
418   vector<cell*> to_block;
419   for(auto p: mazetype) {
420     if(p.second == 2) to_block.push_back(p.first);
421     }
422   block_cells(to_block, [] (cell *c) { return true; });
423   for(cell *c: to_block) if(among(c->wall, waNone, waInvisibleFloor) && !c->monst) c->wall = waBarrier;
424   }
425 
426 EX bool bounded_track;
427 
generate_track()428 EX void generate_track() {
429 
430   TWIDTH = getDistLimit() - 1;
431   if(TWIDTH == 1) TWIDTH = 2;
432   if(sl2) TWIDTH = 2;
433   TWIDTH += race_try / 8;
434 
435   #if CAP_FILES
436   if(ghostset().empty())
437     read_ghosts(track_code, modecode());
438   #endif
439 
440   track.clear();
441 
442   /*
443   int t = -1;
444   bignum full_id;
445   bool onlychild = true;
446   */
447 
448   cell *s = currentmap->gamestart();
449 
450   if(specialland == laCrossroads) {
451     celllister cl(s, TWIDTH, 1000000, NULL);
452     for(cell *c: cl.lst) c->bardir = NOBARRIERS;
453     }
454 
455   int dl = 7 - getDistLimit() - genrange_bonus;
456 
457   setdist(s, 6, NULL);
458   makeEmpty(s);
459   cview(); // needed for some virtualRebases
460 
461   use_exhaustive_distance = exhaustive_distance_appropriate();
462 
463   if(use_exhaustive_distance)
464     permanent_long_distances(s);
465 
466   bounded_track = false;
467 
468   length = LENGTH;
469   if(WDIM == 3 || weirdhyperbolic) length = max(length - 10 * race_try, 10);
470 
471   if(use_exhaustive_distance) {
472     int maxsd = max_saved_distance(s);
473     println(hlog, "max length = ", maxsd);
474     length = min(length, maxsd * 5/6);
475     }
476 
477   try {
478   if(bounded && !prod && !(cgflags & qHUGE_BOUNDED)) {
479     bounded_track = true;
480     make_bounded_track(s);
481     }
482   else if(use_exhaustive_distance) {
483     find_track(s, 0, length);
484     }
485   else if(asonov::in()) {
486     find_track(s, 0, length);
487     }
488   else if(sol && !asonov::in()) {
489     track.push_back(s);
490     find_track(s, 1, length/4);
491     find_track(track.back(), -1, length-2*(length/4));
492     find_track(track.back(), 1, length/4);
493     }
494   else if(nih) {
495     track.push_back(s);
496     find_track(s, 1, length/2);
497     find_track(track.back(), -1, length/2);
498     }
499   else if(nil) {
500     track.push_back(s);
501     find_track(s, 1, length/4);
502     find_track(track.back(), 2, length/4);
503     find_track(track.back(), 3, length/4);
504     find_track(track.back(), 4, length/4);
505     }
506   else find_track(s, 0, length);
507     }
508   catch(const hr_track_failure&) {
509     race_try++;
510     gamegen_failure = true;
511     return;
512     }
513   catch(const hr_shortest_path_exception&) {
514     addMessage("error: could not build path");
515     gamegen_failure = true;
516     racing::on = false;
517     return;
518     }
519 
520   if(WDIM == 3) dl = 7 - TWIDTH;
521   for(cell *c:track) setdist(c, dl, NULL);
522 
523   if(true) {
524     manual_celllister cl;
525 
526     for(int i=0; i<isize(track); i++) {
527       tie_info(track[i], 0, i);
528       cl.add(track[i]);
529       }
530 
531     int win = isize(track) - DROP;
532 
533     for(int i=0; i<isize(cl.lst); i++) {
534       cell *c = cl.lst[i];
535       auto p = get_info(c);
536       forCellEx(c2, c) if(!rti_id.count(c2)) {
537         tie_info(c2, p.from_track+1, p.completion);
538         cl.add(c2);
539         }
540       if(bounded_track) continue;
541       c->item = itNone;
542       if(c->wall == waMirror || c->wall == waCloud) c->wall = waNone;
543       if(!isIvy(c))
544         c->monst = moNone;
545       if(c->monst == moIvyHead) c->monst = moIvyWait;
546       if(inmirror(c->land))
547         ;
548       else if(p.from_track == TWIDTH) {
549         killMonster(c, moNone, 0);
550         c->wall = waBarrier;
551         c->land = laBarrier;
552         }
553       else if(p.from_track > TWIDTH) {
554         killMonster(c, moNone, 0);
555         c->land = laMemory;
556         c->wall = waChasm;
557         }
558       if(p.completion >= win && p.from_track < TWIDTH) {
559         c->wall = hrand(2) ? waMirror : waCloud;
560         killMonster(c, moNone, 0);
561         }
562       }
563     }
564 
565   int byat[65536];
566   for(int a=0; a<16; a++) byat[a] = 0;
567   for(const auto s: rti) byat[s.from_track]++;
568   for(int a=0; a<16; a++) printf("%d: %d\n", a, byat[a]);
569 
570   if(s->land == laCaves || (s->land == laCrossroads && !keep_to_crossroads())) {
571     set<unsigned> hash;
572     while(true) {
573       unsigned hashval = 7;
574       int id = 0;
575       for(auto s: rti) {
576         fix_cave(s.c);
577         if(s.c->wall == waCavewall)
578           hashval = (3+2*(id++)) * hashval + 1;
579         if(s.c->wall == waCavefloor)
580           hashval = (3+2*(id++)) * hashval + 2;
581         }
582       printf("hashval = %x id = %d\n", hashval, id);
583       if(hash.count(hashval)) break;
584       hash.insert(hashval);
585       }
586     }
587 
588   if(!bounded_track) for(cell *sc: track) {
589     straight = calc_relative_matrix(sc, track[0], C0);
590     if(straight[GDIM][GDIM] > 1e8) break;
591     }
592   straight = rspintox(straight * C0);
593 
594   ld& a = start_line_width;
595   if(WDIM == 2 && !bounded_track) for(a=0; a<10; a += .1) {
596     hyperpoint h = straight * parabolic1(a) * C0;
597     cell *at = s;
598     virtualRebase(at, h);
599     if(!rti_id.count(at) || get_info(at).from_track >= TWIDTH) break;
600     }
601 
602   if(WDIM == 2 && !bounded_track) for(ld cleaner=0; cleaner<a*.75; cleaner += .2) for(int dir=-1; dir<=1; dir+=2) {
603     transmatrix T = straight * parabolic1(cleaner * dir);
604     cell *at = s;
605     virtualRebase(at, T);
606     get_info(at).from_start = 0;
607     for(ld u=0; u<50; u++) {
608       if(at->wall != waBarrier)
609         makeEmpty(at);
610       killMonster(at, moNone, 0);
611       T = T * xpush(.1);
612       virtualRebase(at, T);
613       }
614     }
615 
616   for(auto s: rti) if(s.c->monst == moIvyDead) s.c->monst = moNone;
617 
618   for(int i=0; i<motypes; i++) kills[i] = 0;
619 
620   vector<shiftmatrix> forbidden;
621   for(auto& ghost: ghostset()[specialland])
622     forbidden.push_back(get_ghostmoment_matrix(ghost.history[0]));
623   for(auto& ghost: oghostset()[specialland])
624     forbidden.push_back(get_ghostmoment_matrix(ghost.history[0]));
625 
626   for(int i=0; i<multi::players; i++) trophy[i] = 0;
627 
628   for(int i=0; i<multi::players; i++) {
629     auto who = shmup::pc[i];
630     // this is intentionally not hrand
631 
632     for(int j=0; j<100; j++) {
633       if(WDIM == 3 || bounded_track)
634         who->at = Id; // straight * cspin(0, 2, rand() % 360) * cspin(1, 2, rand() % 360);
635       else
636         who->at = straight * parabolic1(start_line_width * (rand() % 20000 - 10000) / 40000) * spin(rand() % 360);
637       who->base = s;
638       bool ok = true;
639       for(const shiftmatrix& t: forbidden) if(hdist(t*C0, shiftless(who->at) * C0) < 10. / (j+10)) ok = false;
640       if(ok) break;
641       }
642     virtualRebase(who);
643     }
644 
645   if(bounded_track) track.back()->wall = waCloud;
646 
647   if(1) {
648     manual_celllister cl;
649     cl.add(s);
650     bool goal = false;
651     for(int i=0; i<isize(cl.lst); i++) {
652       cell *c = cl.lst[i];
653       forCellEx(c2, c) {
654         if(among(c2->wall, waCloud, waMirror)) goal = true;
655         if(passable(c2, c, P_ISPLAYER))
656           cl.add(c2);
657         }
658       }
659     if(!goal) {
660       printf("error: goal unreachable\n");
661       gamegen_failure = true;
662       race_try++;
663       return;
664       }
665     }
666 
667   if(1) {
668     map<cell*, pair<int, int> > cdists;
669     manual_celllister cl;
670     cl.add(s);
671     for(auto cc: rti) if(cc.from_start == 0) cl.add(cc.c);
672 
673     for(int i=0; i<isize(cl.lst); i++) {
674       cell *c = cl.lst[i];
675       forCellEx(c2, c) {
676         if(passable(c2, c, P_ISPLAYER) && !cl.listed(c2))
677           cl.add(c2), get_info(c2).from_start = get_info(c).from_start + 1;
678         }
679       }
680 
681     }
682 
683   if(1) {
684     map<cell*, pair<int, int> > cdists;
685     manual_celllister cl;
686     for(auto cc: rti) if(among(cc.c->wall, waCloud, waMirror))
687       cc.from_goal = 0, cl.add(cc.c);
688 
689     for(int i=0; i<isize(cl.lst); i++) {
690       cell *c = cl.lst[i];
691       forCellEx(c2, c) {
692         if(passable(c2, c, P_ISPLAYER) && !cl.listed(c2))
693           cl.add(c2), get_info(c2).from_goal = get_info(c).from_goal + 1;
694         }
695       }
696 
697     }
698 
699   int total_track = get_info(s).from_goal;
700 
701   auto blockoff = [&total_track] (race_cellinfo& cc) {
702     if(cc.from_start < 1 || cc.from_goal < 1) return false;
703     int dif = cc.from_start + cc.from_goal - 2 * cc.from_track - total_track;
704     return dif > 3;
705     };
706 
707   vector<cell*> to_block;
708 
709   for(auto cc: rti) if(blockoff(cc)) to_block.push_back(cc.c);
710 
711   block_cells(to_block, [&blockoff] (cell *c) {
712     forCellEx(c2, c) if(passable(c2, c, P_ISPLAYER) && !blockoff(get_info(c2))) return true;
713     return false;
714     });
715 
716 
717   // for(cell *c: to_block) if(blockbound(c)) c->land = laOvergrown;
718 
719   /*
720   for(cell *c: track) {
721     int i = trackval(c) - celldist(c);
722     if(i == 0) c->item = itDiamond;
723     if(i == 1) c->item = itGold;
724     if(i == 2) c->item = itEmerald;
725     if(i == 3) c->item = itSapphire;
726     if(i == 4) c->item = itRuby;
727     if(i >= 5) c->item = itBone;
728     }
729   */
730 
731   track_ready = true;
732   race_checksum = hrand(1000000);
733 
734   race_start_tick = 0;
735   for(int i=0; i<MAXPLAYER; i++) race_finish_tick[i] = 0;
736 
737   official_race = (track_code == "OFFICIAL" && modecode() == 2);
738   if(official_race && isize(oghostset() [specialland]) && race_checksum != oghostset() [specialland] [0].checksum) {
739     official_race = false;
740     addMessage(XLAT("Race did not generate correctly for some reason -- not ranked"));
741     }
742   }
743 
744 bool inrec = false;
745 
746 EX ld race_angle = 90;
747 
force_standard_centering()748 EX bool force_standard_centering() {
749   return nonisotropic || hybri || quotient || bounded;
750   }
751 
use_standard_centering()752 EX bool use_standard_centering() {
753   return standard_centering || force_standard_centering();
754   }
755 
track_matrix(int at,int dir)756 EX transmatrix track_matrix(int at, int dir) {
757   transmatrix res = unshift(ggmatrix(racing::track[at]));
758   while(true) {
759     if(at+dir < 0 || at+dir >= isize(racing::track)) return res;
760     for(int x=0; x<MXDIM; x++) for(int y=0; y<MXDIM; y++)
761       if(abs(res[y][x]) > 10000) return res;
762     cell *cur = racing::track[at];
763     at += dir;
764     cell *next = racing::track[at];
765     int nei = neighborId(cur, next);
766     if(nei == -1) return res;
767     res = res * currentmap->adj(cur, nei);
768     }
769   }
770 
set_view()771 EX bool set_view() {
772 
773   multi::cpid = subscreens::in ? subscreens::current_player : 0;
774 
775   if(race_start_tick == 0) race_start_tick = ticks + 5000;
776 
777   shmup::monster *who = shmup::pc[multi::cpid];
778 
779   if(!inrec) {
780     const transmatrix T = who->at;
781     ld alpha = -atan2(T * C0);
782     ld distance = hdist0(T * C0);
783     ld beta = -atan2(xpush(-distance) * spin(-alpha) * T * Cx1);
784     current_history[multi::cpid].emplace_back(ghostmoment{ticks - race_start_tick, rti_id[who->base],
785       angle_to_uchar(alpha),
786       frac_to_uchar(distance / distance_multiplier),
787       angle_to_uchar(beta),
788       frac_to_uchar(who->footphase)
789       });
790     }
791 
792   if(use_standard_centering()) return false;
793   if(player_relative && specialland == laAsteroids) return false;
794 
795   transmatrix at = ypush(-vid.yshift) * unshift(ggmatrix(who->base)) * who->at;
796 
797   if(racing::player_relative || quotient || (kite::in() && GDIM == 3)) {
798     View = iso_inverse(at) * View;
799     }
800   else {
801     /* this works only in isotropic geometries, but we don't really care about this in 3D */
802     int z = get_info(who->base).completion;
803 
804     transmatrix T1 = track_matrix(z, -1);
805     transmatrix T2 = track_matrix(z, +1);
806 
807     transmatrix iT1 = iso_inverse(T1);
808 
809     transmatrix T = spintox(iT1 * T2 * C0);
810 
811     hyperpoint h = T * iT1 * ypush(vid.yshift) * at * C0;
812     ld y = GDIM == 2 ? asin_auto(h[1]) : asin_auto(hypot(h[1], h[2]));
813     ld x = asin_auto(h[0] / cos_auto(y));
814     x += race_advance;
815     if(GDIM == 3 && race_advance == 0 && pmodel == mdPerspective) race_advance = -1;
816 
817     View = xpush(-x) * T * iT1 * ypush(vid.yshift) * View;
818 
819     if(GDIM == 3) View = cspin(2, 0, M_PI/2) * View;
820     fixmatrix(View);
821     }
822   if(GDIM == 3 && WDIM == 2)
823     View = cspin(0, 1, M_PI) * cspin(2, 1, M_PI/2 + shmup::playerturny[multi::cpid]) * spin(-M_PI/2) * View;
824   else if(GDIM == 2) View = spin(race_angle * degree) * View;
825   return true;
826   }
827 
828 #if CAP_COMMANDLINE
829 void show();
830 
readArgs()831 int readArgs() {
832   using namespace arg;
833 
834   if(0) ;
835   else if(argis("-racing")) {
836     PHASEFROM(2);
837     stop_game();
838     switch_game_mode(rg::racing);
839     }
840   else if(argis("-rsc")) {
841     standard_centering = true;
842     }
843   else if(argis("-rfast")) {
844     PHASEFROM(3);
845     start_game();
846     race_start_tick = 1;
847     }
848   else return 1;
849   return 0;
850   }
851 #endif
852 
853 int tstart, tstop;
854 heptspin sview;
855 
856 auto hook =
857 #if CAP_COMMANDLINE
858   addHook(hooks_args, 100, readArgs)
859 #endif
__anon7cd889710402() 860 + addHook(hooks_clearmemory, 0, []() {
861     track_ready = false;
862     track.clear();
863     rti.clear();
864     rti_id.clear();
865     for(auto &ch: current_history) ch.clear();
866     })
__anon7cd889710502null867 + addHook(hooks_configfile, 100, [] {
868     addsaver(racing::race_advance, "race_advance");
869     addsaver(racing::race_angle, "race_angle");
870     addsaver(racing::ghosts_to_show, "race_ghosts_to_show");
871     addsaver(racing::ghosts_to_save, "race_ghosts_to_save");
872     addsaver(racing::guiding, "race_guiding");
873     addsaver(racing::player_relative, "race_player_relative");
874     addsaver(racing::standard_centering, "race_standard_centering");
875     })
876 // + addHook(hooks_handleKey, 120, akh);
877   ;
878 
879 EX vector<eLand> race_lands = {
880   laHunting,
881   laCrossroads,
882   laJungle,
883   laDesert,
884   laRedRock,
885   laDragon,
886   laMirror,
887   laRuins,
888   laCaves,
889   laWildWest,
890   laIce,
891   laHell,
892   laTerracotta,
893   laElementalWall,
894   laDryForest,
895   laDeadCaves,
896   laAsteroids
897   };
898 
899 vector<string> playercmds_race = {
900   "forward", "backward", "turn left", "turn right",
901   "forward", "backward", "turn left", "turn right",
902   "", "", "",
903   "", "change camera", "", ""
904   };
905 
racetimeformat(int t)906 EX string racetimeformat(int t) {
907   string times = "";
908   int digits = 0;
909   bool minus = (t < 0);
910   if(t < 0) t = -t;
911   while(t || digits < 6) {
912     int mby = (digits == 4 ? 6 : 10);
913     times = char('0'+(t%mby)) + times;
914     t /= mby; digits++;
915     if(digits == 3) times = "." + times;
916     if(digits == 5) times = ":" + times;
917     }
918   if(minus) times = "-" + times;
919   return times;
920   }
921 
922 extern int playercfg;
923 
track_chooser(string new_track)924 void track_chooser(string new_track) {
925   cmode = 0;
926   gamescreen(2);
927   dialog::init(XLAT("Racing"));
928 
929   map<char, eLand> landmap;
930 
931   dynamicval<bool> so(shmup::on, true);
932   dynamicval<bool> ro(racing::on, true);
933 
934   char let = 'a';
935   for(eLand l: race_lands) {
936     auto& ghs = race_ghosts[make_pair(new_track, modecode())];
937     const int LOST = 3600000;
938     int best = LOST;
939     if(ghs.count(l)) {
940       auto& gh = ghs[l];
941       for(auto& gc: gh) best = min(best, gc.result);
942       }
943     string s = (best == LOST) ? "" : racetimeformat(best);
944     landmap[let] = l;
945     dialog::addSelItem(XLAT1(linf[l].name), s, let++);
946     dialog::add_action([l, new_track] () {
947       stop_game();
948       multi::players = playercfg;
949       if(!racing::on) switch_game_mode(rg::racing);
950       track_code = new_track;
951       specialland = l;
952       // because of an earlier issue, some races in the Official track start with race_try = 1
953       race_try = among(l, laCaves, laWildWest, laHell, laTerracotta, laElementalWall, laDryForest, laDeadCaves) ? 1 : 0;
954       start_game();
955       popScreenAll();
956       });
957     }
958 
959   dialog::addBack();
960   dialog::display();
961 
962   if(landmap.count(getcstat) && new_track == "OFFICIAL" && modecode() == 2)
963     displayScore(landmap[getcstat]);
964   }
965 
race_projection()966 void race_projection() {
967   cmode = sm::SIDE | sm::MAYDARK;
968   gamescreen(1);
969 
970   dialog::init(XLAT("racing projections"));
971 
972   dialog::addBoolItem(XLAT("Poincaré disk model"), pmodel == mdDisk && !pconf.camera_angle, '1');
973   dialog::add_action([] () {
974     pmodel = mdDisk;
975     race_advance = 0;
976     vid.yshift = 0;
977     pconf.camera_angle = 0;
978     pconf.xposition = 0;
979     pconf.yposition = 0;
980     pconf.scale = 1;
981     vid.use_smart_range = 0;
982     vid.smart_range_detail = 3;
983     });
984 
985   dialog::addBoolItem(XLAT("band"), pmodel == mdBand, '2');
986   dialog::add_action([] () {
987     pmodel = mdBand;
988     pconf.model_orientation = race_angle;
989     race_advance = 1;
990     vid.yshift = 0;
991     pconf.camera_angle = 0;
992     pconf.xposition = 0;
993     pconf.yposition = 0;
994     pconf.scale = 1;
995     vid.use_smart_range = 1;
996     vid.smart_range_detail = 3;
997     });
998 
999   dialog::addBoolItem(XLAT("half-plane"), pmodel == mdHalfplane, '3');
1000   dialog::add_action([] () {
1001     pmodel = mdHalfplane;
1002     pconf.model_orientation = race_angle + 90;
1003     race_advance = 0.5;
1004     vid.yshift = 0;
1005     pconf.camera_angle = 0;
1006     pconf.xposition = 0;
1007     pconf.yposition = 0;
1008     pconf.scale = 1;
1009     vid.use_smart_range = 1;
1010     vid.smart_range_detail = 3;
1011     });
1012 
1013   dialog::addBoolItem(XLAT("third-person perspective"), pmodel == mdDisk && pconf.camera_angle, '4');
1014   dialog::add_action([] () {
1015     pmodel = mdDisk;
1016     race_advance = 0;
1017     vid.yshift = -0.3;
1018     pconf.camera_angle = -45;
1019     pconf.scale = 18/16. * vid.xres / vid.yres / multi::players;
1020     pconf.xposition = 0;
1021     pconf.yposition = -0.9;
1022     vid.use_smart_range = 1;
1023     vid.smart_range_detail = 3;
1024     });
1025 
1026   if(true) {
1027     dialog::addSelItem(XLAT("point of view"), player_relative ? XLAT("player") : XLAT("track"), 'p');
1028     if(racing::use_standard_centering())
1029       dialog::lastItem().value = XLAT("N/A");
1030     dialog::add_action([] () {
1031       player_relative = !player_relative;
1032       if(pmodel == mdBand || pmodel == mdHalfplane)
1033         pmodel = mdDisk;
1034       if(racing::on)
1035         set_view();
1036       });
1037     }
1038   else dialog::addBreak(100);
1039 
1040   if(GDIM == 2) {
1041     dialog::addSelItem(XLAT("race angle"), fts(race_angle), 'a');
1042     dialog::add_action([] () {
1043       dialog::editNumber(race_angle, 0, 360, 15, 90, XLAT("race angle"), "");
1044       int q = pconf.model_orientation - race_angle;
1045       dialog::reaction = [q] () { pconf.model_orientation = race_angle + q; };
1046       });
1047     }
1048 
1049   dialog::addSelItem(XLAT("show more in front"), fts(race_advance), 'A');
1050   dialog::add_action([] () {
1051     dialog::editNumber(race_advance, 0, 360, 0.1, 1, XLAT("show more in front"), "");
1052     });
1053 
1054   dialog::addBoolItem(XLAT("do not use special centering for racing"), standard_centering, 'C');
1055   if(force_standard_centering()) dialog::lastItem().value = XLAT("N/A");
1056   dialog::add_action([] () {
1057     standard_centering = !standard_centering;
1058     });
1059 
1060   dialog::addBack();
1061   dialog::display();
1062   }
1063 
1064   int playercfg;
1065   bool editing_track;
1066   string new_track;
1067 
1068 /* struct race_configurer { */
1069 
set_race_configurer()1070   void set_race_configurer() { editing_track = false; new_track = track_code; playercfg = multi::players; }
1071 
random_track_name()1072   static string random_track_name() {
1073     string s = "";
1074     for(int a = 0; a < 4; a++)  {
1075       int u = rand() % 2;
1076       if(u == 0)
1077         s += "AEIOUY" [ rand() % 6];
1078       s += "BCDFGHJKLMNPRSTVWZ" [ rand() % 18];
1079       if(u == 1)
1080         s += "AEIOUY" [ rand() % 6];
1081       }
1082     return s;
1083     }
1084 
racecheck(int sym,int uni)1085   static string racecheck(int sym, int uni) {
1086     if(uni >= 'A' && uni <= 'Z') return string("") + char(uni);
1087     if(uni >= 'a' && uni <= 'z') return string("") + char(uni - 32);
1088     return "";
1089     }
1090 
1091   bool alternate = false;
1092 
1093   #if MAXMDIM >= 4
thurston_racing()1094   EX void thurston_racing() {
1095     gamescreen(1);
1096     dialog::init(XLAT("racing in Thurston geometries"));
1097 
1098     dialog::addBreak(100);
1099 
1100     char ch = '1';
1101 
1102     auto add_thurston_race = [&ch] (string caption, reaction_t launcher) {
1103       dialog::addBigItem(caption, ch++);
1104       dialog::add_action([=] { stop_game();
1105         if(!racing::on) switch_game_mode(rg::racing);
1106         racing::standard_centering = true;
1107         launcher();
1108         track_code = "OFFICIAL";
1109         start_game();
1110         popScreenAll();
1111         });
1112       dialog::addBreak(50);
1113       };
1114 
1115     if(!alternate) {
1116       add_thurston_race(XLAT("Euclidean"), [] { stop_game(); euc::clear_torus3(); set_geometry(gBitrunc3); });
1117       add_thurston_race(XLAT("hyperbolic"), [] { set_geometry(gBinary3); vid.texture_step = 4; });
1118       add_thurston_race(XLAT("spherical"), [] { set_geometry(gCell120); });
1119       #if CAP_SOLV
1120       add_thurston_race(XLAT("Solv geometry"), [] { sn::solrange_xy = 10; sn::solrange_z = 3; set_geometry(gSol); });
1121       #endif
1122       add_thurston_race(XLAT("S2xE"), [] { set_geometry(gSphere); set_variation(eVariation::bitruncated); set_geometry(gProduct); });
1123       add_thurston_race(XLAT("H2xE"), [] { set_geometry(gNormal); set_variation(eVariation::bitruncated); set_geometry(gProduct); });
1124       add_thurston_race(XLAT("Nil"), [] { stop_game(); nilv::nilperiod[0] = 0; set_geometry(gNil); });
1125       add_thurston_race(XLAT("PSL(2,R)"), [] { set_geometry(gNormal); set_variation(eVariation::pure); set_geometry(gRotSpace); });
1126       }
1127     else {
1128       #if CAP_SOLV
1129       add_thurston_race(XLAT("stretched hyperbolic"), [] { set_geometry(gNIH); vid.texture_step = 4; });
1130       add_thurston_race(XLAT("stretched Solv"), [] { set_geometry(gSolN); sn::solrange_xy = 10; sn::solrange_z = 3; vid.texture_step = 4; });
1131       add_thurston_race(XLAT("periodic Solv"), [] { stop_game(); sn::solrange_xy = 5; sn::solrange_z = 2; asonov::period_xy = 8; asonov::period_z = 0; asonov::set_flags(); set_geometry(gArnoldCat); });
1132       #endif
1133       add_thurston_race(XLAT("hyperbolic crystal"), [] { set_geometry(gCrystal344); vid.texture_step = 4; });
1134       add_thurston_race(XLAT("torus x E"), [] { stop_game(); euc::eu_input = euc::torus3(4, 4, 0); set_geometry(gCubeTiling); });
1135       add_thurston_race(XLAT("hyperbolic regular"), [] { set_geometry(gSpace534); });
1136       add_thurston_race(XLAT("S2xE regular"), [] { set_geometry(gSphere); set_variation(eVariation::pure); set_geometry(gProduct); });
1137       add_thurston_race(XLAT("H2xE regular"), [] { set_geometry(gNormal); set_variation(eVariation::pure); set_geometry(gProduct); });
1138       add_thurston_race(XLAT("periodic Nil"), [] { stop_game(); nilv::nilperiod[0] = 3; set_geometry(gNil); });
1139       }
1140     dialog::addBoolItem_action(XLAT("alternate versions"), alternate, 'x');
1141 
1142     dialog::addBreak(100);
1143     dialog::addBack();
1144     dialog::display();
1145     }
1146   #endif
1147 
raceconfigurer()1148   void raceconfigurer() {
1149 
1150     gamescreen(1);
1151 
1152     dialog::init(XLAT("Racing"));
1153 
1154     if(false)
1155       dialog::addInfo(XLAT("Racing available only in unbounded worlds."), 0xFF0000);
1156     else {
1157       dialog::addItem(XLAT("select the track and start!"), 's');
1158       dialog::add_action([/*this*/] () {
1159         dynamicval<bool> so(shmup::on, true);
1160         dynamicval<bool> ro(racing::on, true);
1161         #if CAP_FILES
1162         if(race_ghosts[make_pair(new_track, modecode())].empty())
1163           read_ghosts(new_track, modecode());
1164         else
1165           println(hlog, "known ghosts: ", isize(race_ghosts[make_pair(new_track, modecode())]));
1166         #endif
1167         pushScreen([/*this*/] () { track_chooser(new_track); });
1168         });
1169       }
1170 
1171     dialog::addBreak(100);
1172 
1173     if(WDIM == 3) {
1174       dialog::addItem(XLAT("3D configuration"), '9');
1175       dialog::add_action_push(show3D);
1176       }
1177     else {
1178       dialog::addItem(XLAT("configure the projection"), 'p');
1179       dialog::add_action_push(race_projection);
1180       }
1181 
1182     dialog::addBoolItem_action(XLAT("guiding line"), guiding, 'g');
1183 
1184     dialog::addItem(multi::player_count_name(playercfg), 'n');
1185     dialog::add_action([/*this*/] () {
1186       playercfg = playercfg == 1 ? 2 : 1;
1187       });
1188 
1189     dialog::addItem(XLAT("configure player 1"), '1');
1190     dialog::add_action([] () {
1191       pushScreen(multi::get_key_configurer(1, playercmds_race));
1192       });
1193 
1194     if(playercfg >= 2) {
1195       dialog::addItem(XLAT("configure player 2"), '2');
1196       dialog::add_action([] () {
1197         pushScreen(multi::get_key_configurer(2, playercmds_race));
1198         });
1199       }
1200     else dialog::addBreak(100);
1201 
1202     dialog::addBreak(100);
1203 
1204     dialog::addSelItem(XLAT("track seed"), editing_track ? dialog::view_edited_string() : new_track, '/');
1205     dialog::add_action([/*this*/] () {
1206       editing_track = !editing_track;
1207       if(editing_track) dialog::start_editing(new_track);
1208       });
1209     dialog::addItem(XLAT("play the official seed"), 'o');
1210     dialog::add_action([/*this*/] () { new_track = "OFFICIAL"; });
1211     dialog::addItem(XLAT("play a random seed"), 'r');
1212     dialog::add_action([/*this*/] () { new_track = random_track_name(); });
1213 
1214     dialog::addBreak(100);
1215 
1216     dialog::addSelItem(XLAT("best scores to show as ghosts"), its(ghosts_to_show), 'c');
1217     dialog::add_action([]() { dialog::editNumber(ghosts_to_show, 0, 100, 1, 5, "best scores to show as ghosts", ""); });
1218 
1219     dialog::addSelItem(XLAT("best scores to save"), its(ghosts_to_save), 'b');
1220     dialog::add_action([]() { dialog::editNumber(ghosts_to_save, 0, 100, 1, 10, "best scores to save", ""); });
1221 
1222 
1223     if(racing::on) {
1224       dialog::addItem(XLAT("disable the racing mode"), 'x');
1225       dialog::add_action([] () {
1226         stop_game();
1227         switch_game_mode(rg::racing);
1228         race_try = 0;
1229         if(geometry == gNormal) specialland = firstland = laIce;
1230         start_game();
1231         });
1232       }
1233 
1234     #if MAXMDIM >= 4
1235     dialog::addItem(XLAT("racing in Thurston geometries"), 'T');
1236     dialog::add_action_push(thurston_racing);
1237     #endif
1238 
1239     dialog::addBack();
1240     dialog::display();
1241 
1242     keyhandler = [/*this*/] (int sym, int uni) {
1243       if(editing_track) {
1244         if(sym == SDLK_RETURN) sym = uni = '/';
1245         if(dialog::handle_edit_string(sym, uni, racecheck)) return;
1246         }
1247       dialog::handleNavigation(sym, uni);
1248       if(doexiton(sym, uni)) { if(editing_track) editing_track = false; else popScreen(); }
1249       };
1250 
1251     }
1252 /*  }; */
1253 
configure_race()1254 EX void configure_race() {
1255   set_race_configurer();
1256 
1257   pushScreen(raceconfigurer);
1258   }
1259 
1260 auto hooks1 =
__anon7cd889712f02(o_funcs& v) 1261   addHook(hooks_o_key, 90, [] (o_funcs& v) {
1262     if(racing::on) { set_race_configurer();  v.push_back(named_dialog(XLAT("racing menu"), raceconfigurer)); }
1263     });
1264 
1265 map<string, map<eLand, int> > scoreboard;
1266 
uploadScore()1267 void uploadScore() {
1268   int tscore = 0;
1269   for(eLand l: race_lands) {
1270     int i = get_score_in_land(l);
1271     if(!i) continue;
1272     scoreboard[myname()][l] = i;
1273     int score = 60000000 / i; // 1000 points for minute, 2000 points for 30 sec
1274     tscore += score;
1275     }
1276 
1277   achievement_score(LB_RACING, tscore);
1278   }
1279 
displayScore(eLand l)1280 EX void displayScore(eLand l) {
1281   int vf = min((vid.yres-64) / 70, vid.xres/80);
1282   int x = vid.xres / 4;
1283 
1284   set_priority_board(LB_RACING);
1285 
1286   if(get_sync_status() == 1) {
1287     displayfr(x, 56, 1, vf, "(syncing)", 0xC0C0C0, 0);
1288     }
1289   else {
1290     vector<pair<int, string> > scores;
1291     for(auto p: scoreboard) if(p.second.count(l)) scores.emplace_back(p.second[l], p.first);
1292     sort(scores.begin(), scores.end());
1293     int i = 0;
1294     for(auto& sc: scores) {
1295       int i0 = 56 + (i++) * vf;
1296       displayfr(x, i0, 1, vf, racetimeformat(sc.first), 0xC0C0C0, 16);
1297       displayfr(x+8, i0, 1, vf, sc.second, 0xC0C0C0, 0);
1298       }
1299     }
1300   }
1301 
race_won()1302 EX void race_won() {
1303   if(!race_finish_tick[multi::cpid]) {
1304     int result = ticks - race_start_tick;
1305     int losers = 0;
1306     int place = 1;
1307     for(int i=0; i<multi::players; i++) if(race_finish_tick[i]) place++; else losers = 0;
1308     for(auto& ghost: ghostset()[specialland]) if(ghost.result < result) place++; else losers++;
1309     for(auto& ghost: oghostset()[specialland]) if(ghost.result < result) place++; else losers++;
1310 
1311     if(place == 1 && losers) trophy[multi::cpid] = 0xFFD500FF;
1312     if(place == 2) trophy[multi::cpid] = 0xFFFFC0FF;
1313     if(place == 3) trophy[multi::cpid] = 0x967444FF;
1314 
1315     if(place + losers > 1)
1316       addMessage(XLAT("Finished the race! Time: %1, place: %2 out of %3", racetimeformat(result), its(place), its(place+losers)));
1317     else
1318       addMessage(XLAT("Finished the race in time %1!", racetimeformat(result)));
1319 
1320     if(place == 1 && losers && official_race && isize(oghostset()[specialland]))
1321       achievement_gain("RACEWON", rg::racing);
1322 
1323     race_finish_tick[multi::cpid] = ticks;
1324     charstyle gcs = getcs();
1325     for(color_t *x: {&gcs.skincolor, &gcs.haircolor, &gcs.dresscolor, &gcs.swordcolor, &gcs.dresscolor2}) {
1326       for(int a=1; a<4; a++)
1327         part(*x, a) = (part(*x, a) + part(gcs.uicolor, a)) / 2;
1328       part(*x, 0) >>= 2;
1329       }
1330 
1331     auto &subtrack = ghostset() [specialland];
1332 
1333     int ngh = 0;
1334     for(int i=0; i<isize(subtrack); i++) {
1335       if(subtrack[i].checksum != race_checksum) {
1336         println(hlog, format("wrong checksum: %x, should be %x", subtrack[i].checksum, race_checksum));
1337         }
1338       else {
1339         if(i != ngh)
1340           subtrack[ngh] = move(subtrack[i]);
1341         ngh++;
1342         }
1343       }
1344     subtrack.resize(ngh);
1345 
1346     subtrack.emplace_back(ghost{gcs, result, race_checksum, time(NULL), current_history[multi::cpid]});
1347     sort(subtrack.begin(), subtrack.end(), [] (const ghost &g1, const ghost &g2) { return g1.result < g2.result; });
1348     if(isize(subtrack) > ghosts_to_save && ghosts_to_save > 0)
1349       subtrack.resize(ghosts_to_save);
1350     #if CAP_FILES
1351     if(ghosts_to_save > 0)
1352       write_ghosts(track_code, modecode());
1353     #endif
1354 
1355     if(official_race) uploadScore();
1356     }
1357   }
1358 
draw_ghost_at(ghost & ghost,cell * w,const shiftmatrix & V,ghostmoment & p)1359 void draw_ghost_at(ghost& ghost, cell *w, const shiftmatrix& V, ghostmoment& p) {
1360   dynamicval<charstyle> x(getcs(), ghost.cs);
1361   if(ghost.cs.charid == -1) {
1362     dynamicval<bool> pc(peace::on, true);
1363     drawMonsterType(eMonster(ghost.cs.uicolor), w, V, ghost.cs.dresscolor, uchar_to_frac(p.footphase), NOCOLOR);
1364     return;
1365     }
1366 
1367   drawMonsterType(moPlayer, w, V, 0, uchar_to_frac(p.footphase), NOCOLOR);
1368   }
1369 
ghost_finished(ghost & ghost)1370 bool ghost_finished(ghost& ghost) {
1371   auto p = std::find_if(ghost.history.begin(), ghost.history.end(), [] (const ghostmoment gm) { return gm.step > ticks - race_start_tick;} );
1372   return p == ghost.history.end();
1373   }
1374 
get_ghostmoment(ghost & ghost)1375 ghostmoment& get_ghostmoment(ghost& ghost) {
1376   auto p = std::find_if(ghost.history.begin(), ghost.history.end(), [] (const ghostmoment gm) { return gm.step > ticks - race_start_tick;} );
1377   if(p == ghost.history.end()) p--, p->footphase = 0;
1378   return *p;
1379   }
1380 
draw_ghost(ghost & ghost)1381 void draw_ghost(ghost& ghost) {
1382   auto& p = get_ghostmoment(ghost);
1383   cell *w = rti[p.where_id].c;
1384   if(!gmatrix.count(w)) return;
1385   draw_ghost_at(ghost, w, get_ghostmoment_matrix(p), p);
1386   }
1387 
racerel(ld rel)1388 shiftmatrix racerel(ld rel) {
1389   int bsize = vid.fsize * 2;
1390   return shiftless(atscreenpos(bsize, vid.yres - bsize - rel * (vid.yres - bsize*2) / 100, bsize) * spin(M_PI/2));
1391   }
1392 
get_percentage(cell * c)1393 EX int get_percentage(cell *c) {
1394   return min(get_info(c).completion * 100 / (isize(track) - DROP), 100);
1395   }
1396 
get_percentage(int i)1397 EX int get_percentage(int i) {
1398   return get_percentage(shmup::pc[i]->base);
1399   }
1400 
draw_ghost_state(ghost & ghost)1401 void draw_ghost_state(ghost& ghost) {
1402   auto& p = get_ghostmoment(ghost);
1403   if(p.where_id >= isize(rti)) return;
1404   cell *w = rti[p.where_id].c;
1405   ld result = ghost_finished(ghost) ? 100 : get_percentage(w);
1406   draw_ghost_at(ghost, w, racerel(result), p);
1407   }
1408 
drawStats()1409 EX void drawStats() {
1410 
1411   if(!racing::on) return;
1412 
1413   flat_model_enabler fme;
1414   initquickqueue();
1415 
1416   int bsize = vid.fsize * 2;
1417   for(int y: {bsize, vid.yres - bsize}) {
1418     curvepoint(atscreenpos(bsize, y, bsize) * C0);
1419     curvepoint(atscreenpos(bsize/2, y, bsize) * C0);
1420     curvepoint(atscreenpos(bsize*3/2, y, bsize) * C0);
1421     curvepoint(atscreenpos(bsize, y, bsize) * C0);
1422     }
1423   queuecurve(shiftless(Id), 0xFFFFFFFF, 0, PPR::ZERO);
1424 
1425   for(auto& ghost: ghostset()[specialland]) draw_ghost_state(ghost);
1426   for(auto& ghost: oghostset()[specialland]) draw_ghost_state(ghost);
1427 
1428   for(int i=0; i<multi::players; i++) {
1429     dynamicval<int> d(multi::cpid, i);
1430     drawMonsterType(moPlayer, shmup::pc[i]->base, racerel(race_finish_tick[i] ? 100 : get_percentage(i)), 0xFFFFFFC0, shmup::pc[i]->footphase, NOCOLOR);
1431     }
1432 
1433   quickqueue();
1434   }
1435 
markers()1436 EX void markers() {
1437   if(!racing::on) return;
1438   if(guiding && WDIM == 2) for(int i=0; i<multi::players; i++) {
1439     shmup::monster *m = shmup::pc[i];
1440     if(!m) continue;
1441     for(int j=0; j<5; j++)
1442       gridline(m->pat, xpush0(j), xpush0(j+1), multi::scs[i].uicolor, 2);
1443     }
1444   if(racing::player_relative || use_standard_centering() || WDIM == 3) {
1445     using namespace racing;
1446     cell *goal = NULL;
1447     for(cell *c: track) if(inscreenrange(c)) goal = c;
1448 
1449     if(!goal) return;
1450 
1451     shiftpoint H = tC0(ggmatrix(goal));
1452     if(invalid_point(H.h)) return;
1453 
1454     queuestr(H, 2*vid.fsize, "X", 0x10100 * int(128 + 100 * sintick(150)));
1455     int cd = celldistance(track.back(), cwt.at);
1456     if(cd != DISTANCE_UNKNOWN)
1457     queuestr(H, vid.fsize,
1458       #if CAP_CRYSTAL
1459       (cryst && !crystal::pure()) ? fts(crystal::space_distance(cwt.at, track.back())) :
1460       #endif
1461       its(cd), 0x10101 * int(128 - 100 * sintick(150)));
1462     addauraspecial(H, 0xFFD500, 0);
1463     }
1464   int ghosts_left = ghosts_to_show;
1465   for(auto& ghost: ghostset()[specialland]) {
1466     if(!ghosts_left) break;
1467     ghosts_left--;
1468     draw_ghost(ghost);
1469     }
1470 
1471   for(auto& ghost: oghostset()[specialland])
1472     draw_ghost(ghost);
1473 
1474   if(gmatrix.count(track[0])) {
1475     hyperpoint h = WDIM == 2 && GDIM == 3 ? zpush(cgi.FLOOR - cgi.human_height/80) * C0 : C0;
1476     for(ld z=-start_line_width; z<=start_line_width; z+=0.1)
1477       curvepoint(parabolic1(z) * h);
1478     queuecurve(ggmatrix(track[0]) * straight, 0xFFFFFFFF, 0, PPR::BFLOOR);
1479     }
1480   }
1481 
add_debug(cell * c)1482 EX void add_debug(cell *c) {
1483   if(racing::on && racing::rti_id[c]) {
1484     auto& r = racing::get_info(c);
1485     dialog::addSelItem("from_track", its(r.from_track), 0);
1486     dialog::addSelItem("from_start", its(r.from_start), 0);
1487     dialog::addSelItem("from_goal", its(r.from_goal), 0);
1488     dialog::addSelItem("completion", its(r.completion), 0);
1489     }
1490   }
1491 
1492 #if MAXMDIM >= 4
start_thurston()1493 EX void start_thurston() {
1494   stop_game();
1495   resetModes();
1496   start_game();
1497   pushScreen(showStartMenu);
1498   pushScreen(racing::thurston_racing);
1499   }
1500 #endif
1501 #endif
1502 
1503 #if !CAP_RACING
1504 EX always_false on;
1505 #endif
1506 EX }
1507 
1508 }
1509