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