├── .browserslistrc ├── .env ├── .eslintrc.js ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── babel.config.js ├── castle.json ├── gen_climate_data.py ├── gen_object_meta.py ├── korok_ids.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── icons │ ├── mapicon_castle.svg │ ├── mapicon_checkpoint.svg │ ├── mapicon_dungeon.svg │ ├── mapicon_dungeon_dlc.svg │ ├── mapicon_hatago.svg │ ├── mapicon_korok.png │ ├── mapicon_labo.svg │ ├── mapicon_shop_bougu.svg │ ├── mapicon_shop_color.svg │ ├── mapicon_shop_jewel.svg │ ├── mapicon_shop_yadoya.svg │ ├── mapicon_shop_yorozu.svg │ ├── mapicon_tower.svg │ └── mapicon_village.svg ├── index.html ├── mstile-150x150.png ├── rupee.svg ├── safari-pinned-tab.svg └── site.webmanifest ├── src ├── App.css ├── App.vue ├── MapBase.ts ├── MapIcon.ts ├── MapMarker.ts ├── MapMarkerGroup.ts ├── MapSearch.ts ├── assets │ ├── background.png │ └── fonts │ │ ├── Calamity-Bold.otf │ │ ├── Calamity-Regular.otf │ │ └── Sheikah-Complete.ttf ├── components │ ├── AppMap.ts │ ├── AppMap.vue │ ├── AppMapDetailsBase.ts │ ├── AppMapDetailsDungeon.ts │ ├── AppMapDetailsDungeon.vue │ ├── AppMapDetailsObj.ts │ ├── AppMapDetailsObj.vue │ ├── AppMapDetailsPlace.ts │ ├── AppMapDetailsPlace.vue │ ├── AppMapFilterMainButton.ts │ ├── AppMapFilterMainButton.vue │ ├── AppMapPopup.ts │ ├── AppMapPopup.vue │ ├── AppMapSettings.ts │ ├── AppMapSettings.vue │ ├── AppMapSidebar.less │ ├── MixinUtil.ts │ ├── ModalGotoCoords.ts │ ├── ModalGotoCoords.vue │ ├── ModalPane.less │ ├── ObjectInfo.ts │ ├── ObjectInfo.vue │ ├── ShopData.ts │ └── ShopData.vue ├── level_scaling.ts ├── main.ts ├── router.ts ├── save.ts ├── services │ ├── MapMgr.ts │ └── MsgMgr.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts └── util │ ├── CanvasMarker.ts │ ├── colorscale.ts │ ├── curves.ts │ ├── leaflet_cluster.ts │ ├── leaflet_tile_workaround.js │ ├── map.ts │ ├── math.ts │ ├── polyline.ts │ ├── settings.ts │ ├── svg.ts │ └── ui.ts ├── tools ├── map_gen_markers.py └── msg_gen_list.py ├── tsconfig.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | >1% 3 | not ie <= 11 4 | not op_mini all 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VUE_APP_GAME_FILES=https://objmap.zeldamods.org/game_files 2 | VUE_APP_RADAR_URL=https://radar.zeldamods.org 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended', 9 | '@vue/typescript' 10 | ], 11 | rules: { 12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 18 15 | cache: 'npm' 16 | - name: install dependencies 17 | run: npm install --include=dev 18 | - name: build 19 | run: npm run build 20 | - name: deploy build 21 | uses: burnett01/rsync-deployments@7.0.1 22 | with: 23 | switches: -avr 24 | path: dist/ 25 | remote_path: ${{ secrets.DEPLOY_PATH }} 26 | remote_host: ${{ secrets.DEPLOY_HOST }} 27 | remote_port: ${{ secrets.DEPLOY_PORT }} 28 | remote_user: ${{ secrets.DEPLOY_USER }} 29 | remote_key: ${{ secrets.DEPLOY_KEY }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | public/game_files 2 | 3 | .DS_Store 4 | node_modules 5 | /dist 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # botw-objmap 2 | _Breath of the Wild_ object map. 3 | 4 | ## Features 5 | * Polished UI: clean BotW look. 6 | * Location texts 7 | * Show them at the correct level. (Location.mubin) 8 | * Map waypoints. (Static.mubin LocationPointer) 9 | * Common objects 10 | * groups: 11 | * Labo 12 | * Tower 13 | * ShopJewel, ShopColor, ShopYorozu, ShopBougu, ShopYadoya 14 | * Hatago, Village, CheckPoint, Castle 15 | * DLC shrines should have a different icon. 16 | * Shrine details 17 | * Shrine name 18 | * Shrine title 19 | * CDungeon number 20 | * All treasure chests 21 | * Search 22 | * search base locations too (no extra work required thanks to how LocationTags work) 23 | * object by ID 24 | * object by name (+ other filters?) 25 | * should work with chest contents, parameters, etc. 26 | * "Add to map" 27 | * "Remove from map" to exclude and hide some search results (suggested by Zant) 28 | * Show up to 2000 search results (suggested by Zant) 29 | * Color by actor type / by search result group (suggested by Zant) 30 | * Objects 31 | * Custom handling for: 32 | * weapons 33 | * enemies 34 | * RememberTag 35 | * rafts 36 | * cooking pots 37 | * Koroks: 38 | * TODO [low-priority] autogenerate information on puzzles if possible? 39 | * TODO [low-priority] show colors, leaf shapes using embedded UMii data? 40 | * treasure chests (show contents) 41 | * Goddess Statues 42 | * Presets 43 | * BTB Enemies 44 | * Launchable Objects 45 | * Treasure Chests 46 | * Arrows 47 | * Custom presets 48 | * Search query syntax documentation 49 | * Show all objects in area. 50 | * Object details 51 | * Respawn information, no-revival area... 52 | * Scaling information 53 | * Instance parameters 54 | * Generation group and links 55 | * Object shape 56 | * Object scale 57 | * Routes 58 | * Regions 59 | * Tower regions 60 | * Map areas (internal) 61 | * TODO Region details 62 | * TODO Climate information 63 | * TODO Autogen information 64 | * TODO Dynamic map data 65 | * TODO Scaling slider (to scale enemies, weapons, etc.) 66 | * Master Mode (auto rankup, Master Mode only actors) 67 | * One-Hit Obliterator challenge mode 68 | * Final boss placement mode 69 | * Polygon/line drawing 70 | * UI 71 | * Saving 72 | * Import/Export 73 | * Colors 74 | * Measuring 75 | * TODO Object tracking 76 | * TODO Track used objects. 77 | * TODO Have checklist views for shrines, locations, Korok seeds, etc. 78 | * TODO The shrine list should show name + title to easily see e.g. combat shrines at a glance. 79 | * TODO Sort by name optionally. For Korok seeds, sort by HashId. 80 | * TODO Group by region optionally 81 | * TODO See also *object details* 82 | * TODO For locations: locations that have save flags are trackable, those that don't appear by default and aren't trackable. 83 | 84 | ## Integration 85 | * Integration with other tools/viewers for special objects, such as: 86 | * EventTag: open event flow in EventEditor (if it exists) 87 | * SignalFlowchart: same 88 | * TODO any actor with an event flow: same 89 | * TODO Autogen: link to EventEditor for AutoPlacement event flows. 90 | 91 | ## Credit 92 | * The authors of the [Sheikah Complete](https://fontstruct.com/fontstructions/show/1371125/sheikah-complete) and [Calamity Sans](https://www.reddit.com/r/zelda/comments/5txuba/breath_of_the_wild_ui_font/) fonts 93 | * Yoshi.noir for the UI map marker icons 94 | * [Korok seed icon](https://www.zeldadungeon.net/breath-of-the-wild-interactive-map/markers/seed.png) 95 | * MrCheeze for the [list of names](https://github.com/MrCheeze/botw-tools/blob/master/botw_names.json) 96 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@vue/app", { 4 | "targets": {"browsers": "last 2 versions and >1% and not ie <= 11 and not op_mini all"}, 5 | }] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /gen_climate_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import yaml 5 | 6 | def u_constrt(loader, node): 7 | return node.value 8 | def io_constrt(loader, node): 9 | value = loader.construct_mapping(node) 10 | return value 11 | def list_constrt(loader, node): 12 | value = loader.construct_mapping(node) 13 | return value 14 | def obj_constrt(loader, node): 15 | value = loader.construct_mapping(node) 16 | return value 17 | def str_constrt(loader, node): 18 | return node.value 19 | def vec3_constrt(loader, node): 20 | return node.value 21 | def color_constrt(loader, node): 22 | return node.value 23 | yaml.add_constructor('!u', u_constrt) 24 | yaml.add_constructor('!io', io_constrt) 25 | yaml.add_constructor('!color', color_constrt) 26 | yaml.add_constructor('!list', list_constrt) 27 | yaml.add_constructor('!obj', obj_constrt) 28 | yaml.add_constructor('!str64', str_constrt) 29 | yaml.add_constructor('!str32', str_constrt) 30 | yaml.add_constructor('!str256', str_constrt) 31 | yaml.add_constructor('!vec3', vec3_constrt) 32 | 33 | def area_data_json(): 34 | with open('Ecosystem/AreaData.yml','r') as f: 35 | data = yaml.load(f, Loader=yaml.FullLoader) 36 | json.dump(data, open('area_data.json','w')) 37 | 38 | 39 | def climate_data_json(): 40 | with open('WorldMgr/normal.winfo.yml','r') as f: 41 | data = yaml.load(f, Loader=yaml.FullLoader) 42 | 43 | climate_data = {} 44 | 45 | z = list(range(0,1100,100)) 46 | for name, val in data['param_root']['objects'].items(): 47 | if not name.startswith('ClimateDefines'): 48 | continue 49 | 50 | day, night = [], [] 51 | 52 | for v in z: 53 | day.append(val[f"ClimateTemperatureDay_{v:04}"]) 54 | night.append(val[f"ClimateTemperatureNight_{v:04}"]) 55 | climate_data[name] = { 56 | 'Day': day, 57 | 'Night': night, 58 | 'height': z, 59 | } 60 | for key, value in val.items(): 61 | if key == "FeatureColor": 62 | #print(val2[0].value) 63 | value = [v.value for v in value] 64 | climate_data[name][key] = value 65 | 66 | json.dump(climate_data, open('climate_data.json','w')) 67 | 68 | area_data_json() 69 | climate_data_json() 70 | -------------------------------------------------------------------------------- /gen_object_meta.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import yaml 5 | import json 6 | import glob 7 | 8 | 9 | def u_constrt(loader, node): 10 | return node.value 11 | 12 | 13 | def io_constrt(loader, node): 14 | value = loader.construct_mapping(node) 15 | return value 16 | 17 | 18 | def list_constrt(loader, node): 19 | value = loader.construct_mapping(node) 20 | return value 21 | 22 | 23 | def obj_constrt(loader, node): 24 | value = loader.construct_mapping(node) 25 | return value 26 | 27 | 28 | def str_constrt(loader, node): 29 | return node.value 30 | 31 | 32 | def vec3_constrt(loader, node): 33 | return node.value 34 | 35 | 36 | def color_constrt(loader, node): 37 | return node.value 38 | 39 | 40 | yaml.add_constructor("!u", u_constrt) 41 | yaml.add_constructor("!io", io_constrt) 42 | yaml.add_constructor("!color", color_constrt) 43 | yaml.add_constructor("!list", list_constrt) 44 | yaml.add_constructor("!obj", obj_constrt) 45 | yaml.add_constructor("!str64", str_constrt) 46 | yaml.add_constructor("!str32", str_constrt) 47 | yaml.add_constructor("!str256", str_constrt) 48 | yaml.add_constructor("!vec3", vec3_constrt) 49 | 50 | 51 | def is_npc(name): 52 | return name.startswith("Npc_") 53 | 54 | 55 | def is_obj(name): 56 | return name.startswith("Obj_") 57 | 58 | 59 | def is_item(name): 60 | return name.startswith("Item_") 61 | 62 | 63 | def is_horse(name): 64 | return name.startswith("GameRomHorse") 65 | 66 | 67 | def is_dummy(name): 68 | return name == "Dummy" 69 | 70 | 71 | def is_tree(name): 72 | return name.startswith("Tree") 73 | 74 | 75 | def is_remain_wind(name): 76 | return name.startswith("RemainsWind") 77 | 78 | 79 | def should_skip(name): 80 | return ( 81 | is_npc(name) 82 | or is_obj(name) 83 | or is_horse(name) 84 | or is_dummy(name) 85 | or is_tree(name) 86 | or is_remain_wind(name) 87 | ) 88 | 89 | 90 | def get_value(data, names): 91 | v = data 92 | for name in names: 93 | if name in v: 94 | v = v[name] 95 | else: 96 | return None 97 | return v 98 | 99 | 100 | meta = {} 101 | 102 | 103 | def add_meta(name, key, value): 104 | if not name in meta: 105 | meta[name] = {} 106 | meta[name][key] = value 107 | 108 | 109 | for file in glob.glob(f"Actor/ActorLink/*.yml"): 110 | with open(file, "r") as f: 111 | bdata = yaml.load(f, Loader=yaml.FullLoader) 112 | 113 | name = os.path.basename(file).replace(".yml", "") 114 | 115 | gpar = get_value(bdata, ["param_root", "objects", "LinkTarget", "GParamUser"]) 116 | if gpar == "Dummy" or gpar == "MessageOnly": 117 | continue 118 | 119 | with open(f"Actor/GeneralParamList/{gpar}.gparamlist.yml", "r") as f: 120 | data = yaml.load(f, Loader=yaml.FullLoader) 121 | 122 | enemy = get_value(data, ["param_root", "objects", "Enemy"]) 123 | weapon = get_value(data, ["param_root", "objects", "WeaponCommon"]) 124 | if not enemy and not weapon: 125 | continue 126 | aname = get_value(data, ["param_root", "objects", "System", "SameGroupActorName"]) 127 | 128 | if should_skip(name): 129 | continue 130 | life = get_value(data, ["param_root", "objects", "General", "Life"]) 131 | if "Enemy_SiteBoss" in file: 132 | # See https://zeldamods.org/wiki/Difficulty_scaling#Ganon_Blights 133 | if "Weapon" in file: 134 | pass 135 | elif "Castle" in file: 136 | life = 2000 137 | elif "_R" in file: 138 | life = 1500 139 | else: 140 | life = "800 - 2000" 141 | if life: 142 | add_meta(name, "life", life) 143 | attack = get_value(data, ["param_root", "objects", "Attack", "Power"]) 144 | if attack: 145 | add_meta(name, "attack", attack) 146 | 147 | json.dump(meta, open("object_meta.json", "w")) 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botw-objmap", 3 | "version": "0.1.0", 4 | "private": true, 5 | "author": "Léo Lam ", 6 | "scripts": { 7 | "serve": "env NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve", 8 | "build": "env NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build", 9 | "lint": "env NODE_OPTIONS=--openssl-legacy-provider vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-free": "^5.13.0", 13 | "bootstrap": "^4.5.0", 14 | "bootstrap-vue": "^2.15.0", 15 | "immer": "^10.0.4", 16 | "leaflet-contextmenu": "^1.4.0", 17 | "leaflet-defaulticon-compatibility": "^0.1.1", 18 | "leaflet-draw": "^1.0.4", 19 | "leaflet-hotline": "github:savage13/Leaflet.hotline", 20 | "leaflet-path-transform": "^1.1.3", 21 | "leaflet-rastercoords": "^1.0.3", 22 | "leaflet-sidebar-v2": "^3.2.3", 23 | "leaflet.markercluster": "^1.4.1", 24 | "lodash": "^4.17.21", 25 | "terser": "^4.8.1", 26 | "vue": "^2.6.11", 27 | "vue-class-component": "^7.2.3", 28 | "vue-property-decorator": "^7.0.0", 29 | "vue-router": "^3.2.0", 30 | "vuedraggable": "^2.24.3" 31 | }, 32 | "devDependencies": { 33 | "@types/leaflet": "^1.5.21", 34 | "@types/leaflet-draw": "^0.4.14", 35 | "@types/leaflet.markercluster": "^1.4.3", 36 | "@types/lodash": "^4.14.168", 37 | "@vue/cli-plugin-babel": "^3.12.1", 38 | "@vue/cli-plugin-eslint": "^3.12.1", 39 | "@vue/cli-plugin-typescript": "^3.12.1", 40 | "@vue/cli-service": "^3.12.1", 41 | "@vue/eslint-config-typescript": "^4.0.0", 42 | "babel-eslint": "^10.1.0", 43 | "eslint": "^5.16.0", 44 | "eslint-plugin-vue": "^5.2.3", 45 | "leaflet": "^1.9.4", 46 | "less": "^3.13.1", 47 | "less-loader": "^4.1.0", 48 | "typescript": "^3.9.7", 49 | "vue-template-compiler": "^2.6.11" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/public/android-chrome-256x256.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2b5797 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/mapicon_castle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/icons/mapicon_checkpoint.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /public/icons/mapicon_dungeon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/mapicon_dungeon_dlc.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 3 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/icons/mapicon_hatago.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 17 | 18 | 19 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /public/icons/mapicon_korok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/public/icons/mapicon_korok.png -------------------------------------------------------------------------------- /public/icons/mapicon_labo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/icons/mapicon_shop_bougu.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 12 | 14 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/mapicon_shop_color.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /public/icons/mapicon_shop_jewel.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /public/icons/mapicon_shop_yadoya.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /public/icons/mapicon_shop_yorozu.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /public/icons/mapicon_tower.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /public/icons/mapicon_village.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BotW Object Map 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/rupee.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 15 | 17 | 23 | 25 | 32 | 34 | 37 | 40 | 42 | 60 | 63 | 65 | 67 | 80 | 82 | 84 | 94 | 96 | 99 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ZeldaMods", 3 | "short_name": "ZeldaMods", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#0b2033", 17 | "background_color": "#0b2033", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Sheikah; 3 | src: url('./assets/fonts/Sheikah-Complete.ttf'); 4 | } 5 | 6 | @font-face { 7 | font-family: Calamity; 8 | font-style: italic; 9 | src: url('./assets/fonts/Calamity-Regular.otf'); 10 | } 11 | 12 | @font-face { 13 | font-family: CalamityB; 14 | font-weight: bold; 15 | font-style: italic; 16 | src: url('./assets/fonts/Calamity-Bold.otf'); 17 | } 18 | 19 | html, body { 20 | height: 100%; 21 | } 22 | 23 | body { 24 | font-family: Calamity, Roboto, sans-serif; 25 | font-size: 16px; 26 | background-color: #000000; 27 | background-image: url("assets/background.png"); 28 | background-attachment: fixed; 29 | box-sizing: border-box; 30 | } 31 | 32 | a, a:active, a:visited { 33 | color: #29d1fc; 34 | } 35 | 36 | code { 37 | color: #ffd800; 38 | } 39 | 40 | .fade-enter-active, .fade-leave-active { 41 | transition: opacity .2s; 42 | } 43 | 44 | .fade-enter, .fade-leave-to { 45 | opacity: 0; 46 | } 47 | 48 | .pane { 49 | background-color: #002645b0; 50 | color: #29abfc; 51 | text-shadow: 0 0 30px #29d1fc7f; 52 | } 53 | 54 | .pane-header { 55 | background-color: #01151ee0; 56 | } 57 | 58 | .pane-content { 59 | padding: 15px; 60 | } 61 | 62 | .form-control, .form-control:focus, .custom-control-label::before { 63 | color: white; 64 | background-color: #01151fe0; 65 | border: 1px solid #01151fe0; 66 | } 67 | 68 | .form-control::placeholder { 69 | color: #7a7a7a; 70 | } 71 | 72 | /* https://stackoverflow.com/a/49852634 */ 73 | .leaflet-tooltip.korok { 74 | background-color: #ffffff00; 75 | border: 0px solid #ffffff00; 76 | box-shadow: 0 1px 3px #ffffff00; 77 | color: black; 78 | } 79 | .leaflet-tooltip.akkala { 80 | text-shadow: 1px 1px #FBFE53 , 1px -1px #FBFE53 , 81 | -1px 1px #FBFE53 , -1px -1px #FBFE53 ; 82 | } 83 | .leaflet-tooltip.central { 84 | text-shadow: 1px 1px #E1FF8E , 1px -1px #E1FF8E , 85 | -1px 1px #E1FF8E , -1px -1px #E1FF8E ; 86 | } 87 | .leaflet-tooltip.eldin { 88 | text-shadow: 1px 1px #F29F5D , 1px -1px #F29F5D , 89 | -1px 1px #F29F5D , -1px -1px #F29F5D ; 90 | } 91 | .leaflet-tooltip.duelingpeaks { 92 | text-shadow: 1px 1px #CB6987 , 1px -1px #CB6987 , 93 | -1px 1px #CB6987 , -1px -1px #CB6987 ; 94 | } 95 | .leaflet-tooltip.faron { 96 | text-shadow: 1px 1px #5FBD52 , 1px -1px #5FBD52 , 97 | -1px 1px #5FBD52 , -1px -1px #5FBD52 ; 98 | } 99 | .leaflet-tooltip.gerudo { 100 | text-shadow: 1px 1px #8ACBE0 , 1px -1px #8ACBE0 , 101 | -1px 1px #8ACBE0 , -1px -1px #8ACBE0 ; 102 | } 103 | .leaflet-tooltip.hebra { 104 | text-shadow: 1px 1px #7FD0E0, 1px -1px #7FD0E0, 105 | -1px 1px #7FD0E0, -1px -1px #7FD0E0; 106 | } 107 | .leaflet-tooltip.woodland { 108 | text-shadow: 1px 1px #6ACF3D , 1px -1px #6ACF3D , 109 | -1px 1px #6ACF3D , -1px -1px #6ACF3D ; 110 | } 111 | .leaflet-tooltip.lake { 112 | text-shadow: 1px 1px #7AA2F8 , 1px -1px #7AA2F8 , 113 | -1px 1px #7AA2F8 , -1px -1px #7AA2F8 ; 114 | } 115 | .leaflet-tooltip.hateno { 116 | text-shadow: 1px 1px #E99E37 , 1px -1px #E99E37 , 117 | -1px 1px #E99E37 , -1px -1px #E99E37 ; 118 | } 119 | .leaflet-tooltip.plateau { 120 | text-shadow: 1px 1px #9DCCC3 , 1px -1px #9DCCC3 , 121 | -1px 1px #9DCCC3 , -1px -1px #9DCCC3 ; 122 | } 123 | .leaflet-tooltip.ridgeland { 124 | text-shadow: 1px 1px #BD6681, 1px -1px #BD6681, 125 | -1px 1px #BD6681, -1px -1px #BD6681; 126 | } 127 | .leaflet-tooltip.tabantha { 128 | text-shadow: 1px 1px #5CBC4F, 1px -1px #5CBC4F, 129 | -1px 1px #5CBC4F, -1px -1px #5CBC4F; 130 | } 131 | .leaflet-tooltip.wasteland { 132 | text-shadow: 1px 1px #F2A33A , 1px -1px #F2A33A , 133 | -1px 1px #F2A33A , -1px -1px #F2A33A ; 134 | } 135 | .leaflet-tooltip.castle { 136 | text-shadow: 1px 1px #B2653F , 1px -1px #B2653F , 137 | -1px 1px #B2653F , -1px -1px #B2653F ; 138 | } 139 | .leaflet-tooltip.lanayru { 140 | text-shadow: 1px 1px #C6B1FF , 1px -1px #C6B1FF , 141 | -1px 1px #C6B1FF , -1px -1px #C6B1FF ; 142 | } 143 | 144 | 145 | .colorscale-label { 146 | position: absolute; 147 | display: inline; 148 | color: white; 149 | transform: translateX(-50%); 150 | } 151 | .colorscale-labelbox { 152 | width: 100%; 153 | min-height: 1.3em; 154 | flex: 0 1 100%; 155 | } 156 | .colorscale { 157 | position: relative; 158 | display: flex; 159 | flex-flow: column nowrap; 160 | align-items: flex-start; 161 | align-content: flex-start; 162 | } 163 | .colorscale-bar { 164 | height: 10px; 165 | min-height: 10px; 166 | width: calc(200px - 1em); 167 | left: 0; 168 | display: block; 169 | flex: 0 1 100%; 170 | } 171 | .colorscale-title { 172 | flex: 0 1 100%; 173 | width: 100%; 174 | text-align: center; 175 | color: white; 176 | margin-bottom: 1px; 177 | } 178 | 179 | .colorscale:hover { 180 | border: 0px solid fuchsia; 181 | } 182 | 183 | .colorscale-picker-title { 184 | color: white; 185 | text-align: center; 186 | } 187 | 188 | .colorscale-picker { 189 | position: absolute; 190 | left: 90%; 191 | top: 0px; 192 | display: none; 193 | background: rgba(255,255,255,0.3); 194 | padding: 2px; 195 | border: 1px solid #333; 196 | border-radius: 3px; 197 | transform: translateY(-80%); 198 | } 199 | 200 | .colorscale:hover .colorscale-picker { 201 | display: block; 202 | } 203 | 204 | .colorscale-picker-sample:hover { 205 | outline: 1px solid white; 206 | } 207 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/MapBase.ts: -------------------------------------------------------------------------------- 1 | import * as L from 'leaflet'; 2 | import 'leaflet/dist/leaflet.css'; 3 | import 'leaflet-rastercoords'; 4 | import 'leaflet-contextmenu'; 5 | import 'leaflet-contextmenu/dist/leaflet.contextmenu.css'; 6 | 7 | import { CanvasMarker } from '@/util/CanvasMarker'; 8 | import * as map from '@/util/map'; 9 | import { Point } from '@/util/map'; 10 | import * as ui from '@/util/ui'; 11 | import '@/util/leaflet_tile_workaround.js'; 12 | import { Settings } from './util/settings'; 13 | import { MapMgr } from '@/services/MapMgr'; 14 | 15 | declare module 'leaflet' { 16 | export type RasterCoords = any; 17 | export let RasterCoords: any; 18 | } 19 | 20 | export const SHOW_ALL_OBJS_FOR_MAP_UNIT_EVENT = 'objmap::SHOW_ALL_OBJS_FOR_MAP_UNIT'; 21 | export const MARKER_SELECTED_EVENT = 'objmap::markerSelected'; 22 | 23 | export class MapBase { 24 | m!: L.Map; 25 | private rc!: L.RasterCoords; 26 | center: Point = [0, 0, 0]; 27 | zoom: number = map.DEFAULT_ZOOM; 28 | private zoomChangeCbs: Array<(zoom: number) => void> = []; 29 | baseImage!: L.Layer; 30 | baseLayer!: L.Layer; 31 | refGrid: Array = []; 32 | refGridOn: boolean = false; 33 | 34 | showBaseMap(show: boolean) { 35 | if (show) { 36 | this.m.addLayer(this.baseImage); 37 | this.m.addLayer(this.baseLayer); 38 | } else { 39 | this.m.removeLayer(this.baseImage); 40 | this.m.removeLayer(this.baseLayer); 41 | } 42 | } 43 | 44 | async loadTowerAreas() { 45 | const areas = await MapMgr.getInstance().fetchAreaMap('MapTower'); 46 | for (const [data, features] of Object.entries(areas)) { 47 | const layers: L.GeoJSON[] = features.map((feature) => { 48 | return L.geoJSON(feature, { 49 | style: function(_) { 50 | return { weight: 0.5, fill: false, color: '#60B0E0' } 51 | }, 52 | }); 53 | }); 54 | layers.forEach(layer => this.refGrid[3].addLayer(layer)); 55 | } 56 | this.showReferenceGridInternal(); 57 | } 58 | 59 | async showReferenceGridInternal() { 60 | if (!this.refGrid.length) { 61 | this.refGrid = this.createMarkers(); 62 | } 63 | const zoomLevel = this.m.getZoom(); 64 | let minZoom = [1, 4, 5, 1]; 65 | if (this.refGridOn) { 66 | this.refGrid.forEach((layer, i) => { 67 | //for (let i = 0; i < 4; i++) { 68 | let visible = this.m.hasLayer(layer); 69 | if (zoomLevel >= minZoom[i]) { 70 | if (!visible) { 71 | this.m.addLayer(layer); 72 | } 73 | } else { 74 | if (visible) { 75 | this.m.removeLayer(layer); 76 | } 77 | } 78 | }); 79 | } else { 80 | this.refGrid.forEach(layer => this.m.removeLayer(layer)); 81 | } 82 | } 83 | 84 | showReferenceGrid(show: boolean) { 85 | this.refGridOn = show; 86 | this.showReferenceGridInternal(); 87 | } 88 | 89 | 90 | toXYZ(latlng: L.LatLng): Point { 91 | return [latlng.lng, 0, latlng.lat]; 92 | } 93 | fromXYZ(pos: Point): L.LatLngExpression { 94 | return [pos[2], pos[0]]; 95 | } 96 | 97 | setView(pos: Point, zoom = -1) { 98 | this.center = pos; 99 | this.setZoomProp(zoom == -1 ? this.m.getZoom() : zoom); 100 | this.m.setView(this.fromXYZ(this.center), this.zoom); 101 | } 102 | 103 | emitMarkerSelectedEvent(marker: any) { this.m.fireEvent(MARKER_SELECTED_EVENT, { marker }); } 104 | registerMarkerSelectedCb(cb: (marker: any) => void) { this.m.on(MARKER_SELECTED_EVENT, (e: any) => cb(e.marker)); } 105 | 106 | registerZoomChangeCb(cb: (zoom: number) => void) { this.zoomChangeCbs.push(cb); } 107 | registerMoveEndCb(cb: any) { this.m.on('moveend', cb); } 108 | registerZoomCb(cb: any) { this.m.on('zoom', cb); } 109 | // Fires shortly after zoomstart with the target zoom level. 110 | registerZoomAnimCb(cb: any) { this.m.on('zoomanim', cb); } 111 | registerZoomEndCb(cb: any) { this.m.on('zoomend', cb); } 112 | 113 | constructor(element: string) { 114 | this.constructMap(element); 115 | this.initBaseMap(); 116 | } 117 | 118 | private constructMap(element: string) { 119 | const crs = L.Util.extend({}, L.CRS.Simple); 120 | // @ts-ignore 121 | crs.transformation = new L.Transformation(4 / map.TILE_SIZE, map.MAP_SIZE[0] / map.TILE_SIZE, 122 | 4 / map.TILE_SIZE, map.MAP_SIZE[1] / map.TILE_SIZE); 123 | 124 | L.Canvas.include({ 125 | _botwDrawCanvasImageMarker(layer: CanvasMarker) { 126 | // @ts-ignore 127 | if (layer._empty()) 128 | return; 129 | // @ts-ignore 130 | const p: L.Point = layer._point; 131 | const ctx: CanvasRenderingContext2D = this._ctx; 132 | const img: HTMLImageElement = (layer.options.icon)!; 133 | if (layer.options.iconWidth && layer.options.iconHeight) { 134 | ctx.drawImage(img, p.x - layer.options.iconWidth / 2, p.y - layer.options.iconHeight / 2, 135 | layer.options.iconWidth, layer.options.iconHeight); 136 | } else { 137 | ctx.drawImage(img, p.x - img.width / 2, p.y - img.height / 2); 138 | } 139 | }, 140 | }); 141 | 142 | let padding = 0.7; 143 | if (L.Browser.safari && L.Browser.mobile && L.Browser.retina) { 144 | padding = 0.1; 145 | } 146 | const renderer = L.canvas({ 147 | // Set a larger padding to avoid markers fading in too late when dragging 148 | padding, 149 | }); 150 | 151 | this.m = new L.Map(element, { 152 | attributionControl: false, 153 | zoomControl: false, 154 | zoom: map.DEFAULT_ZOOM, 155 | minZoom: map.MIN_ZOOM, 156 | maxZoom: map.MAX_ZOOM, 157 | maxBoundsViscosity: 1.0, 158 | crs, 159 | 160 | renderer, 161 | preferCanvas: true, 162 | 163 | // @ts-ignore 164 | contextmenu: true, 165 | contextmenuItems: [ 166 | { 167 | text: 'Copy coordinates', 168 | callback: ({ latlng }: ui.LeafletContextMenuCbArg) => { 169 | const [x, y, z] = this.toXYZ(latlng); 170 | ui.copyToClipboard(`${x},${z}`); 171 | }, 172 | }, 173 | { 174 | text: 'Center map here', 175 | callback: ({ latlng }: ui.LeafletContextMenuCbArg) => { 176 | this.m.panTo(latlng); 177 | } 178 | }, 179 | { 180 | text: 'Show all objects in map unit', 181 | callback: ({ latlng }: ui.LeafletContextMenuCbArg) => { 182 | this.m.fire(SHOW_ALL_OBJS_FOR_MAP_UNIT_EVENT, { latlng }); 183 | }, 184 | } 185 | ], 186 | }); 187 | 188 | this.rc = new L.RasterCoords(this.m, map.MAP_SIZE); 189 | this.rc.setMaxBounds(); 190 | 191 | this.registerZoomAnimCb((evt: L.ZoomAnimEvent) => { 192 | this.setZoomProp(evt.zoom); 193 | }); 194 | this.registerMoveEndCb(() => { 195 | this.center = this.toXYZ(this.m.getCenter()); 196 | }); 197 | } 198 | 199 | private initBaseMap() { 200 | // Add a base image to make tile loading less noticeable. 201 | const BASE_PANE = 'base'; 202 | this.m.createPane(BASE_PANE).style.zIndex = '0'; 203 | const southWest = this.rc.unproject([0, this.rc.height]); 204 | const northEast = this.rc.unproject([this.rc.width, 0]); 205 | const bounds = new L.LatLngBounds(southWest, northEast); 206 | this.baseImage = L.imageOverlay(`${map.GAME_FILES}/maptex/base.png`, bounds, { 207 | pane: BASE_PANE, 208 | }); 209 | this.baseImage.addTo(this.m); 210 | 211 | const baseLayer = L.tileLayer(`${map.GAME_FILES}/maptex/{z}/{x}/{y}.png`, { 212 | maxNativeZoom: 7, 213 | }); 214 | baseLayer.addTo(this.m); 215 | this.baseLayer = baseLayer; 216 | 217 | this.m.createPane('front').style.zIndex = '1000'; 218 | this.m.createPane('front2').style.zIndex = '1001'; 219 | 220 | this.refGridOn = false; 221 | this.m.on("zoom", () => { 222 | this.showReferenceGridInternal(); 223 | }); 224 | } 225 | svgIconBase(width: number) { 226 | return L.divIcon({ 227 | html: ` 229 | 230 | `, 231 | className: "", 232 | iconSize: [10, 10], 233 | iconAnchor: [5, 5], 234 | }); 235 | } 236 | createMarkers() { 237 | const svgIcon = this.svgIconBase(3); 238 | const svgIcon2 = this.svgIconBase(6); 239 | const svgIcon3 = this.svgIconBase(12); 240 | let size = 125; 241 | let markers = [L.layerGroup(), L.layerGroup(), L.layerGroup(), L.layerGroup()]; 242 | for (let i = 0; i < 20 * 4; i++) { 243 | for (let j = 0; j < 16 * 4; j++) { 244 | let z = -4000 + j * size + 125 / 2; 245 | let x = -5000 + i * size + 125 / 2; 246 | let k = 2; 247 | let icon = svgIcon; 248 | if (i % 4 == 0 && j % 4 == 0) { 249 | icon = svgIcon3; 250 | k = 0; 251 | } else if (i % 4 == 0 || j % 4 == 0) { 252 | icon = svgIcon2; 253 | k = 1; 254 | } 255 | markers[k].addLayer(L.marker([z, x], { icon })); 256 | } 257 | } 258 | this.loadTowerAreas(); 259 | return markers; 260 | } 261 | 262 | private setZoomProp(zoom: number) { 263 | this.zoom = zoom; 264 | for (const cb of this.zoomChangeCbs) 265 | cb(zoom); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/MapIcon.ts: -------------------------------------------------------------------------------- 1 | import * as L from 'leaflet'; 2 | 3 | export const DUNGEON = L.icon({ 4 | iconUrl: '/icons/mapicon_dungeon.svg', 5 | iconSize: L.point(36, 36), 6 | className: 'mapicon-Dungeon', 7 | }); 8 | export const DUNGEON_DLC = L.icon({ 9 | iconUrl: '/icons/mapicon_dungeon_dlc.svg', 10 | iconSize: L.point(36, 36), 11 | className: 'mapicon-Dungeon', 12 | }); 13 | 14 | 15 | export const VILLAGE = L.icon({ 16 | iconUrl: '/icons/mapicon_village.svg', 17 | iconSize: L.point(32, 32), 18 | tooltipAnchor: [0, 20], 19 | }); 20 | export const CHECKPOINT = L.icon({ 21 | iconUrl: '/icons/mapicon_checkpoint.svg', 22 | iconSize: L.point(26, 26), 23 | tooltipAnchor: [0, 18], 24 | }); 25 | export const HATAGO = L.icon({ 26 | iconUrl: '/icons/mapicon_hatago.svg', 27 | iconSize: L.point(32, 32), 28 | tooltipAnchor: [0, 20], 29 | }); 30 | export const CASTLE = L.icon({ 31 | iconUrl: '/icons/mapicon_castle.svg', 32 | iconSize: L.point(32, 32), 33 | tooltipAnchor: [0, 20], 34 | }); 35 | 36 | 37 | export const TOWER = L.icon({ 38 | iconUrl: '/icons/mapicon_tower.svg', 39 | iconSize: L.point(40, 40), 40 | className: 'mapicon-Tower', 41 | }); 42 | export const LABO = L.icon({ 43 | iconUrl: '/icons/mapicon_labo.svg', 44 | iconSize: L.point(32, 32), 45 | }); 46 | 47 | 48 | export const SHOP_BOUGU = L.icon({ 49 | iconUrl: '/icons/mapicon_shop_bougu.svg', 50 | iconSize: L.point(32, 32), 51 | }); 52 | export const SHOP_COLOR = L.icon({ 53 | iconUrl: '/icons/mapicon_shop_color.svg', 54 | iconSize: L.point(32, 32), 55 | }); 56 | export const SHOP_JEWEL = L.icon({ 57 | iconUrl: '/icons/mapicon_shop_jewel.svg', 58 | iconSize: L.point(32, 32), 59 | }); 60 | export const SHOP_YADOYA = L.icon({ 61 | iconUrl: '/icons/mapicon_shop_yadoya.svg', 62 | iconSize: L.point(32, 32), 63 | }); 64 | export const SHOP_YOROZU = L.icon({ 65 | iconUrl: '/icons/mapicon_shop_yorozu.svg', 66 | iconSize: L.point(32, 32), 67 | }); 68 | 69 | 70 | export const KOROK = L.icon({ 71 | iconUrl: '/icons/mapicon_korok.png', 72 | iconSize: L.point(20, 20), 73 | }); 74 | -------------------------------------------------------------------------------- /src/MapMarker.ts: -------------------------------------------------------------------------------- 1 | import * as L from 'leaflet'; 2 | 3 | import { rankUpEnemyForHardMode } from '@/level_scaling'; 4 | import { MapBase } from '@/MapBase'; 5 | import * as MapIcons from '@/MapIcon'; 6 | import { MapMgr, ObjectData, ObjectMinData } from '@/services/MapMgr'; 7 | import { MsgMgr } from '@/services/MsgMgr'; 8 | import { CanvasMarker, CanvasMarkerOptions } from '@/util/CanvasMarker'; 9 | import { Point } from '@/util/map'; 10 | import * as math from '@/util/math'; 11 | import * as map from '@/util/map'; 12 | import * as ui from '@/util/ui'; 13 | import { Settings } from '@/util/settings'; 14 | 15 | export abstract class MapMarker { 16 | public title = ''; 17 | public readonly mb: MapBase; 18 | 19 | constructor(mb: MapBase) { 20 | this.mb = mb; 21 | } 22 | 23 | abstract getMarker(): L.Marker | L.CircleMarker; 24 | shouldBeShown(): boolean { return true; } 25 | 26 | protected commonInit(): void { 27 | this.getMarker().on({ 'click': () => this.mb.emitMarkerSelectedEvent(this) }); 28 | } 29 | } 30 | 31 | class MapMarkerImpl extends MapMarker { 32 | constructor(mb: MapBase, title: string, xyz: Point, options: L.MarkerOptions = {}) { 33 | super(mb); 34 | this.title = title; 35 | this.marker = L.marker(this.mb.fromXYZ(xyz), Object.assign(options, { 36 | title, 37 | contextmenu: true, 38 | })); 39 | super.commonInit(); 40 | } 41 | 42 | getMarker() { return this.marker; } 43 | 44 | protected setTitle(title: string) { 45 | this.title = title; 46 | this.marker.options.title = title; 47 | } 48 | 49 | protected marker: L.Marker; 50 | } 51 | 52 | class MapMarkerCanvasImpl extends MapMarker { 53 | constructor(mb: MapBase, title: string, pos: Point, options: CanvasMarkerOptions = {}) { 54 | super(mb); 55 | this.title = title; 56 | let extra: any = {}; 57 | if (options.showLabel) { 58 | extra['permanent'] = true; 59 | } 60 | if (options.className) { 61 | extra['className'] = options.className; 62 | } 63 | this.marker = new CanvasMarker(mb.fromXYZ(pos), Object.assign(options, { 64 | bubblingMouseEvents: false, 65 | contextmenu: true, 66 | })); 67 | this.marker.bindTooltip(title, { pane: 'front2', ...extra }); 68 | super.commonInit(); 69 | } 70 | 71 | getMarker() { return this.marker; } 72 | 73 | protected marker: L.CircleMarker; 74 | } 75 | 76 | class MapMarkerGenericLocationMarker extends MapMarkerImpl { 77 | public readonly lm: map.LocationMarker; 78 | 79 | private static ICONS_AND_LABELS: { [type: string]: [L.Icon, string] } = { 80 | 'Village': [MapIcons.VILLAGE, ''], 81 | 'Hatago': [MapIcons.HATAGO, ''], 82 | 'Castle': [MapIcons.CASTLE, ''], 83 | 'CheckPoint': [MapIcons.CHECKPOINT, ''], 84 | 'Tower': [MapIcons.TOWER, ''], 85 | 'Labo': [MapIcons.LABO, ''], 86 | 'Dungeon': [MapIcons.DUNGEON, ''], 87 | 'ShopBougu': [MapIcons.SHOP_BOUGU, 'Armor Shop'], 88 | 'ShopColor': [MapIcons.SHOP_COLOR, 'Dye Shop'], 89 | 'ShopJewel': [MapIcons.SHOP_JEWEL, 'Jewelry Shop'], 90 | 'ShopYadoya': [MapIcons.SHOP_YADOYA, 'Inn'], 91 | 'ShopYorozu': [MapIcons.SHOP_YOROZU, 'General Store'], 92 | }; 93 | 94 | constructor(mb: MapBase, l: any, showLabel: boolean, zIndexOffset?: number) { 95 | const lm = new map.LocationMarker(l); 96 | const [icon, label] = MapMarkerGenericLocationMarker.ICONS_AND_LABELS[lm.getIcon()]; 97 | const msgId = lm.getMessageId(); 98 | const msg = msgId ? MsgMgr.getInstance().getMsgWithFile('StaticMsg/LocationMarker', msgId) : label; 99 | super(mb, msg, lm.getXYZ(), { 100 | icon, 101 | zIndexOffset, 102 | }); 103 | if (showLabel) { 104 | this.marker.bindTooltip(msg, { 105 | permanent: true, 106 | direction: 'center', 107 | className: `map-marker type-${lm.getIcon()}`, 108 | }); 109 | } 110 | this.lm = lm; 111 | } 112 | } 113 | 114 | export class MapMarkerPlateauRespawnPos extends MapMarkerCanvasImpl { 115 | constructor(mb: MapBase, pos: Point) { 116 | super(mb, 'Plateau Respawn Location', pos, { 117 | fillColor: '#ff0000', 118 | fill: true, 119 | fillOpacity: 1, 120 | stroke: false, 121 | radius: 5, 122 | }); 123 | } 124 | } 125 | 126 | export class MapMarkerLocation extends MapMarkerCanvasImpl { 127 | public readonly lp: map.LocationPointer; 128 | 129 | constructor(mb: MapBase, l: any) { 130 | const lp = new map.LocationPointer(l); 131 | const markerTypeStr = map.markerTypetoStr(lp.getType()); 132 | const visibleMarkerTypeStr = l.PointerType ? 'Place' : markerTypeStr; 133 | const msg = MsgMgr.getInstance().getMsgWithFile('StaticMsg/LocationMarker', lp.getMessageId()); 134 | 135 | super(mb, msg, lp.getXYZ(), { stroke: false, fill: false }); 136 | this.marker.unbindTooltip(); 137 | this.marker.bindTooltip(msg + `${visibleMarkerTypeStr}`, { 138 | permanent: true, 139 | direction: 'center', 140 | className: `map-location show-level-${lp.getShowLevel()} type-${markerTypeStr}`, 141 | }); 142 | this.lp = lp; 143 | } 144 | 145 | shouldBeShown() { 146 | return this.lp.shouldShowAtZoom(this.mb.zoom); 147 | } 148 | } 149 | 150 | export class MapMarkerDungeon extends MapMarkerGenericLocationMarker { 151 | public readonly dungeonNum: number; 152 | 153 | constructor(mb: MapBase, l: any) { 154 | super(mb, l, false, 1000); 155 | // Yes, extracting the dungeon number from the save flag is what Nintendo does. 156 | const dungeonNum = parseInt(this.lm.getSaveFlag().replace('Location_Dungeon', ''), 10); 157 | this.marker.setIcon(dungeonNum >= 120 ? MapIcons.DUNGEON_DLC : MapIcons.DUNGEON); 158 | this.setTitle(MsgMgr.getInstance().getMsgWithFile('StaticMsg/Dungeon', this.lm.getMessageId())); 159 | this.marker.options.title = ''; 160 | this.dungeonNum = dungeonNum; 161 | const sub = MsgMgr.getInstance().getMsgWithFile('StaticMsg/Dungeon', this.lm.getMessageId() + '_sub'); 162 | this.marker.bindTooltip(`${this.title}
${sub}`, { pane: 'front2' }); 163 | } 164 | } 165 | 166 | export class MapMarkerDungeonDLC extends MapMarkerDungeon { 167 | constructor(mb: MapBase, l: any) { 168 | super(mb, l); 169 | } 170 | } 171 | 172 | export class MapMarkerPlace extends MapMarkerGenericLocationMarker { 173 | private isVillage: boolean; 174 | 175 | constructor(mb: MapBase, l: any) { 176 | const isVillage = l['Icon'] == 'Village'; 177 | super(mb, l, isVillage); 178 | this.isVillage = isVillage; 179 | } 180 | 181 | shouldBeShown() { 182 | if (this.isVillage) 183 | return this.mb.zoom < 7; 184 | return true; 185 | } 186 | } 187 | 188 | export class MapMarkerTower extends MapMarkerGenericLocationMarker { 189 | constructor(mb: MapBase, l: any) { 190 | super(mb, l, false, 1001); 191 | } 192 | } 193 | 194 | export class MapMarkerLabo extends MapMarkerGenericLocationMarker { 195 | constructor(mb: MapBase, l: any) { 196 | super(mb, l, false); 197 | } 198 | } 199 | 200 | export class MapMarkerShop extends MapMarkerGenericLocationMarker { 201 | constructor(mb: MapBase, l: any) { 202 | super(mb, l, false); 203 | } 204 | 205 | shouldBeShown() { 206 | return this.mb.zoom >= 7; 207 | } 208 | } 209 | 210 | const KOROK_ICON = (() => { 211 | const img = new Image(); 212 | img.src = '/icons/mapicon_korok.png'; 213 | return img; 214 | })(); 215 | 216 | export class MapMarkerKorok extends MapMarkerCanvasImpl { 217 | public readonly info: any; 218 | 219 | constructor(mb: MapBase, info: any, extra: any) { 220 | let id = info.id || 'Korok'; 221 | super(mb, `${id}`, [info.Translate.X, info.Translate.Y, info.Translate.Z], { 222 | icon: KOROK_ICON, 223 | iconWidth: 20, 224 | iconHeight: 20, 225 | showLabel: extra.showLabel, 226 | className: classToColor(id), 227 | }); 228 | this.info = info; 229 | // @ts-ignore 230 | this.obj = info; 231 | } 232 | } 233 | 234 | // Convert first letter of Korok ID to CSS classname 235 | function classToColor(id: string): string { 236 | let classes: any = { 237 | 'A': 'akkala', 238 | 'C': 'central', 239 | 'E': 'eldin', 240 | 'D': 'duelingpeaks', 241 | 'F': 'faron', 242 | 'G': 'gerudo', 243 | 'H': 'hebra', 244 | 'K': 'woodland', 245 | 'L': 'lake', 246 | 'N': 'hateno', 247 | 'P': 'plateau', 248 | 'R': 'ridgeland', 249 | 'T': 'tabantha', 250 | 'W': 'wasteland', 251 | 'X': 'castle', 252 | 'Z': 'lanayru', 253 | }; 254 | if (id[0] in classes) { 255 | return classes[id[0]] + ' korok'; 256 | } 257 | return 'default'; 258 | } 259 | 260 | function getName(name: string) { 261 | if (Settings.getInstance().useActorNames) 262 | return name; 263 | return MsgMgr.getInstance().getName(name) || name; 264 | } 265 | 266 | function setObjMarkerTooltip(title: string, layer: L.Layer, obj: ObjectMinData) { 267 | const tooltipInfo = [title]; 268 | if (obj.name === 'LocationTag' && obj.messageid) { 269 | const locationName = MsgMgr.getInstance().getMsgWithFile('StaticMsg/LocationMarker', obj.messageid) 270 | || MsgMgr.getInstance().getMsgWithFile('StaticMsg/Dungeon', obj.messageid); 271 | tooltipInfo.push(`${locationName}`); 272 | } 273 | if (obj.drop) { 274 | if (obj.drop[0] == 1) 275 | tooltipInfo.push(getName(obj.drop[1])); 276 | else if (obj.drop[0] == 2) 277 | tooltipInfo.push('Drop table: ' + obj.drop[1]); 278 | } 279 | if (obj.equip) { 280 | for (const e of obj.equip) 281 | tooltipInfo.push(getName(e)); 282 | } 283 | layer.setTooltipContent(tooltipInfo.join('
')); 284 | } 285 | 286 | function hashString(s: string) { 287 | // https://stackoverflow.com/a/7616484/1636285 288 | var hash = 0, i, chr; 289 | if (s.length === 0) return hash; 290 | for (i = 0; i < s.length; i++) { 291 | chr = s.charCodeAt(i); 292 | hash = ((hash << 5) - hash) + chr; 293 | hash |= 0; 294 | } 295 | return hash >>> 0; 296 | } 297 | 298 | export const enum SearchResultUpdateMode { 299 | UpdateStyle = 1 << 0, 300 | UpdateVisibility = 1 << 1, 301 | UpdateTitle = 1 << 2, 302 | } 303 | 304 | export class MapMarkerObj extends MapMarkerCanvasImpl { 305 | constructor(mb: MapBase, public readonly obj: ObjectMinData, fillColor: string, strokeColor: string) { 306 | super(mb, '', obj.pos, { 307 | radius: 7, 308 | weight: 2, 309 | fillOpacity: 0.7, 310 | fillColor, 311 | color: strokeColor, 312 | 313 | // @ts-ignore 314 | contextmenuItems: [ 315 | { 316 | text: 'Show no-revival area', 317 | callback: ({ latlng }: ui.LeafletContextMenuCbArg) => { 318 | const [x, y, z] = mb.toXYZ(latlng); 319 | const col = math.clamp(((x + 5000) / 1000) | 0, 0, 9); 320 | const row = math.clamp(((z + 4000) / 1000) | 0, 0, 7); 321 | 322 | let minx = (col - 1) * 1000 - 4500; 323 | let maxx = (col + 1) * 1000 - 4500; 324 | minx = math.clamp(minx, -5000, 5000); 325 | maxx = math.clamp(maxx, -5000, 5000); 326 | 327 | let minz = (row - 1) * 1000 - 3500; 328 | let maxz = (row + 1) * 1000 - 3500; 329 | minz = math.clamp(minz, -4000, 4000); 330 | maxz = math.clamp(maxz, -4000, 4000); 331 | 332 | const pt1 = mb.fromXYZ([minx, 0, minz]); 333 | const pt2 = mb.fromXYZ([maxx, 0, maxz]); 334 | const rect = L.rectangle(L.latLngBounds(pt1, pt2), { 335 | color: "#ff7800", 336 | weight: 2, 337 | // @ts-ignore 338 | contextmenu: true, 339 | contextmenuItems: [{ 340 | text: 'Hide no-revival area', 341 | callback: () => { rect.remove(); }, 342 | }], 343 | }); 344 | rect.addTo(mb.m); 345 | }, 346 | index: 0, 347 | }, 348 | { 349 | text: 'Show generation group', 350 | callback: ({ latlng }: ui.LeafletContextMenuCbArg) => { 351 | mb.m.fire('AppMap:show-gen-group', { 352 | mapType: this.obj.map_type, 353 | mapName: this.obj.map_name, 354 | hashId: this.obj.hash_id, 355 | }); 356 | }, 357 | index: 0, 358 | }, 359 | ], 360 | }); 361 | this.marker.bringToFront(); 362 | this.updateTitle(); 363 | } 364 | 365 | updateTitle() { 366 | const actor = (Settings.getInstance().hardMode && !this.obj.disable_rankup_for_hard_mode) 367 | ? rankUpEnemyForHardMode(this.obj.name) 368 | : this.obj.name; 369 | this.title = getName(actor); 370 | setObjMarkerTooltip(this.title, this.marker, this.obj); 371 | } 372 | 373 | update(groupFillColor: string, groupStrokeColor: string, mode: SearchResultUpdateMode) { 374 | if (mode & SearchResultUpdateMode.UpdateTitle) 375 | this.updateTitle(); 376 | 377 | if (mode & SearchResultUpdateMode.UpdateStyle) { 378 | let fillColor = groupFillColor; 379 | let color = groupStrokeColor; 380 | if (Settings.getInstance().colorPerActor) { 381 | fillColor = ui.genColor(1000, hashString(this.title) % 1000); 382 | color = ui.shadeColor(fillColor, -15); 383 | } 384 | 385 | this.marker.setStyle({ 386 | fillColor, 387 | color, 388 | }); 389 | } 390 | 391 | const radius = Math.min(Math.max(this.mb.zoom, 4), 7); 392 | this.marker.setRadius(radius); 393 | this.marker.setStyle({ 394 | weight: radius >= 5 ? 2 : 0, 395 | }); 396 | } 397 | } 398 | 399 | export class MapMarkerSearchResult extends MapMarkerObj { 400 | constructor(mb: MapBase, obj: ObjectMinData) { 401 | super(mb, obj, '#e02500', '#ff2a00'); 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/MapMarkerGroup.ts: -------------------------------------------------------------------------------- 1 | import * as L from 'leaflet'; 2 | import { MapMarker } from '@/MapMarker'; 3 | import { makeClusterGroup } from '@/util/leaflet_cluster'; 4 | 5 | export class MapMarkerGroup { 6 | public markerGroup: L.MarkerClusterGroup; 7 | private markers: MapMarker[] = []; 8 | private shownMarkers: boolean[] = []; 9 | private enableUpdates: boolean; 10 | private isInitialUpdate: boolean = true; 11 | 12 | constructor(markers: MapMarker[], preloadPad: number = 1, enableUpdates = true) { 13 | this.markerGroup = makeClusterGroup(preloadPad); 14 | this.markers = markers; 15 | this.enableUpdates = enableUpdates; 16 | } 17 | 18 | destroy() { 19 | this.markerGroup.remove(); 20 | this.markerGroup.clearLayers(); 21 | this.markers = []; 22 | this.shownMarkers = []; 23 | } 24 | 25 | addToMap(map: L.Map) { this.markerGroup.addTo(map); } 26 | removeFromMap(map: L.Map) { this.markerGroup.removeFrom(map); } 27 | showOnMap(map: L.Map, doShow: boolean) { 28 | if (doShow) 29 | this.addToMap(map); 30 | else 31 | this.removeFromMap(map); 32 | } 33 | 34 | update() { 35 | if (!this.enableUpdates && !this.isInitialUpdate) 36 | return; 37 | 38 | const addMarkers: Array = []; 39 | const remMarkers: Array = []; 40 | 41 | for (const [i, marker] of this.markers.entries()) { 42 | const shouldShow = marker.shouldBeShown(); 43 | if (shouldShow == this.shownMarkers[i]) 44 | continue; 45 | if (shouldShow) 46 | addMarkers.push(marker.getMarker()); 47 | else 48 | remMarkers.push(marker.getMarker()); 49 | this.shownMarkers[i] = shouldShow; 50 | } 51 | 52 | this.markerGroup.removeLayers(remMarkers); 53 | this.markerGroup.addLayers(addMarkers); 54 | this.isInitialUpdate = false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/MapSearch.ts: -------------------------------------------------------------------------------- 1 | import * as L from 'leaflet'; 2 | import {MapBase} from '@/MapBase'; 3 | import {SearchResultUpdateMode} from '@/MapMarker'; 4 | import * as MapMarkers from '@/MapMarker'; 5 | import {MapMgr, ObjectMinData} from '@/services/MapMgr'; 6 | import {Settings} from '@/util/settings'; 7 | import * as ui from '@/util/ui'; 8 | 9 | export interface SearchPreset { 10 | label: string; 11 | query: string; 12 | } 13 | 14 | export interface SearchPresetGroup { 15 | label: string; 16 | presets: SearchPreset[]; 17 | } 18 | 19 | const LAUNCHABLE_OBJS = `TwnObj_City_GoronPot_A_M_Act_01 20 | FldObj_BoardIron_A_01 21 | FldObj_FallingRock_* 22 | FldObj_KorokStoneLift_A_01 23 | FldObj_PushRock* 24 | Kibako* 25 | Obj_BoardIron_* 26 | Obj_BoxIron_* 27 | Obj_BreakBoxIron* 28 | Obj_LiftRock* 29 | Obj_RockCover 30 | Barrel`; 31 | 32 | function makeActorQuery(actors: string[]): string { 33 | return actors.map(x => `actor:^${x}`).join(' OR '); 34 | } 35 | 36 | function makeNameQuery(names: string[]): string { 37 | return names.map(x => `name:^"${x}"`).join(' OR '); 38 | } 39 | 40 | export const SEARCH_PRESETS: ReadonlyArray = Object.freeze([ 41 | { 42 | label: '', 43 | presets: [ 44 | {label: 'Treasure Chests', query: 'actor:^"TBox_"'}, 45 | {label: 'Arrows', query: 'Arrow'}, 46 | {label: 'Ore Deposits', query: 'name:"Ore Deposit"'}, 47 | {label: 'Weapons (excluding enemies)', query: 'Weapon_ NOT actor:^"Enemy_"'}, 48 | ], 49 | }, 50 | { 51 | label: '', 52 | presets: [ 53 | {label: 'Cooking Pots', query: 'actor:Item_CookSet'}, 54 | {label: 'Fruits', query: 'actor:^Item_Fruit_*'}, 55 | {label: 'Enduring Ingredients', query: makeNameQuery(['Endura', 'Tireless Frog'])}, 56 | {label: 'Fireproof Ingredients', query: makeNameQuery(['Fireproof', 'Smotherwing Butterfly'])}, 57 | {label: 'Hasty Ingredients', query: makeNameQuery(['Hightail Lizard', 'Hot-Footed Frog', 'Fleet-Lotus Seeds', 'Rushroom', 'Swift Carrot', 'Swift Violet'])}, 58 | {label: 'Hasty Ingredients (Lvl2 only)', query: makeNameQuery(['Hot-Footed Frog', 'Fleet-Lotus Seeds', 'Swift Violet'])}, 59 | {label: 'Hearty Ingredients', query: 'name:Hearty'}, 60 | {label: 'Mighty Ingredients', query: makeNameQuery(['Mighty', 'Razorshroom', 'Razorclaw Crab', 'Bladed Rhino Beetle']),}, 61 | {label: 'Tough Ingredients', query: makeNameQuery(['Ironshroom', 'Fortified Pumpkin', 'Rugged Rhino Beetle', 'Ironshell Crab', 'Armored', 'Armoranth'])}, 62 | ], 63 | }, 64 | { 65 | label: '', 66 | presets: [ 67 | {label: 'Blue Dye Materials', query: makeNameQuery(['Blue nightshade', 'Ice Keese', 'Chillshroom'])+' OR drop: "sapphire"'}, 68 | {label: 'Red Dye Materials', query: makeNameQuery(['Apple', 'Spicy Pepper', 'Hylian Shroom', 'Sunshroom','Fire Keese','Fire Chuchu'])}, 69 | {label: 'Yellow Dye Materials', query: makeNameQuery(['Mighty Bananas', 'Zapshroom','Bird Egg','Thunderwing Butterfly','Electric Darner','Energetic Rhino Beetle','Electric Lizalfos','Electric Keese','Electric Chuchu','Hinox'])+' OR drop:"Topaz"'}, 70 | {label: 'White Dye Materials', query: makeNameQuery(['Hylian Rice','Silent Princess','Fresh Milk','Ice Chuchu','Lynel'])+' OR drop:"Star Fragment" OR drop:"Diamond"'}, 71 | {label: 'Black Dye Materials', query: makeNameQuery(['Hearty Truffle','Big Hearty Truffle','Lynel'])+' OR drop:"Flint"'}, 72 | {label: 'Purple Dye Materials', query: makeNameQuery(['Rushroom','Swift Violet','Armoranth','Sunset Firefly','Bokoblin Guts'])}, 73 | {label: 'Green Dye Materials', query: makeNameQuery(['Hydromelon','Fleet-Lotus Seeds','Stamella Shroom','Hyrule Herb','Cane Sugar','Restless Cricket','Rugged Rhino Beetle','Hot-Footed Frog', 'Molduga'])+' OR '+makeActorQuery(['Enemy_Lizalfos_Middle','Enemy_Lizalfos_Senior','Enemy_Lizalfos_Junior',])}, 74 | {label: 'Light Blue Dye Materials', query: makeNameQuery(['Silent Shroom','Cool Safflina','Blue Moblin','Black Moblin'])+makeActorQuery(['Enemy_Chuchu_Junior','Enemy_Chuchu_Middle','Enemy_Chuchu_Senior'])}, 75 | {label: 'Navy Dye Materials', query: 'name:"Bladed Rhino Beetle" OR drop:"Luminous Stone"'}, 76 | {label: 'Orange Dye Materials', query: makeNameQuery(['Voltfruit', 'Endura Shroom', 'Swift Carrot', 'Fortified Pumpkin', 'Warm Safflina', 'Mighty Thistle', 'Courser Bee Honey'])+' OR drop:"Amber"'}, 77 | {label: 'Peach Dye Materials', query: makeNameQuery(['Wildberry', 'Big Hearty Radish', 'Hearty Radish', 'Rock Salt'])}, 78 | {label: 'Crimson Dye Materials', query: makeNameQuery(['Razorshroom', 'Chickaloo Tree Nut', 'Tireless Frog'])}, 79 | {label: 'Light Yellow Dye Materials', query: makeNameQuery(['Hearty Durian', 'Palm Fruit', 'Endura Carrot', 'Electric Safflina', 'Tabantha Wheat', 'Goat Butter', 'Bokoblin'])+' OR drop:"Opal"'}, 80 | {label: 'Brown Dye Materials', query: makeNameQuery(['Ironshroom', 'Acorn', 'Hightail Lizard', 'Hinox', 'Stalnox', 'Molduga'])}, 81 | {label: 'Gray Dye Materials', query: makeNameQuery(['Smotherwing Butterfly', 'Fireproof Lizard', 'Moblin', 'Lizalfos'])+' OR actor:"enemy_bokoblin_*"'}, 82 | ] 83 | }, 84 | { 85 | label: '', 86 | presets: [ 87 | {label: 'Memory Locations', query: 'name:"Memory"'}, 88 | {label: 'Goddess Statues', query: 'name:"Goddess Statue"'}, 89 | {label: 'Rafts', query: 'name:Raft'}, 90 | {label: 'Enemies', query: 'actor:^"Enemy_"'}, 91 | {label: 'BtB Enemies', query: '(' + makeActorQuery(['Enemy_Bokoblin', 'Enemy_Lizalfos', 'Enemy_Moriblin', 'Enemy_Giant', 'Enemy_Wizzrobe']) + ') NOT actor:bone'}, 92 | {label: 'Launchable Objects', query: makeActorQuery(LAUNCHABLE_OBJS.split('\n'))}, 93 | { label: 'Shrine Elevators', query: 'actor:EntranceElev*'}, 94 | { label: 'Zora Stone Monuments', query: 'actor:FldObj_RockZoraRelief' }, 95 | ], 96 | } 97 | ]); 98 | 99 | export class SearchExcludeSet { 100 | constructor(public query: string, public label: string, public hidden = false) { 101 | } 102 | 103 | size() { 104 | return this.ids.size; 105 | } 106 | 107 | ids: Set = new Set(); 108 | 109 | async init() { 110 | this.ids = new Set(await MapMgr.getInstance().getObjids(Settings.getInstance().mapType, Settings.getInstance().mapName, this.query)); 111 | } 112 | } 113 | 114 | export class SearchResultGroup { 115 | constructor(public query: string, public label: string, public enabled = true) { 116 | } 117 | 118 | size() { 119 | return this.markers.data ? this.markers.data.length : 0; 120 | } 121 | 122 | getMarkers() { 123 | return this.markers.data; 124 | } 125 | 126 | remove() { 127 | this.markerGroup.data.remove(); 128 | this.markerGroup.data.clearLayers(); 129 | this.shownMarkers = new ui.Unobservable([]); 130 | } 131 | 132 | update(mode: SearchResultUpdateMode, excludedSets: SearchExcludeSet[]) { 133 | const isExcluded = (marker: MapMarkers.MapMarkerObj) => { 134 | return excludedSets.some(set => set.ids.has(marker.obj.objid)); 135 | }; 136 | for (const [i, marker] of this.markers.data.entries()) { 137 | const shouldShow = mode & SearchResultUpdateMode.UpdateVisibility 138 | ? (this.enabled && !isExcluded(marker)) : this.shownMarkers.data[i]; 139 | if (shouldShow != this.shownMarkers.data[i]) { 140 | if (shouldShow) 141 | this.markerGroup.data.addLayer(marker.getMarker()); 142 | else 143 | this.markerGroup.data.removeLayer(marker.getMarker()); 144 | this.shownMarkers.data[i] = shouldShow; 145 | } 146 | if (shouldShow) 147 | marker.update(this.fillColor, this.strokeColor, mode); 148 | } 149 | } 150 | 151 | setObjects(map: MapBase, objs: ObjectMinData[]) { 152 | this.markers = new ui.Unobservable( 153 | objs.map(r => new MapMarkers.MapMarkerObj(map, r, this.fillColor, this.strokeColor))); 154 | this.markerGroup.data.clearLayers(); 155 | this.shownMarkers = new ui.Unobservable([]); 156 | } 157 | 158 | async init(map: MapBase) { 159 | this.fillColor = ui.shadeColor(ui.genColor(10, SearchResultGroup.COLOR_COUNTER++), -5); 160 | this.strokeColor = ui.shadeColor(this.fillColor, -20); 161 | this.markerGroup.data.addTo(map.m); 162 | if (!this.query) 163 | return; 164 | const results = await MapMgr.getInstance().getObjs(Settings.getInstance().mapType, Settings.getInstance().mapName, this.query); 165 | this.setObjects(map, results); 166 | } 167 | 168 | private static COLOR_COUNTER = 0; 169 | private markerGroup = new ui.Unobservable(L.layerGroup()); 170 | private markers = new ui.Unobservable([]); 171 | private shownMarkers: ui.Unobservable = new ui.Unobservable([]); 172 | private fillColor = ''; 173 | private strokeColor = ''; 174 | } 175 | -------------------------------------------------------------------------------- /src/assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/src/assets/background.png -------------------------------------------------------------------------------- /src/assets/fonts/Calamity-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/src/assets/fonts/Calamity-Bold.otf -------------------------------------------------------------------------------- /src/assets/fonts/Calamity-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/src/assets/fonts/Calamity-Regular.otf -------------------------------------------------------------------------------- /src/assets/fonts/Sheikah-Complete.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeldamods/objmap/8f8978bb17680a1a694d7a188cf2a7ee9440aabb/src/assets/fonts/Sheikah-Complete.ttf -------------------------------------------------------------------------------- /src/components/AppMap.vue: -------------------------------------------------------------------------------- 1 | 330 | 331 | 332 | 497 | -------------------------------------------------------------------------------- /src/components/AppMapDetailsBase.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { Prop } from 'vue-property-decorator'; 3 | import Component, { mixins } from 'vue-class-component'; 4 | 5 | import MixinUtil from '@/components/MixinUtil'; 6 | import { ObjectMinData } from '@/services/MapMgr'; 7 | import * as ui from '@/util/ui'; 8 | 9 | @Component({ 10 | watch: { 11 | // @ts-ignore 12 | marker: function() { this.init(); }, 13 | } 14 | }) 15 | export default class AppMapDetailsBase extends mixins(MixinUtil) { 16 | @Prop() 17 | protected marker!: ui.Unobservable; 18 | protected init() { } 19 | 20 | private created() { 21 | this.init(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/AppMapDetailsDungeon.ts: -------------------------------------------------------------------------------- 1 | import Component from 'vue-class-component'; 2 | 3 | import { MapMarkerDungeon } from '@/MapMarker'; 4 | import AppMapDetailsBase from '@/components/AppMapDetailsBase'; 5 | import ObjectInfo from '@/components/ObjectInfo'; 6 | import { MapMgr, ObjectMinData } from '@/services/MapMgr'; 7 | import { MsgMgr } from '@/services/MsgMgr'; 8 | import * as ui from '@/util/ui'; 9 | 10 | @Component({ 11 | components: { 12 | ObjectInfo, 13 | }, 14 | }) 15 | export default class AppMapDetailsDungeon extends AppMapDetailsBase { 16 | private id = ''; 17 | private sub = ''; 18 | private tboxObjs: ObjectMinData[] = []; 19 | private enemies: ObjectMinData[] = []; 20 | 21 | protected init() { 22 | this.id = this.marker.data.lm.getMessageId(); 23 | this.sub = MsgMgr.getInstance().getMsgWithFile('StaticMsg/Dungeon', this.id + '_sub'); 24 | 25 | MapMgr.getInstance().getObjs('CDungeon', this.id, 'actor:^"TBox_"').then(d => this.tboxObjs = d); 26 | MapMgr.getInstance().getObjs('CDungeon', this.id, 'actor:^"Enemy_"').then(d => this.enemies = d); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/AppMapDetailsDungeon.vue: -------------------------------------------------------------------------------- 1 | 21 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/AppMapDetailsObj.vue: -------------------------------------------------------------------------------- 1 | 116 | 148 | 149 | -------------------------------------------------------------------------------- /src/components/AppMapDetailsPlace.ts: -------------------------------------------------------------------------------- 1 | import Component from 'vue-class-component'; 2 | 3 | import { MapMarkerPlace } from '@/MapMarker'; 4 | import AppMapDetailsBase from '@/components/AppMapDetailsBase'; 5 | import ObjectInfo from '@/components/ObjectInfo'; 6 | import ShopData from '@/components/ShopData'; 7 | import { MapMgr, ObjectMinData } from '@/services/MapMgr'; 8 | import { MsgMgr } from '@/services/MsgMgr'; 9 | import * as ui from '@/util/ui'; 10 | 11 | 12 | @Component({ 13 | components: { 14 | ObjectInfo, 15 | ShopData, 16 | }, 17 | }) 18 | export default class AppMapDetailsPlace extends AppMapDetailsBase { 19 | private id = ''; 20 | private sub = ''; 21 | private shopData: any = {}; 22 | private minobj: ObjectMinData | null = null; 23 | private shrine: any | null = null; 24 | private shrineSub: any | null = null; 25 | private shrineObj: any | null = null; 26 | private tower: any | null = null 27 | private shrines: any = { 28 | 'Woodland Stable': 'Dungeon056', 29 | 'East Akkala Stable': 'Dungeon013', 30 | 'South Akkala Stable': 'Dungeon048', 31 | 'Foothill Stable': 'Dungeon031', 32 | 'Wetland Stable': 'Dungeon049', 33 | 'Riverside Stable': 'Dungeon057', 34 | 'Dueling Peaks Stable': 'Dungeon045', 35 | 'Lakeside Stable': 'Dungeon050', 36 | 'Highland Stable': 'Dungeon054', 37 | 'Gerudo Canyon Stable': 'Dungeon010', 38 | 'Outskirt Stable': 'Dungeon027', 39 | 'Tabantha Bridge Stable': 'Dungeon037', 40 | 'Serenne Stable': 'Dungeon011', 41 | 'Snowfield Stable': 'Dungeon042', 42 | 'Rito Stable': 'Dungeon008', 43 | } 44 | 45 | protected async init() { 46 | this.id = this.marker.data.lm.getMessageId(); 47 | this.sub = MsgMgr.getInstance().getMsgWithFile('StaticMsg/LocationMarker', this.id); 48 | this.shopData = {}; 49 | if (this.sub.includes('Stable') || this.sub == 'Kara Kara Bazaar') { 50 | this.shopData = await MapMgr.getInstance().getObjShopData(); 51 | } 52 | 53 | MapMgr.getInstance().getObjs('MainField', '', this.id + ' actor: LocationTag').then(d => { 54 | this.minobj = d[0]; 55 | }); 56 | this.shrine = MsgMgr.getInstance().getMsgWithFile('StaticMsg/Dungeon', this.shrines[this.sub]); 57 | this.shrineSub = MsgMgr.getInstance().getMsgWithFile('StaticMsg/Dungeon', this.shrines[this.sub] + '_sub'); 58 | MapMgr.getInstance().getObjs('MainField', '', this.shrines[this.sub] + ' actor: LocationTag') 59 | .then(d => this.shrineObj = d[0]); 60 | 61 | } 62 | 63 | shopDataExists() { 64 | return Object.keys(this.shopData).length > 0; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/AppMapDetailsPlace.vue: -------------------------------------------------------------------------------- 1 | 27 | 29 | 30 | -------------------------------------------------------------------------------- /src/components/AppMapFilterMainButton.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { Prop } from 'vue-property-decorator'; 3 | import Component from 'vue-class-component'; 4 | 5 | import { Settings } from '@/util/settings'; 6 | 7 | @Component 8 | export default class AppMapFilterMainButton extends Vue { 9 | @Prop({ default: '', type: String }) 10 | private icon!: string; 11 | @Prop({ type: String, required: true }) 12 | private label!: string; 13 | @Prop({ type: String, required: true }) 14 | private type!: string; 15 | 16 | private active = false; 17 | 18 | created() { 19 | this.active = Settings.getInstance().shownGroups.has(this.type); 20 | } 21 | 22 | private onClick() { 23 | this.active = !this.active; 24 | if (this.active) { 25 | Settings.getInstance().shownGroups.add(this.type); 26 | } else { 27 | Settings.getInstance().shownGroups.delete(this.type); 28 | } 29 | this.$emit('toggle'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/AppMapFilterMainButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 52 | 53 | -------------------------------------------------------------------------------- /src/components/AppMapPopup.ts: -------------------------------------------------------------------------------- 1 | 2 | import Vue from 'vue'; 3 | import Component from 'vue-class-component'; 4 | 5 | @Component({ 6 | props: { 7 | title: String, 8 | text: String, 9 | pathLength: Number, 10 | }, 11 | watch: { 12 | title: function(new_val: string, old_val: string) { 13 | this.$emit('title', new_val); 14 | }, 15 | text: function(new_val: string, old_val: string) { 16 | this.$emit('text', new_val); 17 | } 18 | }, 19 | }) 20 | 21 | export default class AppMapPopup extends Vue { } 22 | -------------------------------------------------------------------------------- /src/components/AppMapPopup.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/AppMapSettings.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { Prop } from 'vue-property-decorator'; 3 | import Component from 'vue-class-component'; 4 | 5 | import { MsgMgr } from '@/services/MsgMgr'; 6 | import { Settings } from '@/util/settings'; 7 | 8 | function makeMainFieldDungeonEntry(mapName: string) { 9 | const text = MsgMgr.getInstance().getMsg(`StaticMsg/LocationMarker:${mapName}`); 10 | return { value: mapName, text: `${text} (${mapName})` }; 11 | } 12 | 13 | function makeCDungeonEntry(n: number) { 14 | const mapName = 'Dungeon' + n.toString().padStart(3, '0'); 15 | const text = MsgMgr.getInstance().getMsg(`StaticMsg/Dungeon:${mapName}`); 16 | const sub = MsgMgr.getInstance().getMsg(`StaticMsg/Dungeon:${mapName}_sub`); 17 | return { value: mapName, text: `${text} (${mapName} - ${sub})` }; 18 | } 19 | 20 | @Component 21 | export default class AppMapSettings extends Vue { 22 | colorMode: string = ''; 23 | s: Settings | null = null; 24 | 25 | optionsMapType = Object.freeze([ 26 | { value: 'MainField', text: 'Hyrule (MainField)' }, 27 | { value: 'MainFieldDungeon', text: 'Divine Beasts (MainFieldDungeon)' }, 28 | { value: 'CDungeon', text: 'Shrines (CDungeon)' }, 29 | { value: 'AocField', text: 'Trial of the Sword (AocField)' }, 30 | ]); 31 | 32 | optionsMapNameForMapType: { [type: string]: any } = Object.freeze({ 33 | 'MainField': [ 34 | { value: '', text: 'All' }, 35 | ], 36 | 'MainFieldDungeon': [{ value: '', text: 'All' }].concat(['RemainsWind', 'RemainsWater', 'RemainsElectric', 'RemainsFire', 'FinalTrial'].map(makeMainFieldDungeonEntry)), 37 | 'CDungeon': [{ value: '', text: 'All' }].concat([...Array(136).keys()].map(makeCDungeonEntry)), 38 | 'AocField': [ 39 | { value: '', text: 'All' }, 40 | ], 41 | }); 42 | 43 | created() { 44 | this.s = Settings.getInstance(); 45 | Settings.getInstance().registerCallback(() => this.loadSettings()); 46 | this.loadSettings(); 47 | } 48 | 49 | toggleY() { 50 | this.$parent.$emit('AppMap:toggle-y-values'); 51 | } 52 | 53 | private loadSettings() { 54 | this.colorMode = Settings.getInstance().colorPerActor ? 'per-actor' : 'per-group'; 55 | } 56 | 57 | private onColorModeChange(mode: string) { 58 | Settings.getInstance().colorPerActor = mode === 'per-actor'; 59 | } 60 | 61 | private resetMapName() { 62 | this.s!.mapName = this.optionsMapNameForMapType[this.s!.mapType][0].value; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/AppMapSettings.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/AppMapSidebar.less: -------------------------------------------------------------------------------- 1 | .leaflet-sidebar-left { 2 | left: 0; 3 | } 4 | 5 | .leaflet-sidebar-right { 6 | right: 0; 7 | .leaflet-sidebar-header { 8 | padding-left: 20px; 9 | } 10 | .leaflet-sidebar-close { 11 | left: auto; 12 | right: 0; 13 | } 14 | } 15 | 16 | #sidebar { 17 | box-sizing: content-box; 18 | font-family: Calamity; 19 | font-size: 15px; 20 | text-rendering: optimizeLegibility; 21 | z-index: 1000; 22 | top: 0; 23 | bottom: 0; 24 | border: 0; 25 | border-radius: 0; 26 | 27 | .form-control { 28 | background-color: rgba(255, 255, 255, 0.3); 29 | border: 1px solid rgba(255, 255, 255, 0.2); 30 | line-height: 3; 31 | } 32 | .form-control::placeholder { 33 | color: #aaa; 34 | } 35 | hr { 36 | border-color: rgba(255, 255, 255, 0.2); 37 | } 38 | .leaflet-sidebar-tabs { 39 | background: rgba(0, 0, 0, 0.6); 40 | } 41 | .leaflet-sidebar-tabs > li, .leaflet-sidebar-tabs > ul > li { 42 | color: rgba(255, 255, 255, 0.8); 43 | } 44 | .leaflet-sidebar-tabs > li:hover, .leaflet-sidebar-tabs > ul > li:hover { 45 | background: rgba(0, 0, 0, 0.3) !important; 46 | color: white; 47 | } 48 | .leaflet-sidebar-tabs > li.active, .leaflet-sidebar-tabs > ul > li.active { 49 | background-color: inherit; 50 | color: #29abfc; 51 | } 52 | .leaflet-sidebar-tabs > li.disabled, .leaflet-sidebar-tabs > ul > li.disabled { 53 | color: rgba(255, 255, 255, 0.3); 54 | pointer-events: none; 55 | } 56 | .leaflet-sidebar-content { 57 | background: rgba(0, 0, 0, 0.6); 58 | color: white; 59 | text-shadow: 0 0 20px #ffffff9f; 60 | padding-top: 0.5em; 61 | 62 | a:not(.btn), a:not(.btn):active, a:not(.btn):visited, .btn-link { 63 | color: #29d1fc; 64 | text-decoration: none; 65 | } 66 | 67 | .btn-link:hover { 68 | color: #8ce7ff; 69 | } 70 | 71 | .dropdown-menu { 72 | font-size: 14px; 73 | } 74 | 75 | .dropdown-menu a { 76 | color: #212529 !important; 77 | padding: 0.25rem 1rem; 78 | } 79 | } 80 | .leaflet-sidebar-header { 81 | margin-bottom: 0.5em; 82 | background-color: inherit; 83 | text-shadow: 0 0 30px #ffffffbf; 84 | font-size: 20px; 85 | font-family: CalamityB; 86 | } 87 | .leaflet-sidebar-close { 88 | padding-top: 7px; 89 | font-size: 18px; 90 | } 91 | } 92 | 93 | // Modified from https://codepen.io/Spemer/pen/baoKPK 94 | .leaflet-sidebar-content::-webkit-scrollbar { 95 | background-color: transparent; 96 | width: 8px; 97 | } 98 | 99 | .leaflet-sidebar-content::-webkit-scrollbar-thumb { 100 | background-color:#babac0; 101 | border-radius:16px; 102 | } 103 | 104 | .leaflet-sidebar-content::-webkit-scrollbar-thumb:hover { 105 | background-color:#a0a0a5; 106 | border:4px solid #f4f4f4 107 | } 108 | .leaflet-sidebar-content::-webkit-scrollbar-button {display:none} 109 | 110 | .subsection-heading { 111 | font-family: CalamityB; 112 | font-size: 16px; 113 | } 114 | 115 | .location-title { 116 | font-size: 22px !important; 117 | color: #b7f1ff; 118 | text-shadow: 0 0 20px #3aa0ff, 0 0 20px #3aa0ff, 0 0 20px #3aa0ff !important; 119 | margin-top: 10px; 120 | margin-bottom: 3px !important; 121 | white-space: nowrap; 122 | span { 123 | padding-bottom: 7px; 124 | border-bottom: 1px solid #b7f1ff3f; 125 | } 126 | } 127 | 128 | .location-sub { 129 | font-size: 15px !important; 130 | color: #b7f1ff; 131 | text-shadow: 0 0 16px #39c7ff, 0 0 16px #39c7ff, 0 0 16px #39c7ff !important; 132 | font-size: 14px; 133 | } 134 | 135 | /* Search */ 136 | .search-groups { 137 | margin-bottom: 5px; 138 | font-size: 95%; 139 | } 140 | -------------------------------------------------------------------------------- /src/components/MixinUtil.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | 4 | import { rankUpEnemyForHardMode } from '@/level_scaling'; 5 | import { ObjectMinData } from '@/services/MapMgr'; 6 | import { MsgMgr } from '@/services/MsgMgr'; 7 | import { Settings } from '@/util/settings'; 8 | 9 | @Component 10 | export default class MixinUtil extends Vue { 11 | getName(name: string) { 12 | if (Settings.getInstance().useActorNames) 13 | return name; 14 | return MsgMgr.getInstance().getName(name) || name; 15 | } 16 | 17 | isTestOfStrengthShrine(obj: ObjectMinData) { 18 | if (obj.map_type != "CDungeon") 19 | return false 20 | if (!obj.map_name) 21 | return false 22 | if (obj.map_name.startsWith("Dungeon07") || obj.map_name.startsWith("Dungeon08")) 23 | return true 24 | if (obj.map_name == "Dungeon135") 25 | return true 26 | return false 27 | } 28 | 29 | getRankedUpActorNameForObj(obj: ObjectMinData) { 30 | if (!Settings.getInstance().hardMode || obj.disable_rankup_for_hard_mode) 31 | return obj.name; 32 | if (this.isTestOfStrengthShrine(obj)) 33 | return obj.name; 34 | return rankUpEnemyForHardMode(obj.name); 35 | } 36 | 37 | getMapNameForObj(obj: ObjectMinData) { 38 | if (obj.map_type == 'CDungeon') { 39 | const uiName = MsgMgr.getInstance().getMsg(`StaticMsg/Dungeon:${obj.map_name}`); 40 | return `${uiName} (${obj.map_name})`; 41 | } 42 | 43 | if (obj.map_type == 'MainFieldDungeon') { 44 | const uiName = MsgMgr.getInstance().getMsg(`StaticMsg/LocationMarker:${obj.map_name}`); 45 | return `${uiName} (${obj.map_name})`; 46 | } 47 | 48 | return obj.map_name; 49 | } 50 | 51 | getMapStaticStringForObj(obj: ObjectMinData) { 52 | return obj.map_static ? 'Static' : 'Dynamic'; 53 | } 54 | 55 | isActuallyRankedUp(obj: ObjectMinData) { 56 | return this.getRankedUpActorNameForObj(obj) != obj.name; 57 | } 58 | 59 | formatObjId(id: number) { 60 | if (!Settings.getInstance().useHexForHashIds) 61 | return id.toString(10); 62 | return '0x' + id.toString(16).padStart(8, '0'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/ModalGotoCoords.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Component from 'vue-class-component'; 3 | 4 | import * as map from '@/util/map'; 5 | 6 | @Component 7 | export default class ModalGotoCoords extends Vue { 8 | private x: string = ""; 9 | private z: string = ""; 10 | 11 | show() { 12 | // @ts-ignore 13 | this.$refs.modalGoto.show(); 14 | } 15 | hide() { 16 | // @ts-ignore 17 | this.$refs.modalGoto.hide(); 18 | } 19 | 20 | private onPaste(evt: ClipboardEvent) { 21 | if (!evt.clipboardData) 22 | return; 23 | let X, Z; 24 | const data = evt.clipboardData.getData('text'); 25 | const array = data.replace('[', '').replace(']', '').replace(' ', '').split(','); 26 | if (array.length === 2) 27 | [X, Z] = array; 28 | else if (array.length === 3) 29 | [X, , Z] = array; 30 | 31 | if (X !== undefined && Z !== undefined) { 32 | this.x = X; 33 | this.z = Z; 34 | evt.preventDefault(); 35 | } 36 | } 37 | 38 | private onSubmit() { 39 | const x = parseFloat(this.x); 40 | const z = parseFloat(this.z); 41 | if (isNaN(x) || isNaN(z) || !map.isValidXYZ(x, 0, z)) { 42 | alert("Invalid coordinates"); 43 | return; 44 | } 45 | this.$emit('submitted', [x, 0, z]); 46 | this.x = this.z = ""; 47 | this.hide(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/ModalGotoCoords.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/ModalPane.less: -------------------------------------------------------------------------------- 1 | .modal.modalpane { 2 | background: none; 3 | 4 | .modal-dialog { 5 | position: fixed; 6 | margin: auto; 7 | width: 100%; 8 | height: 200px; 9 | max-width: 100%; 10 | -webkit-transform: translate3d(0%, 0, 0); 11 | -ms-transform: translate3d(0%, 0, 0); 12 | -o-transform: translate3d(0%, 0, 0); 13 | transform: translate3d(0%, 0, 0); 14 | 15 | @media only screen and (max-width: 600px) { 16 | height: 100%; 17 | } 18 | } 19 | 20 | .modal-content { 21 | height: 100%; 22 | overflow-y: auto; 23 | border-radius: 0; 24 | 25 | background-color: #002645b0; 26 | color: #29abfc; 27 | text-shadow: 0 0 30px #29d1fc7f; 28 | } 29 | 30 | .modal-body { 31 | padding: 15px; 32 | } 33 | 34 | .modal-header { 35 | background-color: #01151fe0; 36 | font-family: Roboto; 37 | padding: 5px; 38 | border: 0; 39 | 40 | .modal-title { 41 | margin-left: auto; 42 | font-weight: 500; 43 | font-size: 16px; 44 | } 45 | 46 | .close { 47 | color: white; 48 | padding: .1rem .1rem; 49 | margin: -.2rem .2rem -.2rem auto; 50 | } 51 | } 52 | 53 | .modal-footer { 54 | display: none; 55 | } 56 | } 57 | 58 | .modal.modalpane.fade .modal-dialog { 59 | bottom: -200px; 60 | transition: opacity 0.3s linear, bottom 0.3s ease-out; 61 | } 62 | 63 | .modal.modalpane.fade.show .modal-dialog { 64 | bottom: 0; 65 | } 66 | -------------------------------------------------------------------------------- /src/components/ObjectInfo.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { Prop } from 'vue-property-decorator'; 3 | import Component, { mixins } from 'vue-class-component'; 4 | 5 | import { rankUpEnemyForHardMode } from '@/level_scaling'; 6 | import MixinUtil from '@/components/MixinUtil'; 7 | import { MsgMgr } from '@/services/MsgMgr'; 8 | import { ObjectData, ObjectMinData, PlacementLink } from '@/services/MapMgr'; 9 | import { Settings } from '@/util/settings'; 10 | 11 | @Component 12 | export default class ObjectInfo extends mixins(MixinUtil) { 13 | @Prop() 14 | private obj!: ObjectData | ObjectMinData | null; 15 | 16 | @Prop() 17 | private link!: PlacementLink | null; 18 | 19 | @Prop({ type: String, default: 'search-result' }) 20 | private className!: string; 21 | 22 | @Prop({ type: Boolean, default: true }) 23 | private isStatic!: boolean; 24 | 25 | @Prop({ type: Boolean, default: false }) 26 | private dropAsName!: boolean; 27 | 28 | @Prop({ type: Boolean, default: false }) 29 | private withPermalink!: boolean; 30 | 31 | private data!: ObjectData | ObjectMinData; 32 | 33 | private metadata: any | null = null; 34 | 35 | private created() { 36 | if ((!this.obj && !this.link) || (this.obj && this.link)) 37 | throw new Error('needs an object *or* a placement link'); 38 | 39 | if (this.link) 40 | this.data = this.link.otherObj; 41 | if (this.obj) 42 | this.data = this.obj; 43 | } 44 | 45 | async loadMetaIfNeeded() { 46 | if (!this.metadata) { 47 | const rname = this.getRankedUpActorNameForObj(this.data); 48 | this.metadata = await MsgMgr.getInstance().getObjectMetaData(rname); 49 | } 50 | } 51 | 52 | private meta(item: string) { 53 | this.loadMetaIfNeeded(); 54 | // Return values may still be null if metadata is not available 55 | return (this.metadata) ? this.metadata[item] : null; 56 | } 57 | 58 | 59 | private name(rankUp: boolean) { 60 | if (this.dropAsName) 61 | return this.drop(); 62 | 63 | const objName = this.data.name; 64 | if (objName === 'LocationTag' && this.data.messageid) { 65 | const locationName = MsgMgr.getInstance().getMsgWithFile('StaticMsg/LocationMarker', this.data.messageid) 66 | || MsgMgr.getInstance().getMsgWithFile('StaticMsg/Dungeon', this.data.messageid); 67 | return `Location: ${locationName}`; 68 | } 69 | 70 | return this.getName(rankUp ? this.getRankedUpActorNameForObj(this.data) : this.data.name); 71 | } 72 | 73 | private isHardMode() { 74 | return Settings.getInstance().hardMode; 75 | } 76 | 77 | private drop() { 78 | let s = ''; 79 | if (!this.data.drop) 80 | return s; 81 | 82 | s += this.data.drop[0] == 2 ? 'Drop table: ' : ''; 83 | s += this.getName(this.data.drop[1]); 84 | 85 | return s; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/components/ObjectInfo.vue: -------------------------------------------------------------------------------- 1 | 64 | 99 | 100 | -------------------------------------------------------------------------------- /src/components/ShopData.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Component, { mixins } from 'vue-class-component'; 3 | import { Prop } from 'vue-property-decorator'; 4 | import MixinUtil from '@/components/MixinUtil'; 5 | import * as ui from '@/util/ui'; 6 | 7 | @Component 8 | export default class ShopData extends mixins(MixinUtil) { 9 | @Prop() 10 | private data!: any; 11 | 12 | length() { 13 | return this.data.Normal.ColumnNum; 14 | } 15 | name(i: number) { 16 | const n = i.toString().padStart(3, '0') 17 | return ui.getName(this.data.Normal[`ItemName${n}`]); 18 | } 19 | num(i: number) { 20 | const n = i.toString().padStart(3, '0') 21 | return this.data.Normal[`ItemNum${n}`]; 22 | } 23 | price(i: number) { 24 | const n = i.toString().padStart(3, '0') 25 | return this.data.Normal[`ItemPrice${n}`]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ShopData.vue: -------------------------------------------------------------------------------- 1 | 15 | 28 | 29 | -------------------------------------------------------------------------------- /src/level_scaling.ts: -------------------------------------------------------------------------------- 1 | // source: aoc2::rankUpEnemy (0x7100D6F0FC Switch 1.5.0) 2 | const RANKUP_MAP: { [actorName: string]: string } = Object.freeze({ 3 | Enemy_Assassin_Junior: 'Enemy_Assassin_Middle', 4 | Enemy_Assassin_Middle: 'Enemy_Assassin_Senior', 5 | Enemy_Assassin_Shooter_Junior: 'Enemy_Assassin_Shooter_Azito_Junior', 6 | 7 | Enemy_Bokoblin_Junior: 'Enemy_Bokoblin_Middle', 8 | Enemy_Bokoblin_Middle: 'Enemy_Bokoblin_Senior', 9 | Enemy_Bokoblin_Senior: 'Enemy_Bokoblin_Dark', 10 | Enemy_Bokoblin_Dark: 'Enemy_Bokoblin_Gold', 11 | Enemy_Bokoblin_Guard_Junior: 'Enemy_Bokoblin_Guard_Middle', 12 | Enemy_Bokoblin_Guard_Junior_Ambush: 'Enemy_Bokoblin_Guard_Middle_Ambush', 13 | Enemy_Bokoblin_Guard_Junior_TreeHouseTop: 'Enemy_Bokoblin_Guard_Middle_TreeHouseTop', 14 | 15 | Enemy_Chuchu_Electric_Junior: 'Enemy_Chuchu_Electric_Middle', 16 | Enemy_Chuchu_Electric_Middle: 'Enemy_Chuchu_Electric_Senior', 17 | Enemy_Chuchu_Fire_Junior: 'Enemy_Chuchu_Fire_Middle', 18 | Enemy_Chuchu_Fire_Middle: 'Enemy_Chuchu_Fire_Senior', 19 | Enemy_Chuchu_Ice_Junior: 'Enemy_Chuchu_Ice_Middle', 20 | Enemy_Chuchu_Ice_Middle: 'Enemy_Chuchu_Ice_Senior', 21 | Enemy_Chuchu_Junior: 'Enemy_Chuchu_Middle', 22 | Enemy_Chuchu_Middle: 'Enemy_Chuchu_Senior', 23 | 24 | Enemy_Giant_Junior: 'Enemy_Giant_Middle', 25 | Enemy_Giant_Middle: 'Enemy_Giant_Senior', 26 | 27 | Enemy_Golem_Junior: 'Enemy_Golem_Middle', 28 | Enemy_Golem_Middle: 'Enemy_Golem_Senior', 29 | 30 | Enemy_Guardian_Mini_Baby: 'Enemy_Guardian_Mini_Junior', 31 | Enemy_Guardian_Mini_Junior: 'Enemy_Guardian_Mini_Middle', 32 | Enemy_Guardian_Mini_Middle: 'Enemy_Guardian_Mini_Senior', 33 | Enemy_Guardian_Mini_Junior_DetachLineBeam: 'Enemy_Guardian_Mini_Middle_DetachLineBeam', 34 | 35 | Enemy_Lizalfos_Junior: 'Enemy_Lizalfos_Middle', 36 | Enemy_Lizalfos_Middle: 'Enemy_Lizalfos_Senior', 37 | Enemy_Lizalfos_Senior: 'Enemy_Lizalfos_Dark', 38 | Enemy_Lizalfos_Dark: 'Enemy_Lizalfos_Gold', 39 | Enemy_Lizalfos_Guard_Junior: 'Enemy_Lizalfos_Guard_Middle', 40 | Enemy_Lizalfos_Guard_Junior_LongVisibility: 'Enemy_Lizalfos_Guard_Middle_LongVisibility', 41 | Enemy_Lizalfos_Junior_Guard_Ambush: 'Enemy_Lizalfos_Middle_Guard_Ambush', 42 | 43 | Enemy_Lynel_Junior: 'Enemy_Lynel_Middle', 44 | Enemy_Lynel_Middle: 'Enemy_Lynel_Senior', 45 | Enemy_Lynel_Senior: 'Enemy_Lynel_Dark', 46 | Enemy_Lynel_Dark: 'Enemy_Lynel_Gold', 47 | 48 | Enemy_Moriblin_Junior: 'Enemy_Moriblin_Middle', 49 | Enemy_Moriblin_Middle: 'Enemy_Moriblin_Senior', 50 | Enemy_Moriblin_Senior: 'Enemy_Moriblin_Dark', 51 | Enemy_Moriblin_Dark: 'Enemy_Moriblin_Gold', 52 | 53 | Enemy_Wizzrobe_Electric: 'Enemy_Wizzrobe_Electric_Senior', 54 | Enemy_Wizzrobe_Fire: 'Enemy_Wizzrobe_Fire_Senior', 55 | Enemy_Wizzrobe_Ice: 'Enemy_Wizzrobe_Ice_Senior', 56 | }); 57 | 58 | export function rankUpEnemyForHardMode(actorName: string): string { 59 | return RANKUP_MAP[actorName] || actorName; 60 | } 61 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import BootstrapVue from 'bootstrap-vue'; 2 | import 'bootstrap/dist/css/bootstrap.css'; 3 | import 'bootstrap-vue/dist/bootstrap-vue.css'; 4 | import '@fortawesome/fontawesome-free/css/fontawesome.css'; 5 | import '@fortawesome/fontawesome-free/css/regular.css'; 6 | import '@fortawesome/fontawesome-free/css/solid.css'; 7 | 8 | import Vue from 'vue'; 9 | import Component from 'vue-class-component' 10 | 11 | import App from './App.vue'; 12 | import router from './router'; 13 | 14 | import { MapMgr } from '@/services/MapMgr'; 15 | import { MsgMgr } from '@/services/MsgMgr'; 16 | 17 | async function main() { 18 | await initServices(); 19 | initUi(); 20 | } 21 | 22 | async function initServices() { 23 | await Promise.all([ 24 | MsgMgr.getInstance().init(), 25 | MapMgr.getInstance().init(), 26 | ]); 27 | } 28 | 29 | function initUi() { 30 | Vue.use(BootstrapVue); 31 | 32 | Vue.config.productionTip = false; 33 | 34 | Component.registerHooks([ 35 | 'beforeRouteEnter', 36 | 'beforeRouteLeave', 37 | 'beforeRouteUpdate', 38 | ]); 39 | 40 | new Vue({ 41 | router, 42 | render: h => h(App) 43 | }).$mount('#app'); 44 | } 45 | 46 | main(); 47 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | import AppMap from '@/components/AppMap.vue'; 5 | 6 | Vue.use(Router); 7 | 8 | export default new Router({ 9 | // mode: 'history', 10 | routes: [ 11 | { path: '/map', redirect: '/map/zx,0,0' }, 12 | { 13 | path: '/map/z:zoom,:x,:z', 14 | name: 'map', 15 | component: AppMap, 16 | }, 17 | { path: '*', redirect: '/map' }, 18 | ], 19 | }); 20 | -------------------------------------------------------------------------------- /src/save.ts: -------------------------------------------------------------------------------- 1 | export const CURRENT_OBJMAP_SV_VERSION = 3; 2 | 3 | export interface SearchGroup { 4 | label: string; 5 | query: string; 6 | enabled: boolean | undefined; 7 | } 8 | 9 | export interface SearchExcludeSet { 10 | label: string; 11 | query: string; 12 | } 13 | 14 | export interface SaveData { 15 | OBJMAP_SV_VERSION: number; 16 | drawData: GeoJSON.FeatureCollection; 17 | 18 | // v2+ 19 | searchGroups: SearchGroup[]; 20 | searchExcludeSets: SearchExcludeSet[]; 21 | } 22 | -------------------------------------------------------------------------------- /src/services/MapMgr.ts: -------------------------------------------------------------------------------- 1 | 2 | import { GAME_FILES } from '@/util/map'; 3 | 4 | const RADAR_URL = process.env.VUE_APP_RADAR_URL; 5 | 6 | export type Vec3 = [number, number, number]; 7 | 8 | export interface ResPlacementObj { 9 | readonly '!Parameters'?: { [key: string]: any }; 10 | readonly SRTHash: number; 11 | readonly HashId: number; 12 | readonly OnlyOne?: boolean; 13 | readonly UniqueName?: string; 14 | readonly UnitConfigName: string; 15 | readonly LinksToObj?: any; 16 | readonly LinksToRail?: any; 17 | readonly Translate: Vec3; 18 | readonly Scale?: Vec3 | number; 19 | readonly Rotate?: Vec3 | number; 20 | } 21 | 22 | export const enum ObjectDropType { 23 | Actor = 1, 24 | Table = 2, 25 | } 26 | 27 | export interface ObjectMinData { 28 | objid: number; 29 | hash_id: number; 30 | map_type: string; 31 | map_name?: string; 32 | map_static: boolean; 33 | name: string; 34 | drop?: [ObjectDropType, string]; 35 | equip?: string[]; 36 | pos: [number, number, number]; 37 | 38 | // False if not present. 39 | hard_mode?: boolean; 40 | one_hit_mode?: boolean; 41 | disable_rankup_for_hard_mode?: boolean; 42 | 43 | // Only for LocationTags. 44 | messageid?: string; 45 | 46 | // Only for weapons and enemies. 47 | scale?: number; 48 | sharp_weapon_judge_type?: number; 49 | 50 | korok_type?: string; 51 | korok_id?: string; 52 | } 53 | 54 | export interface ObjectData extends ObjectMinData { 55 | map_name: string; 56 | data: ResPlacementObj; 57 | } 58 | 59 | export class PlacementLink { 60 | constructor(public readonly otherObj: ObjectData, 61 | public readonly linkIter: any, 62 | public readonly ltype: string, 63 | ) { } 64 | } 65 | 66 | function parse(r: Response) { 67 | if (r.status == 404) 68 | return null; 69 | return r.json().then(d => Object.freeze(d)); 70 | } 71 | 72 | export class MapMgr { 73 | private static instance: MapMgr; 74 | static getInstance() { 75 | if (!this.instance) 76 | this.instance = new this(); 77 | return this.instance; 78 | } 79 | 80 | private infoMainField: any; 81 | 82 | async init() { 83 | await Promise.all([ 84 | fetch(`${GAME_FILES}/map_summary/MainField/static.json`).then(r => r.json()) 85 | .then((d) => { 86 | d.markers["DungeonDLC"] = d.markers["Dungeon"].filter((l: any) => parseInt(l.SaveFlag.replace('Location_Dungeon', ''), 10) >= 120); 87 | d.markers["Dungeon"] = d.markers["Dungeon"].filter((l: any) => parseInt(l.SaveFlag.replace('Location_Dungeon', ''), 10) < 120); 88 | this.infoMainField = Object.freeze(d); 89 | }), 90 | ]); 91 | } 92 | 93 | fetchAreaMap(name: string): Promise<{ [data: number]: Array }> { 94 | return fetch(`${GAME_FILES}/ecosystem/${name}.json`).then(parse); 95 | } 96 | 97 | getInfoMainField() { 98 | return this.infoMainField; 99 | } 100 | 101 | getObjByObjId(objid: number): Promise { 102 | return fetch(`${RADAR_URL}/obj/${objid}`).then(parse); 103 | } 104 | getObj(mapType: string, mapName: string, hashId: number): Promise { 105 | return fetch(`${RADAR_URL}/obj/${mapType}/${mapName}/${hashId}`).then(parse); 106 | } 107 | 108 | getObjGenGroup(mapType: string, mapName: string, hashId: number): Promise { 109 | return fetch(`${RADAR_URL}/obj/${mapType}/${mapName}/${hashId}/gen_group`).then(parse); 110 | } 111 | 112 | getObjShopData() { 113 | return fetch(`${GAME_FILES}/ecosystem/beedle_shop_data.json`).then(parse); 114 | } 115 | 116 | getObjDropTables(unitConfigName: string, tableName: string) { 117 | return fetch(`${RADAR_URL}/drop/${unitConfigName}/${tableName}`).then(parse); 118 | } 119 | 120 | getObjRails(hashId: number): Promise { 121 | return fetch(`${RADAR_URL}/rail/${hashId}`).then(parse); 122 | } 123 | 124 | getObjs(mapType: string, mapName: string, query: string, withMapNames = false, limit = -1): Promise { 125 | let url = new URL(`${RADAR_URL}/objs/${mapType}/${mapName}`); 126 | url.search = new URLSearchParams({ 127 | q: query, 128 | withMapNames: withMapNames.toString(), 129 | limit: limit.toString(), 130 | }).toString(); 131 | return fetch(url.toString()).then(parse); 132 | } 133 | 134 | getObjids(mapType: string, mapName: string, query: string): Promise { 135 | let url = new URL(`${RADAR_URL}/objids/${mapType}/${mapName}`); 136 | url.search = new URLSearchParams({ 137 | q: query, 138 | }).toString(); 139 | return fetch(url.toString()).then(parse); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/services/MsgMgr.ts: -------------------------------------------------------------------------------- 1 | import { GAME_FILES } from '@/util/map'; 2 | 3 | type File = { [label: string]: string }; 4 | 5 | export class MsgMgr { 6 | private static instance: MsgMgr; 7 | static getInstance() { 8 | if (!this.instance) 9 | this.instance = new this(); 10 | return this.instance; 11 | } 12 | 13 | private names: { [actorName: string]: string } = {}; 14 | private files: Map = new Map(); 15 | private climate: any | null = null; 16 | private area: any | null = null; 17 | private metadata: any | null = null; 18 | 19 | async init() { 20 | const PREFIX = `${GAME_FILES}/text/`; 21 | const fileList: string[] = await fetch(PREFIX + 'list.json').then(r => r.json()); 22 | const fileLoadPromises = []; 23 | for (const path of fileList) { 24 | const file = path.replace('.json', ''); 25 | fileLoadPromises.push(fetch(PREFIX + path).then(r => r.json()).then((d) => { 26 | this.files.set(file, Object.freeze(d)); 27 | })); 28 | } 29 | fileLoadPromises.push(fetch(`${GAME_FILES}/names.json`).then(r => r.json()).then((d) => { 30 | this.names = d; 31 | })); 32 | await Promise.all(fileLoadPromises); 33 | } 34 | 35 | private getFile(file: string) { 36 | return this.files.get(file); 37 | } 38 | 39 | getMsgWithFile(file: string, label: string): string { 40 | const f = this.getFile(file); 41 | return f === undefined ? "???" : f[label]; 42 | } 43 | 44 | async getAreaData(item: number) { 45 | if (!this.area) { 46 | const res = await fetch(`${GAME_FILES}/area_data.json`); 47 | this.area = await res.json(); 48 | } 49 | if (this.area) { 50 | return this.area.find((val: any) => val.AreaNumber == item); 51 | } 52 | return null; 53 | } 54 | 55 | async getClimateData(item: number) { 56 | if (!this.climate) { 57 | const res = await fetch(`${GAME_FILES}/climate_data.json`); 58 | this.climate = await res.json(); 59 | } 60 | if (this.climate) { 61 | return this.climate[`ClimateDefines_${item}`]; 62 | } 63 | return null; 64 | } 65 | 66 | async getObjectMetaData(item: string) { 67 | if (!this.metadata) { 68 | const res = await fetch(`${GAME_FILES}/object_meta.json`); 69 | this.metadata = await res.json(); 70 | } 71 | return this.metadata[item]; 72 | } 73 | 74 | /// Get a message by its message ID (e.g. EventFlowMsg/AncientBall_Kakariko:Label). 75 | getMsg(id: string): string { 76 | const [file, label] = id.split(':'); 77 | return this.getMsgWithFile(file, label); 78 | } 79 | 80 | getName(actorName: string): string { 81 | return this.names[actorName]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode { } 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue { } 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /src/util/CanvasMarker.ts: -------------------------------------------------------------------------------- 1 | import * as L from 'leaflet'; 2 | 3 | export interface CanvasMarkerOptions extends L.CircleMarkerOptions { 4 | icon?: HTMLImageElement; 5 | iconWidth?: number; 6 | iconHeight?: number; 7 | showLabel?: boolean; 8 | } 9 | 10 | export class CanvasMarker extends L.CircleMarker { 11 | options!: CanvasMarkerOptions; 12 | 13 | _updatePath() { 14 | if (!this.options.icon) { 15 | // @ts-ignore 16 | super._updatePath(); 17 | return; 18 | } 19 | 20 | // @ts-ignore 21 | this._renderer._botwDrawCanvasImageMarker(this); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/util/colorscale.ts: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 2-Clause License 3 | 4 | Copyright (c) 2022, Brian Savage 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | 29 | import * as L from 'leaflet'; 30 | 31 | export type ColorScaleOptions = { 32 | min: number; 33 | max: number; 34 | palettes?: { [key: number]: any }; 35 | name?: string; 36 | }; 37 | 38 | export class ColorScale extends L.Control { 39 | min: number 40 | max: number 41 | max_ticks: number; 42 | nice_min: number; 43 | nice_max: number; 44 | tick_spacing: number; 45 | palettes: any; 46 | name: string; 47 | 48 | constructor(opt: ColorScaleOptions, options?: L.ControlOptions) { 49 | super(options) 50 | this.name = opt.name || "viridis"; 51 | this.palettes = Object.assign({}, this.base_palettes, opt.palettes); 52 | this.min = opt.min; 53 | this.max = opt.max; 54 | this.max_ticks = 5; 55 | this.nice_min = this.min; 56 | this.nice_max = this.max; 57 | this.tick_spacing = (this.max - this.min) / this.max_ticks; 58 | if (!(this.name in this.palettes)) { 59 | this.name = Object.keys(this.palettes)[0]; 60 | } 61 | this._calculate(); 62 | } 63 | 64 | base_palettes: any = { 65 | gist_rainbow: { 66 | 0.0: '#ff0028', 67 | 0.06666666666666667: '#ff3200', 68 | 0.13333333333333333: '#ff8e00', 69 | 0.2: '#ffea00', 70 | 0.26666666666666666: '#b7ff00', 71 | 0.3333333333333333: '#5bff00', 72 | 0.4: '#00ff00', 73 | 0.4666666666666667: '#00ff5b', 74 | 0.5333333333333333: '#00ffb6', 75 | 0.6: '#00ebff', 76 | 0.6666666666666666: '#008fff', 77 | 0.7333333333333333: '#0032ff', 78 | 0.8: '#2900ff', 79 | 0.8666666666666667: '#8500ff', 80 | 0.9333333333333333: '#e200ff', 81 | 1.0: '#ff00bf', 82 | }, 83 | viridis: { 84 | 0.0: '#440154', 85 | 0.14285714285714285: '#46317e', 86 | 0.2857142857142857: '#365b8c', 87 | 0.42857142857142855: '#277e8e', 88 | 0.5714285714285714: '#1fa187', 89 | 0.7142857142857143: '#49c16d', 90 | 0.8571428571428571: '#9fd938', 91 | 1.0: '#fde724', 92 | }, 93 | terrain: { 94 | 0.000: '#ddf892', 95 | 0.125: '#eee98f', 96 | 0.250: '#ccbd7d', 97 | 0.375: '#aa926b', 98 | 0.500: '#886658', 99 | 0.625: '#997c76', 100 | 0.750: '#bba7a3', 101 | 0.875: '#ddd3d1', 102 | 1.000: '#ffffff', 103 | }, 104 | grays: { 0.0: 'black', 1.0: 'white' }, 105 | }; 106 | 107 | createPicker() { 108 | let div = L.DomUtil.create('div', 'colorscale-picker'); 109 | let title = L.DomUtil.create('div', 'colorscale-picker-title'); 110 | title.textContent = "Change Color Scheme"; 111 | div.appendChild(title); 112 | for (const name of Object.keys(this.palettes)) { 113 | let cmap = this.palettes[name]; 114 | let link = L.DomUtil.create('a'); 115 | link.href = "#"; 116 | link.title = `Use ${name} colormap`; 117 | link.addEventListener('click', (event: any) => { 118 | this.updateColorScaleByName(name); 119 | event.stopPropagation(); 120 | // @ts-ignore 121 | this._map.fire('ColorScale:change', { 122 | name: name, 123 | palette: this.palettes[name], 124 | min: this.min, 125 | max: this.max, 126 | }); 127 | }); 128 | let line = this.createColorBar(cmap); 129 | line.classList.add('colorscale-picker-sample'); 130 | line.style.margin = '5px'; 131 | link.appendChild(line); 132 | div.appendChild(link); 133 | } 134 | 135 | return div; 136 | } 137 | 138 | createColorBar(palette: any) { 139 | let div = L.DomUtil.create('div', 'colorscale-bar'); 140 | this.setColorScale(div, palette); 141 | return div; 142 | } 143 | 144 | setColorScale(scale: HTMLElement, palette: any) { 145 | let stops = Object.keys(palette).sort() 146 | .map((v: any) => `${palette[v]} ${v * 100}%`).join(", "); 147 | scale.style.background = `linear-gradient(to right, ${stops})`; 148 | } 149 | updateColorScale(palette: any) { 150 | let div = L.DomUtil.get('colorscale-bar-id'); 151 | if (div) { 152 | this.setColorScale(div, palette); 153 | } 154 | } 155 | updateColorScaleByName(name: string) { 156 | if (name in this.palettes) { 157 | this.updateColorScale(this.palettes[name]); 158 | } 159 | } 160 | 161 | 162 | createScale(parent: any) { 163 | let title = L.DomUtil.create('div', 'colorscale-title'); 164 | title.innerHTML = 'Height (m)'; 165 | parent.appendChild(title); 166 | 167 | let div = L.DomUtil.create('div', 'colorscale-bar'); 168 | this.setColorScale(div, this.palettes[this.name]); 169 | div.id = 'colorscale-bar-id' 170 | parent.appendChild(div); 171 | 172 | div = L.DomUtil.create('div', 'colorscale-labelbox'); 173 | 174 | let i = 0; 175 | let tick = this.nice_min + i * this.tick_spacing; 176 | let scale = Math.floor(Math.log10(this.tick_spacing)); 177 | scale = (scale < 0) ? Math.abs(scale) : 0; 178 | 179 | while (tick <= this.nice_max) { 180 | let tag = L.DomUtil.create('div', 'colorscale-label'); 181 | tag.innerHTML = `${tick.toFixed(scale)}`; 182 | let pct = this._valToNorm(tick); 183 | tag.style.left = `${pct * 100}% `; 184 | div.appendChild(tag); 185 | i += 1; 186 | tick = this.nice_min + i * this.tick_spacing; 187 | } 188 | parent.appendChild(div); 189 | parent.appendChild(this.createPicker()); 190 | return parent; 191 | } 192 | 193 | updateScale() { 194 | let parent: any = L.DomUtil.get('colorscale-id'); 195 | L.DomUtil.empty(parent); 196 | this.createScale(parent); 197 | } 198 | 199 | onAdd(map: L.Map) { 200 | let parent = L.DomUtil.create('div', 'colorscale'); 201 | parent.id = "colorscale-id"; 202 | return this.createScale(parent); 203 | } 204 | 205 | minmax(min: number, max: number) { 206 | this.min = min; 207 | this.max = max; 208 | this._calculate(); 209 | this.updateScale(); 210 | } 211 | 212 | palette(): any { 213 | return this.palettes[this.name]; 214 | } 215 | 216 | _calculate() { 217 | let range = this._niceNum(this.max - this.min, false); 218 | this.tick_spacing = this._niceNum(range / (this.max_ticks - 1), true); 219 | this.nice_min = Math.ceil(this.min / this.tick_spacing) * this.tick_spacing; 220 | this.nice_max = Math.floor(this.max / this.tick_spacing) * this.tick_spacing; 221 | } 222 | 223 | 224 | _niceNum(range: number, round: boolean): number { 225 | let exponent = Math.floor(Math.log10(range)); 226 | let fraction = range / Math.pow(10, exponent); 227 | 228 | let nice_fraction = 1.0; 229 | if (round) { 230 | if (fraction < 1.5) { 231 | nice_fraction = 1; 232 | } else if (fraction < 3) { 233 | nice_fraction = 2; 234 | } else if (fraction < 7) { 235 | nice_fraction = 5; 236 | } else { 237 | nice_fraction = 10; 238 | } 239 | } else { 240 | if (fraction <= 1) { 241 | nice_fraction = 1; 242 | } else if (fraction <= 2) { 243 | nice_fraction = 2; 244 | } else if (fraction <= 5) { 245 | nice_fraction = 5; 246 | } else { 247 | nice_fraction = 10; 248 | } 249 | } 250 | return nice_fraction * Math.pow(10, exponent); 251 | } 252 | 253 | _valToNorm(x: number): number { 254 | return (x - this.min) / (this.max - this.min); 255 | } 256 | _normToValue(x: number): number { 257 | return this.min + (this.max - this.min) * x; 258 | } 259 | 260 | } 261 | -------------------------------------------------------------------------------- /src/util/curves.ts: -------------------------------------------------------------------------------- 1 | 2 | function range(n: number): number[] { 3 | return [...Array(n)].map((item: any, index: number) => index); 4 | } 5 | 6 | // Calculate Bezier coefficients: A row of Pascal's Triangle 7 | function bezierCoeff(n: number): number[] { 8 | let row = Array(n + 1).fill(0); 9 | row[0] = 1; 10 | for (let i = 0; i <= n; i++) { 11 | for (let j = i; j > 0; j--) { 12 | row[j] += row[j - 1]; 13 | } 14 | } 15 | return row; 16 | } 17 | 18 | // dot-product 19 | function dot(a: number[], b: number[]): number { 20 | let x = 0; 21 | for (let i = 0; i < a.length; i++) { 22 | x += a[i] * b[i]; 23 | } 24 | return x; 25 | } 26 | 27 | // Point addition 28 | function ptAdd(a: number[], b: number[]): number[] { 29 | return [a[0] + b[0], a[1] + b[1], a[2] + b[2]] 30 | } 31 | 32 | // Bezier Curve 33 | // 34 | // B(t) = \Sum_{i=0}^{n} coeff(i,n) (1 - t)^{n-i} t^i P_i 35 | // 36 | // n = Number of points - 1 ; (n is inclusive) 37 | // All segments are divided into 36 segments, t = 0 to 1 ; dt = 1/36 38 | // Curve construction is performed on all components [x,y,z] 39 | // 40 | function bezier(pts: any) { 41 | let steps = 36; 42 | let out = []; 43 | let n = pts.length; 44 | if (n == 2) { 45 | return pts; 46 | } 47 | const coeff = bezierCoeff(n - 1); 48 | 49 | let t0 = 0; 50 | let dt = 1.0 / (steps - 1); 51 | let xi = pts.map((p: any) => p[0]); 52 | let yi = pts.map((p: any) => p[1]); 53 | let zi = pts.map((p: any) => p[2]); 54 | for (let k = 0; k < steps; k++) { 55 | const t = t0 + k * dt; 56 | const ti = range(n).map((i: number) => coeff[i] * Math.pow(1.0 - t, n - 1 - i) * Math.pow(t, i)); 57 | for (let i = 0; i < n; i++) { 58 | let pt = [dot(ti, xi), dot(ti, yi), dot(ti, zi)]; 59 | out.push(pt); 60 | } 61 | } 62 | return out; 63 | } 64 | 65 | // Linear Rail path without any Control Points 66 | function railPathLinear(rail: any): any { 67 | let pts = rail.RailPoints.map((pt: any) => pt.Translate); 68 | if (rail['IsClosed']) { 69 | pts.push(pts[0]) 70 | } 71 | return pts; 72 | } 73 | 74 | // Bezier Path with Control Points 75 | // Each true is assumed to have two control points associated with it 76 | // The 2nd [1] control point builds the curve towards the next distinct point 77 | // The 1st [0] control point builds the curve towards the previous distinct point 78 | // 79 | // b1 ____ c0 80 | // / \ 81 | // a b c d 82 | // \___/ \___/ 83 | // a1 b0 c1 d0 84 | // 85 | // Points: a, b, c, d 86 | // Control points: a1, b0, b1, c0, c1, d0 87 | // 88 | // Loop over sections of the bezier curve 89 | // - Build: [b, b+b1, c+c0, c] that is passed to bezier() 90 | // - Append bezier() output to the curve 91 | // If the curve is closed, the the first point as a last point 92 | // If the curve is open, do not use the last point as a curve starting point 93 | // 94 | function railPathBezier(rail: any): any { 95 | let out = []; 96 | let n = rail.RailPoints.length; 97 | if (!rail['IsClosed']) { 98 | n -= 1; 99 | } 100 | 101 | for (let i = 0; i < n; i++) { 102 | let j = (i + 1); 103 | if (rail['IsClosed']) { 104 | j = j % n; 105 | } 106 | let p0 = rail.RailPoints[i].Translate; 107 | let p1 = rail.RailPoints[j].Translate; 108 | let bez = [p0]; 109 | if (rail.RailPoints[i].ControlPoints) { 110 | bez.push(ptAdd(p0, rail.RailPoints[i].ControlPoints[1])); 111 | } 112 | if (rail.RailPoints[j].ControlPoints) { 113 | bez.push(ptAdd(p1, rail.RailPoints[j].ControlPoints[0])); 114 | } 115 | bez.push(p1); 116 | out.push(...bezier(bez)) 117 | } 118 | return out; 119 | } 120 | 121 | export function railPath(rail: any): any { 122 | if (rail.RailType == "Linear") { 123 | return railPathLinear(rail); 124 | } else if (rail.RailType == "Bezier") { 125 | return railPathBezier(rail); 126 | } 127 | return null; 128 | } 129 | -------------------------------------------------------------------------------- /src/util/leaflet_cluster.ts: -------------------------------------------------------------------------------- 1 | import L from 'leaflet'; 2 | import 'leaflet.markercluster'; 3 | 4 | /// Returns a MarkerClusterGroup whose main purpose is to cull invisible objects. 5 | export function makeClusterGroup(pad: number = 1, disableCluster = true): L.MarkerClusterGroup { 6 | const cg = L.markerClusterGroup({ 7 | disableClusteringAtZoom: disableCluster ? -10 : 6, 8 | showCoverageOnHover: false, 9 | zoomToBoundsOnClick: false, 10 | removeOutsideVisibleBounds: true, 11 | spiderfyOnMaxZoom: false, 12 | chunkedLoading: true, 13 | // @ts-ignore 14 | chunkInterval: 0, 15 | }); 16 | // Override _getExpandedVisibleBounds to reduce the number of rendered markers for better perf. 17 | // @ts-ignore 18 | cg._getExpandedVisibleBounds = function() { 19 | // @ts-ignore 20 | return this._checkBoundsMaxLat(this._map.getBounds().pad(pad)); 21 | }; 22 | return cg; 23 | } 24 | -------------------------------------------------------------------------------- /src/util/leaflet_tile_workaround.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import L from 'leaflet'; 3 | // https://github.com/Leaflet/Leaflet.TileLayer.NoGap 4 | // "THE BEER-WARE LICENSE": 5 | // wrote this file. As long as you retain this notice you 6 | // can do whatever you want with this stuff. If we meet some day, and you think 7 | // this stuff is worth it, you can buy me a beer in return. 8 | 9 | // @class TileLayer 10 | 11 | L.TileLayer.mergeOptions({ 12 | // @option keepBuffer 13 | // The amount of tiles outside the visible map area to be kept in the stitched 14 | // `TileLayer`. 15 | 16 | // @option dumpToCanvas: Boolean = true 17 | // Whether to dump loaded tiles to a `` to prevent some rendering 18 | // artifacts. (Disabled by default in IE) 19 | dumpToCanvas: L.Browser.canvas && !L.Browser.ie, 20 | }); 21 | 22 | L.TileLayer.include({ 23 | _onUpdateLevel: function(z, zoom) { 24 | if (this.options.dumpToCanvas) { 25 | this._levels[z].canvas.style.zIndex = 26 | this.options.maxZoom - Math.abs(zoom - z); 27 | } 28 | }, 29 | 30 | _onRemoveLevel: function(z) { 31 | if (this.options.dumpToCanvas) { 32 | L.DomUtil.remove(this._levels[z].canvas); 33 | } 34 | }, 35 | 36 | _onCreateLevel: function(level) { 37 | if (this.options.dumpToCanvas) { 38 | level.canvas = L.DomUtil.create( 39 | "canvas", 40 | "leaflet-tile-container leaflet-zoom-animated", 41 | this._container 42 | ); 43 | level.ctx = level.canvas.getContext("2d"); 44 | this._resetCanvasSize(level); 45 | } 46 | }, 47 | 48 | _removeTile: function(key) { 49 | if (this.options.dumpToCanvas) { 50 | var tile = this._tiles[key]; 51 | var level = this._levels[tile.coords.z]; 52 | var tileSize = this.getTileSize(); 53 | 54 | if (level) { 55 | // Where in the canvas should this tile go? 56 | var offset = L.point(tile.coords.x, tile.coords.y) 57 | .subtract(level.canvasRange.min) 58 | .scaleBy(this.getTileSize()); 59 | 60 | level.ctx.clearRect(offset.x, offset.y, tileSize.x, tileSize.y); 61 | } 62 | } 63 | 64 | L.GridLayer.prototype._removeTile.call(this, key); 65 | }, 66 | 67 | _resetCanvasSize: function(level) { 68 | var buff = this.options.keepBuffer, 69 | pixelBounds = this._getTiledPixelBounds(this._map.getCenter()), 70 | tileRange = this._pxBoundsToTileRange(pixelBounds), 71 | tileSize = this.getTileSize(); 72 | 73 | tileRange.min = tileRange.min.subtract([buff, buff]); // This adds the no-prune buffer 74 | tileRange.max = tileRange.max.add([buff + 1, buff + 1]); 75 | 76 | var pixelRange = L.bounds( 77 | tileRange.min.scaleBy(tileSize), 78 | tileRange.max.add([1, 1]).scaleBy(tileSize) // This prevents an off-by-one when checking if tiles are inside 79 | ), 80 | mustRepositionCanvas = false, 81 | neededSize = pixelRange.max.subtract(pixelRange.min); 82 | 83 | // Resize the canvas, if needed, and only to make it bigger. 84 | if ( 85 | neededSize.x > level.canvas.width || 86 | neededSize.y > level.canvas.height 87 | ) { 88 | // Resizing canvases erases the currently drawn content, I'm afraid. 89 | // To keep it, dump the pixels to another canvas, then display it on 90 | // top. This could be done with getImageData/putImageData, but that 91 | // would break for tainted canvases (in non-CORS tilesets) 92 | var oldSize = { x: level.canvas.width, y: level.canvas.height }; 93 | // console.info('Resizing canvas from ', oldSize, 'to ', neededSize); 94 | 95 | var tmpCanvas = L.DomUtil.create("canvas"); 96 | tmpCanvas.style.width = (tmpCanvas.width = oldSize.x) + "px"; 97 | tmpCanvas.style.height = (tmpCanvas.height = oldSize.y) + "px"; 98 | tmpCanvas.getContext("2d").drawImage(level.canvas, 0, 0); 99 | // var data = level.ctx.getImageData(0, 0, oldSize.x, oldSize.y); 100 | 101 | level.canvas.style.width = (level.canvas.width = neededSize.x) + "px"; 102 | level.canvas.style.height = (level.canvas.height = neededSize.y) + "px"; 103 | level.ctx.drawImage(tmpCanvas, 0, 0); 104 | // level.ctx.putImageData(data, 0, 0, 0, 0, oldSize.x, oldSize.y); 105 | } 106 | 107 | // Translate the canvas contents if it's moved around 108 | if (level.canvasRange) { 109 | var offset = level.canvasRange.min 110 | .subtract(tileRange.min) 111 | .scaleBy(this.getTileSize()); 112 | 113 | // console.info('Offsetting by ', offset); 114 | 115 | if (!L.Browser.safari) { 116 | // By default, canvases copy things "on top of" existing pixels, but we want 117 | // this to *replace* the existing pixels when doing a drawImage() call. 118 | // This will also clear the sides, so no clearRect() calls are needed to make room 119 | // for the new tiles. 120 | level.ctx.globalCompositeOperation = "copy"; 121 | level.ctx.drawImage(level.canvas, offset.x, offset.y); 122 | level.ctx.globalCompositeOperation = "source-over"; 123 | } else { 124 | // Safari clears the canvas when copying from itself :-( 125 | if (!this._tmpCanvas) { 126 | var t = (this._tmpCanvas = L.DomUtil.create("canvas")); 127 | t.width = level.canvas.width; 128 | t.height = level.canvas.height; 129 | this._tmpContext = t.getContext("2d"); 130 | } 131 | this._tmpContext.clearRect( 132 | 0, 133 | 0, 134 | level.canvas.width, 135 | level.canvas.height 136 | ); 137 | this._tmpContext.drawImage(level.canvas, 0, 0); 138 | level.ctx.clearRect(0, 0, level.canvas.width, level.canvas.height); 139 | level.ctx.drawImage(this._tmpCanvas, offset.x, offset.y); 140 | } 141 | 142 | mustRepositionCanvas = true; // Wait until new props are set 143 | } 144 | 145 | level.canvasRange = tileRange; 146 | level.canvasPxRange = pixelRange; 147 | level.canvasOrigin = pixelRange.min; 148 | 149 | // console.log('Canvas tile range: ', level, tileRange.min, tileRange.max ); 150 | // console.log('Canvas pixel range: ', pixelRange.min, pixelRange.max ); 151 | // console.log('Level origin: ', level.origin ); 152 | 153 | if (mustRepositionCanvas) { 154 | this._setCanvasZoomTransform( 155 | level, 156 | this._map.getCenter(), 157 | this._map.getZoom() 158 | ); 159 | } 160 | }, 161 | 162 | /// set transform/position of canvas, in addition to the transform/position of the individual tile container 163 | _setZoomTransform: function(level, center, zoom) { 164 | L.GridLayer.prototype._setZoomTransform.call(this, level, center, zoom); 165 | if (this.options.dumpToCanvas) { 166 | this._setCanvasZoomTransform(level, center, zoom); 167 | } 168 | }, 169 | 170 | // This will get called twice: 171 | // * From _setZoomTransform 172 | // * When the canvas has shifted due to a new tile being loaded 173 | _setCanvasZoomTransform: function(level, center, zoom) { 174 | // console.log('_setCanvasZoomTransform', level, center, zoom); 175 | if (!level.canvasOrigin) { 176 | return; 177 | } 178 | var scale = this._map.getZoomScale(zoom, level.zoom), 179 | translate = level.canvasOrigin 180 | .multiplyBy(scale) 181 | .subtract(this._map._getNewPixelOrigin(center, zoom)) 182 | .round(); 183 | 184 | if (L.Browser.any3d) { 185 | L.DomUtil.setTransform(level.canvas, translate, scale); 186 | } else { 187 | L.DomUtil.setPosition(level.canvas, translate); 188 | } 189 | }, 190 | 191 | _onOpaqueTile: function(tile) { 192 | if (!this.options.dumpToCanvas) { 193 | return; 194 | } 195 | 196 | this.dumpPixels(tile.coords, tile.el); 197 | 198 | // Do not remove the tile itself, as it is needed to check if the whole 199 | // level (and its canvas) should be removed (via level.el.children.length) 200 | tile.el.style.display = "none"; 201 | }, 202 | 203 | // @section Extension methods 204 | // @uninheritable 205 | 206 | // @method dumpPixels(coords: Object, imageSource: CanvasImageSource): this 207 | // Dumps pixels from the given `CanvasImageSource` into the layer, into 208 | // the space for the tile represented by the `coords` tile coordinates (an object 209 | // like `{x: Number, y: Number, z: Number}`; the image source must have the 210 | // same size as the `tileSize` option for the layer. Has no effect if `dumpToCanvas` 211 | // is `false`. 212 | dumpPixels: function(coords, imageSource) { 213 | var level = this._levels[coords.z], 214 | tileSize = this.getTileSize(); 215 | 216 | if (!level.canvasRange || !this.options.dumpToCanvas) { 217 | return; 218 | } 219 | 220 | // Check if the tile is inside the currently visible map bounds 221 | // There is a possible race condition when tiles are loaded after they 222 | // have been panned outside of the map. 223 | if (!level.canvasRange.contains(coords)) { 224 | this._resetCanvasSize(level); 225 | } 226 | 227 | // Where in the canvas should this tile go? 228 | var offset = L.point(coords.x, coords.y) 229 | .subtract(level.canvasRange.min) 230 | .scaleBy(this.getTileSize()); 231 | 232 | try { 233 | level.ctx.drawImage(imageSource, offset.x, offset.y, tileSize.x, tileSize.y); 234 | } catch (_) { 235 | // Ignore. 236 | } 237 | 238 | // TODO: Clear the pixels of other levels' canvases where they overlap 239 | // this newly dumped tile. 240 | return this; 241 | }, 242 | }); 243 | -------------------------------------------------------------------------------- /src/util/map.ts: -------------------------------------------------------------------------------- 1 | export const TILE_SIZE = 256; 2 | export const MAP_SIZE = [24000, 20000]; 3 | 4 | export const GAME_FILES = process.env.VUE_APP_GAME_FILES; 5 | 6 | export type Point = [number, number, number]; 7 | 8 | export function isValidXYZ(x: number, y: number, z: number) { 9 | return Math.abs(x) <= 6000 && Math.abs(z) <= 5000; 10 | } 11 | 12 | export function isValidPoint(p: Point) { 13 | return isValidXYZ(p[0], p[1], p[2]); 14 | } 15 | 16 | export function pointToMapUnit(p: Point) { 17 | const col = ((p[0] + 5000) / 1000) >>> 0; 18 | const row = ((p[2] + 4000) / 1000) >>> 0; 19 | return String.fromCharCode('A'.charCodeAt(0) + col) 20 | + '-' 21 | + String.fromCharCode('1'.charCodeAt(0) + row); 22 | } 23 | 24 | export const enum MarkerType { 25 | Water = 0, 26 | Artifact = 1, 27 | Magma = 2, 28 | Location = 3, 29 | Timber = 4, 30 | Region = 5, 31 | Region2 = 6, 32 | Mountain = 7, 33 | } 34 | const MARKER_TYPE_STRS = [ 35 | 'Water', 'Artifact', 'Magma', 'Location', 'Timber', 'Region', 'Region', 'Mountain' 36 | ]; 37 | export function markerTypetoStr(type: MarkerType) { 38 | return MARKER_TYPE_STRS[type]; 39 | } 40 | 41 | export const enum ShowLevel { 42 | Region = 1, 43 | Area = 2, 44 | Location = 4, 45 | SecondaryLocation = 8, 46 | } 47 | 48 | export const DEFAULT_ZOOM = 3; 49 | export const MIN_ZOOM = 2; 50 | export const MAX_ZOOM = 10; 51 | 52 | export function shouldShowLocationMarker(showLevel: ShowLevel, zoom: number) { 53 | switch (showLevel) { 54 | case ShowLevel.Region: 55 | return zoom === 2; 56 | case ShowLevel.Area: 57 | return zoom === 3 || zoom === 4; 58 | case ShowLevel.Location: 59 | return zoom >= 5; 60 | case ShowLevel.SecondaryLocation: 61 | return zoom >= 7; 62 | } 63 | } 64 | 65 | export class LocationMarkerBase { 66 | protected l: any; 67 | 68 | constructor(data: any) { 69 | this.l = data; 70 | } 71 | 72 | getMessageId(): string { return this.l.MessageID; } 73 | getXYZ(): Point { return [this.l.Translate.X, this.l.Translate.Y, this.l.Translate.Z]; } 74 | } 75 | 76 | export class LocationMarker extends LocationMarkerBase { 77 | getId(): string { 78 | return `${this.l.Icon}:${this.l.MessageId || ''}:${this.l.Translate.X}:${this.l.Translate.Y}:${this.l.Translate.Z}`; 79 | } 80 | getSaveFlag(): string { return this.l.SaveFlag; } 81 | getIcon(): string { return this.l.Icon; } 82 | } 83 | 84 | export class LocationPointer extends LocationMarkerBase { 85 | getId(): string { 86 | return `${this.l.MessageID}:${this.l.ShowLevel}:${this.l.Translate.X}:${this.l.Translate.Y}:${this.l.Translate.Z}`; 87 | } 88 | 89 | getShowLevel(): ShowLevel { return this.l.ShowLevel; } 90 | getType(): MarkerType { 91 | const type = this.l.PointerType; 92 | return type === undefined ? this.l.Type : type; 93 | } 94 | 95 | shouldShowAtZoom(zoom: number): boolean { 96 | return shouldShowLocationMarker(this.getShowLevel(), zoom); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/util/math.ts: -------------------------------------------------------------------------------- 1 | export function clamp(number: number, min: number, max: number) { 2 | return Math.max(min, Math.min(number, max)); 3 | } 4 | -------------------------------------------------------------------------------- /src/util/polyline.ts: -------------------------------------------------------------------------------- 1 | import * as L from 'leaflet'; 2 | import * as ui from '@/util/ui'; 3 | 4 | function pointDist(a: L.LatLng, b: L.LatLng): number { 5 | let dx = a.lng - b.lng; 6 | let dy = a.lat - b.lat; 7 | return Math.sqrt(dx * dx + dy * dy); 8 | } 9 | 10 | function pointArrayLength(p: L.LatLng[] | L.LatLng[][] | L.LatLng[][][]): number { 11 | let dist = 0.0; 12 | if (p.length == 0) { 13 | return dist; 14 | } 15 | if (p[0] instanceof L.LatLng) { 16 | for (let i = 1; i < p.length; i++) { 17 | dist += pointDist(p[i - 1] as L.LatLng, p[i] as L.LatLng); 18 | } 19 | return dist; 20 | } 21 | for (let i = 0; i < p.length; i++) { 22 | dist += pointArrayLength(p[i] as L.LatLng[] | L.LatLng[][]); 23 | } 24 | return dist; 25 | } 26 | 27 | function polyLineLength(layer: L.Polyline) { 28 | return pointArrayLength(layer.getLatLngs()); 29 | } 30 | 31 | export function calcLayerLength(layer: L.Marker | L.Polyline) { 32 | if (!layer.feature) { 33 | return; 34 | } 35 | layer.feature.properties.pathLength = 0; 36 | if (ui.leafletType(layer) == ui.LeafletType.Polyline) { 37 | layer.feature.properties.pathLength = polyLineLength(layer as L.Polyline); 38 | } else { 39 | layer.feature.properties.length = 0; 40 | } 41 | // Tell Popup the length parameter 42 | let z: any = layer; 43 | if (z.popup) { 44 | z.popup.pathLength = layer.feature.properties.pathLength; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/util/settings.ts: -------------------------------------------------------------------------------- 1 | export type SettingChangeCb = () => void; 2 | export type SettingBeforeSaveCb = () => void; 3 | 4 | export class Settings { 5 | private static instance: Settings; 6 | static getInstance() { 7 | if (!this.instance) { 8 | const instance = new this(); 9 | this.instance = new Proxy(instance, instance.makeProxyHandler()); 10 | } 11 | return this.instance; 12 | } 13 | 14 | /// Register a callback that will be invoked whenever a setting is modified. 15 | registerCallback(cb: SettingChangeCb) { 16 | this.callbacks.push(cb); 17 | } 18 | registerBeforeSaveCallback(cb: SettingBeforeSaveCb) { 19 | this.beforeSaveCallbacks.push(cb); 20 | } 21 | 22 | shownGroups!: Set; 23 | drawLayerGeojson!: string; 24 | 25 | colorPerActor!: boolean; 26 | 27 | useActorNames!: boolean; 28 | useHexForHashIds!: boolean; 29 | 30 | mapType!: string; 31 | mapName!: string; 32 | 33 | hardMode!: boolean; 34 | ohoMode!: boolean; 35 | lastBossMode!: boolean; 36 | 37 | customSearchPresets!: Array<[string, string]>; 38 | 39 | left!: boolean; 40 | hylianMode!: boolean; 41 | drawControlsShown!: boolean; 42 | 43 | decompBannerHidden!: boolean; 44 | 45 | private constructor() { 46 | this.load(); 47 | window.addEventListener('beforeunload', (event) => { 48 | this.save(); 49 | }); 50 | } 51 | 52 | private load() { 53 | const dataStr = localStorage.getItem(Settings.KEY); 54 | const data = dataStr ? JSON.parse(dataStr) : {}; 55 | 56 | this.shownGroups = parse(data.shownGroups, (d) => new Set(d), 57 | new Set(['Location', 'Dungeon', 'DungeonDLC', 'Place', 'Tower', 'Shop', 'Labo'])); 58 | this.drawLayerGeojson = parse(data.drawLayerGeojson, Id, ''); 59 | this.colorPerActor = parse(data.colorPerActor, Id, true); 60 | this.useActorNames = parse(data.useActorNames, Id, false); 61 | this.useHexForHashIds = parse(data.useHexForHashIds, Id, true); 62 | this.hardMode = parse(data.hardMode, Id, false); 63 | this.ohoMode = parse(data.ohoMode, Id, false); 64 | this.lastBossMode = parse(data.lastBossMode, Id, false); 65 | this.customSearchPresets = parse(data.customSearchPresets, Id, []); 66 | this.left = parse(data.left, Id, true); 67 | this.mapType = parse(data.mapType, Id, 'MainField'); 68 | this.mapName = parse(data.mapName, Id, ''); 69 | this.hylianMode = false; 70 | this.drawControlsShown = parse(data.drawControlsShown, Id, false); 71 | this.decompBannerHidden = parse(data.decompBannerHidden, Id, false); 72 | 73 | this.invokeCallbacks(); 74 | } 75 | 76 | private save() { 77 | for (const cb of this.beforeSaveCallbacks) 78 | cb(); 79 | const data = { 80 | shownGroups: Array.from(this.shownGroups), 81 | drawLayerGeojson: this.drawLayerGeojson, 82 | colorPerActor: this.colorPerActor, 83 | useActorNames: this.useActorNames, 84 | useHexForHashIds: this.useHexForHashIds, 85 | hardMode: this.hardMode, 86 | ohoMode: this.ohoMode, 87 | lastBossMode: this.lastBossMode, 88 | customSearchPresets: this.customSearchPresets, 89 | left: this.left, 90 | mapType: this.mapType, 91 | mapName: this.mapName, 92 | hylianMode: this.hylianMode, 93 | drawControlsShown: this.drawControlsShown, 94 | decompBannerHidden: this.decompBannerHidden, 95 | }; 96 | // Merge with existing data to avoid data loss. 97 | const existingDataStr = localStorage.getItem(Settings.KEY); 98 | const existingData = existingDataStr ? JSON.parse(existingDataStr) : {}; 99 | localStorage.setItem(Settings.KEY, JSON.stringify(Object.assign(existingData, data))); 100 | } 101 | 102 | private invokeCallbacks() { 103 | for (const cb of this.callbacks) 104 | cb(); 105 | } 106 | 107 | private makeProxyHandler(): ProxyHandler { 108 | return { 109 | set: (target, prop, value) => { 110 | const r = Reflect.set(target, prop, value); 111 | if (!r) 112 | return false; 113 | this.invokeCallbacks(); 114 | return true; 115 | }, 116 | }; 117 | } 118 | 119 | private static KEY = 'storage'; 120 | private beforeSaveCallbacks: SettingBeforeSaveCb[] = []; 121 | private callbacks: SettingChangeCb[] = []; 122 | } 123 | 124 | function parse(data: any, parseFn: (data: any) => T, defaultVal: T) { 125 | if (data !== undefined) 126 | return parseFn(data); 127 | return defaultVal; 128 | } 129 | 130 | /// Identity function. 131 | function Id(data: any) { return data; } 132 | -------------------------------------------------------------------------------- /src/util/svg.ts: -------------------------------------------------------------------------------- 1 | 2 | export const raceGoal = ''; 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/util/ui.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as L from 'leaflet'; 3 | import { MsgMgr } from '@/services/MsgMgr'; 4 | 5 | /// Wrapper class for objects that should not be observed by Vue. 6 | export class Unobservable { 7 | public readonly data: T; 8 | constructor(data: T) { 9 | this.data = data; 10 | Object.freeze(this); 11 | } 12 | } 13 | 14 | export type LeafletContextMenuCbArg = { latlng: L.LatLng }; 15 | 16 | export function copyToClipboard(text: string) { 17 | let textarea = document.createElement("textarea"); 18 | textarea.textContent = text; 19 | textarea.style.position = "fixed"; 20 | document.body.appendChild(textarea); 21 | textarea.select(); 22 | try { 23 | document.execCommand("copy"); 24 | } catch (ex) { 25 | // *shrug* 26 | } finally { 27 | document.body.removeChild(textarea); 28 | } 29 | } 30 | 31 | export function genColor(numOfSteps: number, step: number) { 32 | // This function generates vibrant, "evenly spaced" colours (i.e. no clustering). This is ideal for creating easily distinguishable vibrant markers in Google Maps and other apps. 33 | // Adam Cole, 2011-Sept-14 34 | // HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript 35 | var r, g, b; 36 | var h = step / numOfSteps; 37 | var i = ~~(h * 6); 38 | var f = h * 6 - i; 39 | var q = 1 - f; 40 | switch (i % 6) { 41 | case 0: r = 1; g = f; b = 0; break; 42 | case 1: r = q; g = 1; b = 0; break; 43 | case 2: r = 0; g = 1; b = f; break; 44 | case 3: r = 0; g = q; b = 1; break; 45 | case 4: r = f; g = 0; b = 1; break; 46 | case 5: r = 1; g = 0; b = q; break; 47 | } 48 | // @ts-ignore 49 | return "#" + ("00" + (~ ~(r * 255)).toString(16)).slice(-2) + ("00" + (~ ~(g * 255)).toString(16)).slice(-2) + ("00" + (~ ~(b * 255)).toString(16)).slice(-2); 50 | } 51 | 52 | export function shadeColor(color: string, percent: number) { 53 | // from https://stackoverflow.com/questions/5560248 54 | const num = parseInt(color.slice(1), 16), 55 | amt = Math.round(2.55 * percent), 56 | R = (num >> 16) + amt, 57 | G = (num >> 8 & 0x00FF) + amt, 58 | B = (num & 0x0000FF) + amt; 59 | return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1); 60 | } 61 | 62 | function areaTooltip(area: any): string { 63 | let auto_areas = ['Fish', 'Bird', 'Insect', 'Animal', 'Enemy', 'Material']; 64 | if (area.type == "Safe") { 65 | return "Safe Area"; 66 | } else if (auto_areas.includes(area.type)) { 67 | let parts = area.items.map((item: any) => `- ${item.real_name}: ${item.num}`).join("
"); 68 | return [`Auto ${area.type}`, parts, `Field Map Area ${area.field_map_area}`].join("
"); 69 | } 70 | return 'Area'; 71 | } 72 | 73 | export function areaMapToLayers(areas: any): L.Path[] { 74 | const nentries = Object.entries(areas).length; 75 | let layers: L.Path[] = Object.values(areas).map((feature: any, i) => { 76 | let layer: L.Circle | L.Polygon = toShape(feature.shape, feature.loc, feature.scale, feature.rotate); 77 | let color = feature.color || genColor(nentries, i); 78 | layer.setStyle({ 79 | color: color, 80 | fillOpacity: 0.2, 81 | weight: 2, 82 | // @ts-ignore 83 | contextmenu: true, 84 | }); 85 | layerHover(layer, areaTooltip(feature)); 86 | return layer; 87 | }); 88 | return layers; 89 | } 90 | 91 | 92 | export function layerHover(layer: L.Path, data: string) { 93 | layer.bindTooltip(data); 94 | layer.on('mouseover', () => { 95 | layer.setStyle({ weight: 4, fillOpacity: 0.3 }); 96 | }); 97 | layer.on('mouseout', () => { 98 | layer.setStyle({ weight: 2, fillOpacity: 0.2 }); 99 | }); 100 | } 101 | 102 | // 103 | // Functions for creating Leaflet objects 104 | // 105 | function yrotate(p: [number, number], angle: number): [number, number] { 106 | let ang = angle * Math.PI / 180.0; 107 | let x = p[0]; 108 | let y = p[1]; 109 | let ca = Math.cos(ang); 110 | let sa = Math.sin(ang); 111 | return [x * ca - y * sa, x * sa + y * ca]; 112 | } 113 | 114 | export function cylinder(loc: number[], scale: number[], rotate: number[]): L.Circle | L.Polygon { 115 | return capsule(loc, scale, rotate); 116 | } 117 | 118 | export function circle(loc: number[], scale: number[], rotate: number[]): L.Circle { 119 | let x0 = loc[0]; 120 | let z0 = loc[2]; 121 | let sx = scale[0]; 122 | return L.circle(L.latLng(z0, x0), { radius: sx }); 123 | } 124 | 125 | export function capsule(loc: number[], scale: number[], rotate: number[]): L.Circle | L.Polygon { 126 | if (Math.abs(rotate[0] - Math.PI / 2) < 0.01) { // 127 | scale = [scale[1] + scale[2], scale[1], scale[2]]; 128 | return rectangle(loc, scale, [0, 0, 0]); 129 | } else { 130 | return circle(loc, scale, rotate); 131 | } 132 | } 133 | 134 | export function rectangle(loc: number[], scale: number[], rotate: number[]): L.Polygon { 135 | let x0 = loc[0]; 136 | //let y0 = loc[1]; 137 | let z0 = loc[2]; 138 | let sx = scale[0]; 139 | //let sy = scale[1]; 140 | let sz = scale[2]; 141 | // Rotate around the y axis (clockwise); 142 | let angle = -rotate[1]; 143 | let pts: [number, number][] = [ 144 | [-sx, -sz], 145 | [+sx, -sz], 146 | [+sx, +sz], 147 | [-sx, +sz], 148 | ]; 149 | pts = pts 150 | .map(p => yrotate(p, angle)) 151 | .map(p => [x0 + p[0], z0 + p[1]]); 152 | let latlng = pts.map(p => L.latLng(p[1], p[0])); 153 | return L.polygon(latlng); 154 | } 155 | 156 | export function toShape(shape: string, loc: number[], scale: number[], rotate: number[]): L.Circle | L.Polygon { 157 | if (shape == "Circle") { 158 | return circle(loc, scale, rotate); 159 | } else if (shape == "Sphere") { 160 | return circle(loc, scale, rotate); 161 | } else if (shape == "Cylinder") { 162 | return cylinder(loc, scale, rotate); 163 | } else if (shape == "Capsule") { 164 | return capsule(loc, scale, rotate); 165 | } else if (shape == "Box") { 166 | return rectangle(loc, scale, rotate); 167 | } else if (shape == "Hull") { 168 | //return null; 169 | } 170 | return circle(loc, scale, rotate); 171 | } 172 | 173 | 174 | export function svgIcon(fill: string): L.DivIcon { 175 | let stroke: string = shadeColor(fill, -10) as string; 176 | // Note: Attach styles directly to paths, as styles within are overwritten by new SVG Icons 177 | let svg = ` 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | `; 192 | const svgIcon = L.divIcon({ 193 | html: svg, 194 | className: "", 195 | iconSize: [40, 40], 196 | iconAnchor: [12, 40], 197 | }); 198 | return svgIcon; 199 | } 200 | 201 | export enum LeafletType { 202 | Rectangle, 203 | Polygon, 204 | Polyline, 205 | Circle, 206 | CircleMarker, 207 | Marker, 208 | Path, 209 | Layer, 210 | } 211 | 212 | // https://leafletjs.com/examples/extending/class-diagram.html 213 | // https://stackoverflow.com/a/56987060 214 | export function leafletType(layer: L.Layer): LeafletType { 215 | if (layer instanceof L.Rectangle) { 216 | return LeafletType.Rectangle; 217 | } else if (layer instanceof L.Polygon) { 218 | return LeafletType.Polygon; 219 | } else if (layer instanceof L.Polyline) { 220 | return LeafletType.Polyline; 221 | } else if (layer instanceof L.Circle) { 222 | return LeafletType.Circle; 223 | } else if (layer instanceof L.CircleMarker) { 224 | return LeafletType.CircleMarker; 225 | } else if (layer instanceof L.Marker) { 226 | return LeafletType.Marker; 227 | } else if (layer instanceof L.Path) { 228 | return LeafletType.Path; 229 | } 230 | return LeafletType.Layer; 231 | } 232 | 233 | export function getName(name: string) { 234 | return MsgMgr.getInstance().getName(name) || name; 235 | } 236 | 237 | -------------------------------------------------------------------------------- /tools/map_gen_markers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from collections import defaultdict 4 | from itertools import chain 5 | import json 6 | from pathlib import Path 7 | import typing 8 | 9 | Point = typing.Tuple[float, float] 10 | class LocationMarkerBase: 11 | def __init__(self, l): 12 | self.l = l 13 | def get_message_id(self) -> str: 14 | return self.l.get('MessageID', '') 15 | def get_xz(self) -> Point: 16 | return (self.l['Translate']['X'], self.l['Translate']['Z']) 17 | 18 | class LocationMarker(LocationMarkerBase): 19 | def get_save_flag(self) -> str: 20 | return self.l['SaveFlag'] 21 | def get_icon(self) -> str: 22 | return self.l['Icon'] 23 | 24 | class LocationPointer(LocationMarkerBase): 25 | def get_show_level(self) -> int: 26 | return self.l['ShowLevel'] 27 | def get_type(self) -> int: 28 | return self.l.get('PointerType', self.l['Type']) 29 | 30 | data: dict = {} 31 | 32 | root = Path(__file__).parent.parent 33 | game_files_dir = root / 'public' / 'game_files' 34 | map_dir = game_files_dir / 'map' 35 | 36 | mainfield_static = json.load(open(map_dir/'MainField'/'Static.json', 'r')) 37 | mainfield_location = json.load(open(map_dir/'MainField'/'Location.json', 'r')) 38 | korok_data = json.load(open('korok_ids.json', 'r')) 39 | 40 | mainfield_markers: defaultdict = defaultdict(list) 41 | for l in mainfield_static['LocationMarker']: 42 | if 'Icon' not in l: 43 | continue 44 | mainfield_markers[l['Icon']].append(l) 45 | 46 | def make_markers(entries, need_message_id=False): 47 | markers = [] 48 | for l in entries: 49 | lm = LocationMarkerBase(l) 50 | if need_message_id and not lm.get_message_id(): 51 | continue 52 | markers.append(l) 53 | return markers 54 | 55 | data['markers'] = dict() 56 | data['markers']['Location'] = make_markers(chain(mainfield_location, mainfield_static['LocationPointer']), need_message_id=True) 57 | data['markers']['Dungeon'] = make_markers(mainfield_markers['Dungeon']) 58 | data['markers']['Place'] = make_markers(chain(*(mainfield_markers[x] for x in ('Village', 'Hatago', 'Castle', 'CheckPoint')))) 59 | data['markers']['Tower'] = make_markers(mainfield_markers['Tower']) 60 | data['markers']['Labo'] = make_markers(mainfield_markers['Labo']) 61 | data['markers']['Shop'] = make_markers(chain(*(mainfield_markers[x] for x in ('ShopBougu', 'ShopColor', 'ShopJewel', 'ShopYadoya', 'ShopYorozu')))) 62 | data['markers']['Korok'] = korok_data 63 | 64 | with open(game_files_dir / 'map_summary' / 'MainField' / 'static.json', 'w') as f: 65 | json.dump(data, f, ensure_ascii=False) 66 | -------------------------------------------------------------------------------- /tools/msg_gen_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Generate a list of text message files. 3 | 4 | import json 5 | from pathlib import Path 6 | 7 | root = Path(__file__).parent.parent 8 | text_dir = root / 'public' / 'game_files' / 'text' 9 | paths = [] 10 | for text_path in text_dir.glob('*/*.json'): 11 | paths.append(str(text_path.relative_to(text_dir))) 12 | with open(text_dir / 'list.json', 'w') as f: 13 | json.dump(paths, f) 14 | 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ], 28 | "skipLibCheck": true 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | productionSourceMap: false 3 | } --------------------------------------------------------------------------------