]> git.localhorst.tv Git - alttp.git/blob - resources/js/components/tracker/Map.js
clean up manual tracking
[alttp.git] / resources / js / components / tracker / Map.js
1 import PropTypes from 'prop-types';
2 import React from 'react';
3 import { useTranslation } from 'react-i18next';
4
5 import {
6         addDungeonCheck,
7         clearAll,
8         completeDungeonChecks,
9         countRemainingLocations,
10         getDungeonClearedItems,
11         getDungeonRemainingItems,
12         hasClearedLocations,
13         hasDungeonBoss,
14         hasDungeonPrize,
15         isDungeonCleared,
16         removeDungeonCheck,
17         resetDungeonChecks,
18         setBossDefeated,
19         setPrizeAcquired,
20         unclearAll,
21 } from '../../helpers/tracker';
22 import { useTracker } from '../../hooks/tracker';
23
24 const LW_DUNGEONS = [
25         {
26                 id: 'hc',
27                 x: 0.5,
28                 y: 0.5,
29         },
30         {
31                 id: 'ct',
32                 x: 0.5,
33                 y: 0.4,
34         },
35         {
36                 id: 'ep',
37                 x: 0.95,
38                 y: 0.42,
39         },
40         {
41                 id: 'dp',
42                 x: 0.075,
43                 y: 0.8,
44         },
45         {
46                 id: 'th',
47                 x: 0.56,
48                 y: 0.05,
49         },
50 ];
51
52 const LW_LOCATIONS = [
53         {
54                 id: 'aginah',
55                 checks: [
56                         'aginah',
57                 ],
58                 x: 0.2,
59                 y: 0.83,
60         },
61         {
62                 id: 'blinds-hut',
63                 checks: [
64                         'blinds-hut-top',
65                         'blinds-hut-left',
66                         'blinds-hut-right',
67                         'blinds-hut-far-left',
68                         'blinds-hut-far-right',
69                 ],
70                 x: 0.14,
71                 y: 0.42,
72         },
73         {
74                 id: 'bombos-tablet',
75                 checks: [
76                         'bombos-tablet',
77                 ],
78                 x: 0.225,
79                 y: 0.925,
80         },
81         {
82                 id: 'bonk-rocks',
83                 checks: [
84                         'bonk-rocks',
85                 ],
86                 x: 0.4,
87                 y: 0.3,
88         },
89         {
90                 id: 'bottle-vendor',
91                 checks: [
92                         'bottle-vendor',
93                 ],
94                 x: 0.1,
95                 y: 0.475,
96         },
97         {
98                 id: 'cave-45',
99                 checks: [
100                         'cave-45',
101                 ],
102                 x: 0.27,
103                 y: 0.83,
104         },
105         {
106                 id: 'checkerboard',
107                 checks: [
108                         'checkerboard',
109                 ],
110                 x: 0.18,
111                 y: 0.78,
112         },
113         {
114                 id: 'chicken-house',
115                 checks: [
116                         'chicken-house',
117                 ],
118                 x: 0.1,
119                 y: 0.53,
120         },
121         {
122                 id: 'dam',
123                 checks: [
124                         'flooded-chest',
125                         'sunken-treasure',
126                 ],
127                 x: 0.4675,
128                 y: 0.9375,
129         },
130         {
131                 id: 'desert-ledge',
132                 checks: [
133                         'desert-ledge',
134                 ],
135                 x: 0.025,
136                 y: 0.9,
137         },
138         {
139                 id: 'ether-tablet',
140                 checks: [
141                         'ether-tablet',
142                 ],
143                 x: 0.425,
144                 y: 0.025,
145         },
146         {
147                 id: 'floating-island',
148                 checks: [
149                         'floating-island',
150                 ],
151                 x: 0.8,
152                 y: 0.025,
153         },
154         {
155                 id: 'flute-spot',
156                 checks: [
157                         'flute-spot',
158                 ],
159                 x: 0.3,
160                 y: 0.675,
161         },
162         {
163                 id: 'graveyard-ledge',
164                 checks: [
165                         'graveyard-ledge',
166                 ],
167                 x: 0.57,
168                 y: 0.28,
169         },
170         {
171                 id: 'hobo',
172                 checks: [
173                         'hobo',
174                 ],
175                 x: 0.7,
176                 y: 0.7,
177         },
178         {
179                 id: 'ice-rod-cave',
180                 checks: [
181                         'ice-rod-cave',
182                 ],
183                 x: 0.9,
184                 y: 0.76,
185         },
186         {
187                 id: 'kak-well',
188                 checks: [
189                         'kak-well-top',
190                         'kak-well-left',
191                         'kak-well-mid',
192                         'kak-well-right',
193                         'kak-well-bottom',
194                 ],
195                 x: 0.04,
196                 y: 0.425,
197         },
198         {
199                 id: 'kings-tomb',
200                 checks: [
201                         'kings-tomb',
202                 ],
203                 x: 0.62,
204                 y: 0.3,
205         },
206         {
207                 id: 'lake-hylia-island',
208                 checks: [
209                         'lake-hylia-island',
210                 ],
211                 x: 0.725,
212                 y: 0.8375,
213         },
214         {
215                 id: 'library',
216                 checks: [
217                         'library',
218                 ],
219                 x: 0.15,
220                 y: 0.65,
221         },
222         {
223                 id: 'links-house',
224                 checks: [
225                         'links-house',
226                 ],
227                 x: 0.55,
228                 y: 0.6875,
229         },
230         {
231                 id: 'lost-woods-hideout',
232                 checks: [
233                         'lost-woods-hideout',
234                 ],
235                 x: 0.19,
236                 y: 0.14,
237         },
238         {
239                 id: 'lumberjack',
240                 checks: [
241                         'lumberjack',
242                 ],
243                 x: 0.3,
244                 y: 0.07,
245         },
246         {
247                 id: 'magic-bat',
248                 checks: [
249                         'magic-bat',
250                 ],
251                 x: 0.325,
252                 y: 0.55,
253         },
254         {
255                 id: 'mimic-cave',
256                 checks: [
257                         'mimic-cave',
258                 ],
259                 x: 0.85,
260                 y: 0.1,
261         },
262         {
263                 id: 'mini-moldorm-cave',
264                 checks: [
265                         'mini-moldorm-left',
266                         'mini-moldorm-right',
267                         'mini-moldorm-far-left',
268                         'mini-moldorm-far-right',
269                         'mini-moldorm-npc',
270                 ],
271                 x: 0.65,
272                 y: 0.95,
273         },
274         {
275                 id: 'mushroom-spot',
276                 checks: [
277                         'mushroom-spot',
278                 ],
279                 x: 0.125,
280                 y: 0.08,
281         },
282         {
283                 id: 'old-man',
284                 checks: [
285                         'old-man',
286                 ],
287                 x: 0.405,
288                 y: 0.195,
289         },
290         {
291                 id: 'paradox-cave',
292                 checks: [
293                         'paradox-lower-far-left',
294                         'paradox-lower-left',
295                         'paradox-lower-right',
296                         'paradox-lower-far-right',
297                         'paradox-lower-mid',
298                         'paradox-upper-left',
299                         'paradox-upper-right',
300                 ],
301                 x: 0.85,
302                 y: 0.2,
303         },
304         {
305                 id: 'pedestal',
306                 checks: [
307                         'pedestal',
308                 ],
309                 x: 0.03,
310                 y: 0.05,
311         },
312         {
313                 id: 'potion-shop',
314                 checks: [
315                         'potion-shop',
316                 ],
317                 x: 0.8,
318                 y: 0.325,
319         },
320         {
321                 id: 'race-game',
322                 checks: [
323                         'race-game',
324                 ],
325                 x: 0.025,
326                 y: 0.7,
327         },
328         {
329                 id: 'saha',
330                 checks: [
331                         'saha',
332                 ],
333                 x: 0.815,
334                 y: 0.465,
335         },
336         {
337                 id: 'saha-hut',
338                 checks: [
339                         'saha-left',
340                         'saha-mid',
341                         'saha-right',
342                 ],
343                 x: 0.815,
344                 y: 0.42,
345         },
346         {
347                 id: 'sick-kid',
348                 checks: [
349                         'sick-kid',
350                 ],
351                 x: 0.155,
352                 y: 0.525,
353         },
354         {
355                 id: 'uncle',
356                 checks: [
357                         'uncle',
358                         'secret-passage',
359                 ],
360                 x: 0.6,
361                 y: 0.4,
362         },
363         {
364                 id: 'spec-rock',
365                 checks: [
366                         'spec-rock',
367                 ],
368                 x: 0.48,
369                 y: 0.09,
370         },
371         {
372                 id: 'spec-rock-cave',
373                 checks: [
374                         'spec-rock-cave',
375                 ],
376                 x: 0.48,
377                 y: 0.14,
378         },
379         {
380                 id: 'spiral-cave',
381                 checks: [
382                         'spiral-cave',
383                 ],
384                 x: 0.8,
385                 y: 0.1,
386         },
387         {
388                 id: 'tavern',
389                 checks: [
390                         'tavern',
391                 ],
392                 x: 0.16,
393                 y: 0.58,
394         },
395         {
396                 id: 'waterfall-fairy',
397                 checks: [
398                         'waterfall-fairy-left',
399                         'waterfall-fairy-right',
400                 ],
401                 x: 0.9,
402                 y: 0.15,
403         },
404         {
405                 id: 'zora',
406                 checks: [
407                         'zora',
408                 ],
409                 x: 0.975,
410                 y: 0.12,
411         },
412         {
413                 id: 'zora-ledge',
414                 checks: [
415                         'zora-ledge',
416                 ],
417                 x: 0.975,
418                 y: 0.165,
419         },
420 ];
421
422 const DW_DUNGEONS = [
423         {
424                 id: 'pd',
425                 x: 0.95,
426                 y: 0.42,
427         },
428         {
429                 id: 'sp',
430                 x: 0.4675,
431                 y: 0.9375,
432         },
433         {
434                 id: 'sw',
435                 x: 0.05,
436                 y: 0.05,
437         },
438         {
439                 id: 'tt',
440                 x: 0.125,
441                 y: 0.475,
442         },
443         {
444                 id: 'ip',
445                 x: 0.7975,
446                 y: 0.86,
447         },
448         {
449                 id: 'mm',
450                 x: 0.12,
451                 y: 0.82,
452         },
453         {
454                 id: 'tr',
455                 x: 0.94,
456                 y: 0.06,
457         },
458         {
459                 id: 'gt',
460                 x: 0.56,
461                 y: 0.05,
462         },
463 ];
464
465 const DW_LOCATIONS = [
466         {
467                 id: 'blacksmith',
468                 checks: [
469                         'blacksmith',
470                 ],
471                 x: 0.15,
472                 y: 0.65,
473         },
474         {
475                 id: 'brewery',
476                 checks: [
477                         'brewery',
478                 ],
479                 x: 0.1,
480                 y: 0.6,
481         },
482         {
483                 id: 'bumper-cave',
484                 checks: [
485                         'bumper-cave',
486                 ],
487                 x: 0.325,
488                 y: 0.15,
489         },
490         {
491                 id: 'c-house',
492                 checks: [
493                         'c-house',
494                 ],
495                 x: 0.2,
496                 y: 0.5,
497         },
498         {
499                 id: 'catfish',
500                 checks: [
501                         'catfish',
502                 ],
503                 x: 0.9,
504                 y: 0.175,
505         },
506         {
507                 id: 'chest-game',
508                 checks: [
509                         'chest-game',
510                 ],
511                 x: 0.05,
512                 y: 0.45,
513         },
514         {
515                 id: 'digging-game',
516                 checks: [
517                         'digging-game',
518                 ],
519                 x: 0.05,
520                 y: 0.7,
521         },
522         {
523                 id: 'hammer-pegs',
524                 checks: [
525                         'hammer-pegs',
526                 ],
527                 x: 0.3125,
528                 y: 0.6,
529         },
530         {
531                 id: 'hookshot-cave',
532                 checks: [
533                         'hookshot-cave-tl',
534                         'hookshot-cave-tr',
535                         'hookshot-cave-bl',
536                 ],
537                 x: 0.85,
538                 y: 0.02,
539         },
540         {
541                 id: 'hookshot-cave-bonk',
542                 checks: [
543                         'hookshot-cave-br',
544                 ],
545                 x: 0.85,
546                 y: 0.065,
547         },
548         {
549                 id: 'hype-cave',
550                 checks: [
551                         'hype-cave-top',
552                         'hype-cave-left',
553                         'hype-cave-right',
554                         'hype-cave-bottom',
555                         'hype-cave-npc',
556                 ],
557                 x: 0.6,
558                 y: 0.75,
559         },
560         {
561                 id: 'mire-shed',
562                 checks: [
563                         'mire-shed-left',
564                         'mire-shed-right',
565                 ],
566                 x: 0.04,
567                 y: 0.8,
568         },
569         {
570                 id: 'purple-chest',
571                 checks: [
572                         'purple-chest',
573                 ],
574                 x: 0.3125,
575                 y: 0.525,
576         },
577         {
578                 id: 'pyramid',
579                 checks: [
580                         'pyramid',
581                 ],
582                 x: 0.575,
583                 y: 0.45,
584         },
585         {
586                 id: 'pyramid-fairy',
587                 checks: [
588                         'pyramid-fairy-left',
589                         'pyramid-fairy-right',
590                 ],
591                 x: 0.45,
592                 y: 0.5,
593         },
594         {
595                 id: 'spike-cave',
596                 checks: [
597                         'spike-cave',
598                 ],
599                 x: 0.575,
600                 y: 0.15,
601         },
602         {
603                 id: 'stumpy',
604                 checks: [
605                         'stumpy',
606                 ],
607                 x: 0.3125,
608                 y: 0.6875,
609         },
610         {
611                 id: 'super-bunny',
612                 checks: [
613                         'super-bunny-top',
614                         'super-bunny-bottom',
615                 ],
616                 x: 0.85,
617                 y: 0.15,
618         },
619 ];
620
621 const Location = ({ number, l, size }) => {
622         const { t } = useTranslation();
623
624         const classNames = ['location', `status-${l.status}`];
625         if (size) {
626                 classNames.push(`size-${size}`);
627         }
628         if (l.handlePrimary) {
629                 classNames.push('clickable');
630         }
631
632         return <g
633                 className={classNames.join(' ')}
634                 onClick={(e) => {
635                         l.handlePrimary();
636                         e.preventDefault();
637                         e.stopPropagation();
638                 }}
639                 onContextMenu={(e) => {
640                         l.handleSecondary();
641                         e.preventDefault();
642                         e.stopPropagation();
643                 }}
644                 transform={`translate(${l.x} ${l.y})`}
645         >
646                 <title>{t(`tracker.location.${l.id}`)}</title>
647                 <rect className="box" x="0" y="0" />
648                 {number && l.remaining ?
649                         <text className="text" x="0" y="0">{l.remaining}</text>
650                 : null}
651         </g>;
652 };
653
654 Location.propTypes = {
655         number: PropTypes.bool,
656         l: PropTypes.shape({
657                 id: PropTypes.string,
658                 x: PropTypes.number,
659                 y: PropTypes.number,
660                 number: PropTypes.number,
661                 remaining: PropTypes.number,
662                 status: PropTypes.string,
663                 handlePrimary: PropTypes.func,
664                 handleSecondary: PropTypes.func,
665         }),
666         size: PropTypes.string,
667 };
668
669 const makeBackground = (src, level) => {
670         const amount = Math.pow(2, Math.max(0, level - 8));
671         const size = 1 / amount;
672         const tiles = [];
673         for (let y = 0; y < amount; ++y) {
674                 for (let x = 0; x < amount; ++x) {
675                         tiles.push(<image
676                                 key={`${x}-${y}`}
677                                 x={x * size}
678                                 y={y * size}
679                                 width={size * 1.002}
680                                 height={size * 1.002}
681                                 href={`/media/alttp/map/${src}/${level}/${x}_${y}.png`}
682                         />);
683                 }
684         }
685         return tiles;
686 };
687
688 const Map = () => {
689         const { dungeons, setState, state } = useTracker();
690
691         const mapDungeon = React.useCallback(dungeon => {
692                 const definition = dungeons.find(d => d.id === dungeon.id);
693                 const remaining = getDungeonRemainingItems(state, definition);
694                 let status = 'available';
695                 if (isDungeonCleared(state, definition)) {
696                         status = 'cleared';
697                 }
698                 return {
699                         ...dungeon,
700                         status,
701                         remaining,
702                         handlePrimary: () => {
703                                 if (getDungeonRemainingItems(state, definition)) {
704                                         setState(addDungeonCheck(definition));
705                                 } else if (
706                                         !hasDungeonBoss(state, definition) || !hasDungeonPrize(state, definition)
707                                 ) {
708                                         if (definition.boss) {
709                                                 setState(setBossDefeated(definition, true));
710                                         }
711                                         if (definition.prize) {
712                                                 setState(setPrizeAcquired(definition, true));
713                                         }
714                                 } else {
715                                         setState(resetDungeonChecks(definition));
716                                         if (definition.boss) {
717                                                 setState(setBossDefeated(definition, false));
718                                         }
719                                         if (definition.prize) {
720                                                 setState(setPrizeAcquired(definition, false));
721                                         }
722                                 }
723                         },
724                         handleSecondary: () => {
725                                 if (isDungeonCleared(state, definition)) {
726                                         if (definition.items) {
727                                                 setState(removeDungeonCheck(definition));
728                                         }
729                                         if (definition.boss) {
730                                                 setState(setBossDefeated(definition, false));
731                                         }
732                                         if (definition.prize) {
733                                                 setState(setPrizeAcquired(definition, false));
734                                         }
735                                 } else if (getDungeonClearedItems(state, definition)) {
736                                         setState(removeDungeonCheck(definition));
737                                 } else {
738                                         setState(completeDungeonChecks(definition));
739                                         if (definition.boss) {
740                                                 setState(setBossDefeated(definition, true));
741                                         }
742                                         if (definition.prize) {
743                                                 setState(setPrizeAcquired(definition, true));
744                                         }
745                                 }
746                         },
747                 };
748         }, [dungeons, setState, state]);
749
750         const mapLocation = React.useCallback(loc => {
751                 const remaining = countRemainingLocations(state, loc.checks);
752                 let status = 'available';
753                 if (hasClearedLocations(state, loc.checks)) {
754                         status = 'cleared';
755                 }
756                 return {
757                         ...loc,
758                         remaining,
759                         status,
760                         handlePrimary: () => {
761                                 if (remaining) {
762                                         setState(clearAll(loc.checks));
763                                 } else {
764                                         setState(unclearAll(loc.checks));
765                                 }
766                         },
767                         handleSecondary: () => {
768                                 if (remaining) {
769                                         setState(clearAll(loc.checks));
770                                 } else {
771                                         setState(unclearAll(loc.checks));
772                                 }
773                         },
774                 };
775         }, [setState, state]);
776
777         const lwDungeons = React.useMemo(() => LW_DUNGEONS.map(mapDungeon), [mapDungeon]);
778         const lwLocations = React.useMemo(() => LW_LOCATIONS.map(mapLocation), [mapLocation]);
779
780         const dwDungeons = React.useMemo(() => DW_DUNGEONS.map(mapDungeon), [mapDungeon]);
781         const dwLocations = React.useMemo(() => DW_LOCATIONS.map(mapLocation), [mapLocation]);
782
783         return <div className="tracker-map">
784                 <svg
785                         xmlns="http://www.w3.org/2000/svg"
786                         className="canvas"
787                         width="2"
788                         height="1"
789                         viewBox="0 0 2 1"
790                         onContextMenu={(e) => {
791                                 e.preventDefault();
792                                 e.stopPropagation();
793                         }}
794                 >
795                         <g className="light-world">
796                                 <g className="background">
797                                         {makeBackground('lw_files', 10)}
798                                 </g>
799                                 <g className="locations">
800                                         {lwLocations.map(l =>
801                                                 <Location key={l.id} l={l} />
802                                         )}
803                                         {lwDungeons.map(l =>
804                                                 <Location key={l.id} number l={l} size="lg" />
805                                         )}
806                                 </g>
807                         </g>
808                         <g className="dark-world" transform="translate(1 0)">
809                                 <g className="background">
810                                         {makeBackground('dw_files', 10)}
811                                 </g>
812                                 <g className="locations">
813                                         {dwLocations.map(l =>
814                                                 <Location key={l.id} l={l} />
815                                         )}
816                                         {dwDungeons.map(l =>
817                                                 <Location key={l.id} number l={l} size="lg" />
818                                         )}
819                                 </g>
820                         </g>
821                 </svg>
822         </div>;
823 };
824
825 export default Map;