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