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