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