├── .gitignore ├── .gitmodules ├── CONTRIBUTING.md ├── README.md ├── assets ├── data │ ├── lang │ │ └── sc │ │ │ └── gui.en_US.json.patch │ └── maps │ │ ├── arid-dng │ │ └── second │ │ │ └── f99 │ │ │ └── boss-2.json.patch │ │ ├── cold-dng │ │ └── b3 │ │ │ └── room7.json.patch │ │ └── meta-space.json.patch └── media │ ├── entity │ └── objects │ │ └── treasure.png │ ├── font │ └── icons-multiworld.png │ └── gui │ └── archipelago-start.png ├── balancing.md ├── build.mjs ├── bundle.sh ├── ccmod.json ├── data ├── in │ ├── chests.json │ ├── cutscenes.json │ ├── elements.json │ ├── item-pools │ │ ├── common.json │ │ ├── epic.json │ │ ├── legendary.json │ │ ├── master.json │ │ ├── pets.json │ │ ├── rare.json │ │ └── required.json │ ├── items.json │ ├── master.json │ ├── prog-items │ │ ├── areas.json │ │ ├── arms.json │ │ ├── heads.json │ │ ├── legs.json │ │ ├── master.json │ │ └── torsos.json │ ├── quests-always.json │ ├── quests-qr.json │ ├── regions.json │ ├── shops.json │ └── vars.json └── out │ ├── data.json │ ├── items.json │ └── locations.json ├── icon-24.png ├── package-lock.json ├── package.json ├── src ├── common.ts ├── item-data.model.ts ├── items.ts ├── patches │ ├── chest.ts │ ├── entity.ts │ ├── event.ts │ ├── gui-misc.ts │ ├── index.ts │ ├── marquee.ts │ ├── multiworld-hud.ts │ ├── multiworld-model.ts │ ├── new-game.ts │ ├── quest.ts │ └── shop.ts ├── plugin.ts ├── types │ ├── multiworld-hud.ts │ └── multiworld-model.ts └── utils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.js 3 | *.ccmod 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "extern/apjs"] 2 | path = extern/apjs 3 | url = git@github.com:CodeTriangle/archipelago.js 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Hello, developers! 2 | 3 | Thank you for your interest in contributing to this project. There are a few things you'll want to know about setting 4 | up the development environment. 5 | 6 | ## Archipelago.js 7 | 8 | Archipelago.js is the JavaScript library that provides the websocket connection to the Archipelago server. As of August 9 | 2024, CCMultiworldRandomizer depends on 10 | [a custom fork of Archipelago.js](https://github.com/CodeTriangle/archipelago.js/tree/crosscode) 11 | tailored for use in CrossCode. 12 | 13 | To ensure correct versioning, it is included in this repository at `extern/apjs` via git submodule. Upon cloning this 14 | repository, run the following to download submodules: 15 | 16 | ```bash 17 | git submodule update --init --recursive 18 | ``` 19 | 20 | If you ever make changes to `extern/apjs`, make sure run `npm install` in the root of the CCMultiworldRandomizer 21 | repository to ensure that your changes are added. 22 | 23 | The fork is necessary because: 24 | * The upstream depends on `isomorphic-ws`, a package which in theory provides the same websocket interface for node.js 25 | and browser installations, but breaks CrossCode due to the nw.js environment, which combines aspects of the browser 26 | and the web. Previously, the project depended on a packed tarball custom-made for CrossCode by the maintainer of 27 | Archipelago.js, which caused a significant amount of friction for people checking out the code for the first time. 28 | * The upstream is not used in many other APWorlds and is therefore somewhat untested for this purpose. The fork will 29 | contain these fixes before they are merged into main. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Client for CrossCode Archipelago Integration 2 | 3 | This is still under heavy development, though it works and will, for the most part, be pretty stable. 4 | 5 | # Discord server 6 | 7 | [**Join our Discord to help with alpha and beta testing.**](https://discord.gg/ZSWfgQdfGr) 8 | 9 | # Setup Guide 10 | 11 | See this [page on setup](https://github.com/CodeTriangle/CCMultiworldRandomizer/wiki/Setup) to learn how to play 12 | randomized CrossCode. 13 | -------------------------------------------------------------------------------- /assets/data/lang/sc/gui.en_US.json.patch: -------------------------------------------------------------------------------- 1 | { 2 | "labels": { 3 | "menu": { 4 | "menu-titles": { 5 | "apConnection": "Archipelago" 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /assets/data/maps/arid-dng/second/f99/boss-2.json.patch: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "ENTER", 4 | "index": ["entities", 180, "settings", "event"] 5 | }, 6 | { 7 | "type": "ADD_ARRAY_ELEMENT", 8 | "index": 30, 9 | "content": { 10 | "type": "MW_GOAL_COMPLETED", 11 | "goal": "creator" 12 | } 13 | }, 14 | { 15 | "type": "EXIT" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /assets/data/maps/cold-dng/b3/room7.json.patch: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "ENTER", 4 | "index": ["entities", 41, "settings", "event", 32, "thenStep"] 5 | }, 6 | { 7 | "type": "FOR_IN", 8 | "keyword": "__UNUSED__", 9 | "values": [1, 2, 3], 10 | "body": [ 11 | { 12 | "type": "REMOVE_ARRAY_ELEMENT", 13 | "index": 1 14 | } 15 | ] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /assets/data/maps/meta-space.json.patch: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "ENTER", 4 | "index": ["entities", 4, "settings", "event"] 5 | }, 6 | { 7 | "type": "ADD_ARRAY_ELEMENT", 8 | "content": { 9 | "type": "MW_GOAL_COMPLETED", 10 | "goal": "creator" 11 | } 12 | }, 13 | { 14 | "type": "EXIT" 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /assets/media/entity/objects/treasure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeTriangle/CCMultiworldRandomizer/5af9f12650420ea4b4a5f28de2bd7a2618ff29d7/assets/media/entity/objects/treasure.png -------------------------------------------------------------------------------- /assets/media/font/icons-multiworld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeTriangle/CCMultiworldRandomizer/5af9f12650420ea4b4a5f28de2bd7a2618ff29d7/assets/media/font/icons-multiworld.png -------------------------------------------------------------------------------- /assets/media/gui/archipelago-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeTriangle/CCMultiworldRandomizer/5af9f12650420ea4b4a5f28de2bd7a2618ff29d7/assets/media/gui/archipelago-start.png -------------------------------------------------------------------------------- /balancing.md: -------------------------------------------------------------------------------- 1 | This document contains the various current balancing proposals 2 | 3 | ## Zelda Mode 4 | 5 | Proposed by Juanba: https://discord.com/channels/1145762453658026104/1145762454106800149/1198713702552178808 6 | 7 | Remove RPG elements entirely, letting the player focus on the more Zelda-like aspects of the game: dungeon crawling and exploration. 8 | 9 | * Experience and equipment are disabled. 10 | * Enemies are level 1. 11 | 12 | ### Pros 13 | 14 | * Deals cleanly with the equipment question so we don't have to think about it. 15 | 16 | ### Cons 17 | 18 | * No arts, which will lead to boring combat. 19 | * Making combat more interesting will lead to a run with almost no challenge. 20 | * Limited customization of playstyle. 21 | 22 | ## Zelda Mode+ 23 | 24 | Proposed by me, right now. 25 | 26 | Remove most RPG elements, but add them back in a limited way so that there is some progression. 27 | 28 | * Experience is disabled. 29 | * Lea is ~level 10. 30 | * Enemies range from ~level 10 to ~level 20. 31 | * A few levels of progressive gear are available, which raise your level to ~level 20. 32 | 33 | ### Pros 34 | 35 | * Mimics a Zelda randomizer more closely. 36 | * Enemies are initially hard to beat but get easier as you receive more progressive equipment. 37 | * You can still face the final boss with limited gear, but it will be easier with better gear. 38 | 39 | ### Cons 40 | 41 | * Limited arts, limited customization of playstyle. 42 | * Might be *extremely* difficult to balance. 43 | 44 | ## Global Boost Mode 45 | 46 | Proposed by CodeTriangle: https://discord.com/channels/1145762453658026104/1157002643177230368/1196284030698668142 47 | 48 | Allow the player to boost at their leisure, but keep the challenge with the bosses. 49 | 50 | * Initially decrease all enemy levels to a universally low band of values, i.e. ~level 4 to ~level 15 51 | * Shuffle boosters into the pool, each of which boosts the level of every enemy by ~10 levels. 52 | * Bosses retain their vanilla levels (with an optional multiplier perhaps) and do not boost like most enemies. 53 | * Bosses are not in logic until Lea has enough boosters to surpass their level. 54 | 55 | ### Pros 56 | 57 | * Even and fair gradient for leveling up. 58 | * Maintains challenge by requiring the player to beat a level 60 creator (but allows them to level past it for an easier fight anyway). 59 | * Ensures the player can make the game as easy or as hard as they want at any moment. 60 | 61 | ### Cons 62 | 63 | * May require large amounts of back-and-forth to find the enemies that are actually at your level once a booster is engaged. 64 | * May lead to a serious grind. 65 | * The interface to activate this would be annoying to access. 66 | 67 | ## Get Down to My Level 68 | 69 | Proposed by CodeTriangle: https://discord.com/channels/1145762453658026104/1157002643177230368/1171185795231662151 70 | 71 | * Everything is vanilla except enemies are only normalized down, not up. 72 | 73 | ### Pros 74 | 75 | * Allows the player to grind to the max level in the area that they will need. 76 | 77 | ### Cons 78 | 79 | * Encourages players to just grind levels wherever they are even if that means grinding from level 10 to level 40 in So'najiz. 80 | 81 | ## Level Zones 82 | 83 | Proposed by GodlFire: https://discord.com/channels/1145762453658026104/1157002643177230368/1196867964532707368 84 | 85 | * Experience is turned off. 86 | * Each area is assigned a band of levels, in a logical manner. 87 | * Progressive levels are items. 88 | * Areas are not in logic until the player has enough levels to reach it. 89 | 90 | ### Pros 91 | 92 | * There will be a logical path that has enemies at your level so its not just a curb stomp or getting stomped. 93 | * You retain the idea of getting stronger as you progress through the seed as older zones are now much lower level. (editado) 94 | 95 | ### Cons 96 | 97 | * Requires progressive levels in order to work as intended. 98 | -------------------------------------------------------------------------------- /build.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild'; 2 | import {polyfillNode} from 'esbuild-plugin-polyfill-node'; 3 | import * as fs from 'fs'; 4 | 5 | if (!fs.existsSync("mw-rando")) { 6 | fs.mkdirSync("mw-rando"); 7 | } 8 | 9 | esbuild.build({ 10 | "target": "es2018", 11 | "format": "esm", 12 | "platform": "node", 13 | "bundle": true, 14 | "sourcemap": "inline", 15 | "outfile": "mw-rando/plugin.js", 16 | "plugins": [ polyfillNode({ 17 | "polyfills": { 18 | "crypto": true, 19 | "fs": false, 20 | "events": false, 21 | } 22 | }) ], 23 | "entryPoints": ["src/plugin.ts"], 24 | }); 25 | -------------------------------------------------------------------------------- /bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version="$(jq -r =0.4.1", 15 | "nax-ccuilib": ">=1.2.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /data/in/cutscenes.json: -------------------------------------------------------------------------------- 1 | { 2 | "cutscenes": { 3 | "Talatu Introductions": { 4 | "location": { 5 | "area": "autumn-area", 6 | "map": "autumn.path-8", 7 | "mapId": 458, 8 | "path": ".settings.event.21.accepted.6" 9 | }, 10 | "region": { 11 | "linear": "3", 12 | "open": "open3" 13 | }, 14 | "properties": { 15 | "alwaysQuest": true 16 | }, 17 | "reward": [ 18 | [ "item", "Disc of Flora", 1 ] 19 | ] 20 | }, 21 | "Schneider Guild Pass": { 22 | "location": { 23 | "area": "autumn-area", 24 | "map": "autumn.path-3-1", 25 | "mapId": 452, 26 | "path": ".settings.npcStates.1.event.10" 27 | }, 28 | "region": { 29 | "linear": "3.1", 30 | "open": "open3.1" 31 | }, 32 | "reward": [ 33 | [ "item", "Guild Pass", 1 ] 34 | ] 35 | }, 36 | "Bergen Mine Guard": { 37 | "location": { 38 | "area": "bergen", 39 | "map": "bergen.mine-entrance", 40 | "mapId": 316, 41 | "path": ".settings.npcStates.0.event.quest.2.thenStep.17" 42 | }, 43 | "region": { 44 | "linear": "4", 45 | "open": "open4.1" 46 | }, 47 | "reward": [ 48 | [ "item", "Just Water", 2 ] 49 | ] 50 | }, 51 | "Bergen Mine Detector": { 52 | "location": { 53 | "area": "bergen", 54 | "map": "bergen.inner-questhub-1", 55 | "mapId": 135, 56 | "path": ".settings.npcStates.1.event.quest.5" 57 | }, 58 | "region": { 59 | "linear": "3", 60 | "open": "open3" 61 | }, 62 | "properties": { 63 | "alwaysQuest": true 64 | }, 65 | "reward": [ 66 | [ "item", "Mine Detector", 1] 67 | ] 68 | }, 69 | "Temple Mine Shade Statue": { 70 | "location": { 71 | "area": "cold-dng", 72 | "map": "cold-dng.g.expo-space", 73 | "mapId": 136, 74 | "path": ".settings.event.20" 75 | }, 76 | "region": { 77 | "linear": "10", 78 | "open": "open4.1" 79 | }, 80 | "condition": [ 81 | ["item", "Mine Master Key", 1] 82 | ], 83 | "reward": [ 84 | [ "item", "Blue Ice Shade", 1 ] 85 | ] 86 | }, 87 | "Old Dojo Shade Statue": { 88 | "location": { 89 | "area": "forest", 90 | "map": "forest.interior.expo-space", 91 | "mapId": 137, 92 | "path": ".settings.event.19" 93 | }, 94 | "region": { 95 | "linear": "32", 96 | "open": "open17" 97 | }, 98 | "reward": [ 99 | [ "item", "Meteor Shade", 1 ] 100 | ] 101 | }, 102 | "Vagabond Dojo Key": { 103 | "location": { 104 | "area": "forest", 105 | "map": "forest.path-04-vaga", 106 | "mapId": 447, 107 | "path": ".settings.npcStates.2.event.quest.12" 108 | }, 109 | "region": { 110 | "linear": "31", 111 | "open": "open16" 112 | }, 113 | "reward": [ 114 | [ "item", "Old Dojo Key", 1 ] 115 | ] 116 | }, 117 | "Faj'ro Shade Statue": { 118 | "location": { 119 | "area": "heat-dng", 120 | "map": "heat.dng-expo-space", 121 | "mapId": 136, 122 | "path": ".settings.event.20" 123 | }, 124 | "region": { 125 | "linear": "19", 126 | "open": "open7.8" 127 | }, 128 | "reward": [ 129 | [ "item", "Red Flame Shade", 1 ] 130 | ] 131 | }, 132 | "Ba'kii Kum Cursed Man": { 133 | "location": { 134 | "area": "heat-village", 135 | "map": "heat-village.interior.house-2", 136 | "mapId": 31, 137 | "path": ".settings.npcStates.0.event.4.0.9" 138 | }, 139 | "region": { 140 | "linear": "11", 141 | "open": "open5" 142 | }, 143 | "condition": [ 144 | [ "item", "Cold", 1 ] 145 | ], 146 | "reward": [ 147 | [ "item", "Cursed Coin", 1 ] 148 | ] 149 | }, 150 | "Ba'kii Eldress": { 151 | "location": { 152 | "area": "heat-village", 153 | "map": "heat-village.interior.quest-hub-og", 154 | "mapId": 112, 155 | "path": ".settings.npcStates.1.event.quest.3.accepted.3" 156 | }, 157 | "region": { 158 | "linear": "11", 159 | "open": "open5" 160 | }, 161 | "reward": [ 162 | [ "item", "Maroon Cave Pass", 1 ] 163 | ] 164 | }, 165 | "Ba'kii Shade Statue": { 166 | "location": { 167 | "area": "heat-village", 168 | "map": "heat-village.special.expo-space", 169 | "mapId": 136, 170 | "path": ".settings.event.17" 171 | }, 172 | "region": { 173 | "linear": "12", 174 | "open": "open6" 175 | }, 176 | "properties": { 177 | "alwaysQuest": true 178 | }, 179 | "reward": [ 180 | [ "item", "Yellow Sand Shade", 1 ] 181 | ] 182 | }, 183 | "Zir'vitar Shade Statue": { 184 | "location": { 185 | "area": "shock-dng", 186 | "map": "jungle.dng.shock-expo-space", 187 | "mapId": 136, 188 | "path": ".settings.event.20" 189 | }, 190 | "region": { 191 | "linear": "25", 192 | "open": "open13.2" 193 | }, 194 | "condition": [ 195 | [ "item", "Wave", 1 ] 196 | ], 197 | "reward": [ 198 | [ "item", "Purple Bolt Shade", 1 ] 199 | ] 200 | }, 201 | "Krys'kajo Shade Statue": { 202 | "location": { 203 | "area": "tree-dng", 204 | "map": "jungle.dng.tree-expo-space", 205 | "mapId": 60, 206 | "path": ".settings.event.25" 207 | }, 208 | "region": { 209 | "linear": "30", 210 | "open": "open15.3" 211 | }, 212 | "reward": [ 213 | [ "item", "Star Shade", 1 ] 214 | ] 215 | }, 216 | "So'najiz Shade Statue": { 217 | "location": { 218 | "area": "wave-dng", 219 | "map": "jungle.dng.wave-expo-space", 220 | "mapId": 136, 221 | "path": ".settings.event.20" 222 | }, 223 | "region": { 224 | "linear": "27", 225 | "open": "open14.5" 226 | }, 227 | "reward": [ 228 | [ "item", "Azure Drop Shade", 1 ] 229 | ] 230 | }, 231 | "Rookie Harbor Shade": { 232 | "location": { 233 | "area": "rookie-harbor", 234 | "map": "rookie-harbor.expo-space", 235 | "mapId": 136, 236 | "path": ".settings.event.74" 237 | }, 238 | "region": { 239 | "linear": "2", 240 | "open": "open2" 241 | }, 242 | "reward": [ 243 | [ "item", "Green Leaf Shade", 1 ] 244 | ] 245 | }, 246 | "Rookie Harbor Disc": { 247 | "location": { 248 | "area": "rookie-harbor", 249 | "map": "rookie-harbor.expo-space", 250 | "mapId": 136, 251 | "path": ".settings.event.90" 252 | }, 253 | "region": { 254 | "linear": "2", 255 | "open": "open2" 256 | }, 257 | "reward": [ 258 | [ "item", "Disc of Insight", 1 ] 259 | ] 260 | }, 261 | "Mine Circuit Override": { 262 | "location": { 263 | "area": "cold-dng", 264 | "map": "rookie-harbor.expo-space", 265 | "mapId": 137, 266 | "path": ".settings.event.36" 267 | }, 268 | "region": { 269 | "linear": "11", 270 | "open": "open4.8" 271 | }, 272 | "reward": [ 273 | [ "item", "Circuit Override", 1 ] 274 | ] 275 | }, 276 | "Faj'ro Circuit Override": { 277 | "location": { 278 | "area": "heat-dng", 279 | "map": "rookie-harbor.expo-space", 280 | "mapId": 138, 281 | "path": ".settings.event.35" 282 | }, 283 | "region": { 284 | "linear": "19", 285 | "open": "open7.8" 286 | }, 287 | "reward": [ 288 | [ "item", "Circuit Override", 1 ] 289 | ] 290 | }, 291 | "Krys'kajo Circuit Override": { 292 | "location": { 293 | "area": "tree-dng", 294 | "map": "rookie-harbor.expo-space", 295 | "mapId": 140, 296 | "path": ".settings.event.36" 297 | }, 298 | "region": { 299 | "linear": "30", 300 | "open": "open15.3" 301 | }, 302 | "reward": [ 303 | [ "item", "Circuit Override", 2 ] 304 | ] 305 | }, 306 | "So'najiz Circuit Override": { 307 | "location": { 308 | "area": "wave-dng", 309 | "map": "rookie-harbor.expo-space", 310 | "mapId": 142, 311 | "path": ".settings.event.14" 312 | }, 313 | "region": { 314 | "linear": "27", 315 | "open": "open14.4" 316 | }, 317 | "reward": [ 318 | [ "item", "Circuit Override", 1 ] 319 | ] 320 | }, 321 | "Zir'vitar Circuit Override": { 322 | "location": { 323 | "area": "shock-dng", 324 | "map": "rookie-harbor.expo-space", 325 | "mapId": 143, 326 | "path": ".settings.event.14" 327 | }, 328 | "region": { 329 | "linear": "25", 330 | "open": "open13.2" 331 | }, 332 | "condition": [ 333 | [ "item", "Wave", 1 ] 334 | ], 335 | "reward": [ 336 | [ "item", "Circuit Override", 1 ] 337 | ] 338 | } , 339 | "Faj'ro SP Upgrade": { 340 | "location": { 341 | "area": "heat-dng", 342 | "map": "heat.dng-expo-space", 343 | "mapId": 136, 344 | "path": ".settings.event.38" 345 | }, 346 | "region": { 347 | "linear": "19", 348 | "open": "open7.8" 349 | }, 350 | "reward": [ 351 | [ "sp" ] 352 | ] 353 | }, 354 | "Krys'kajo SP Upgrade": { 355 | "location": { 356 | "area": "tree-dng", 357 | "map": "jungle.dng.tree-expo-space", 358 | "mapId": 60, 359 | "path": ".settings.event.44" 360 | }, 361 | "region": { 362 | "linear": "30", 363 | "open": "open15.3" 364 | }, 365 | "reward": [ 366 | [ "sp" ] 367 | ] 368 | } 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /data/in/elements.json: -------------------------------------------------------------------------------- 1 | { 2 | "elements": { 3 | "Heat Pedestal": { 4 | "location": { 5 | "area": "cold-dng", 6 | "map": "cold-dng.b3.room7", 7 | "mapId": 45 8 | }, 9 | "region": { 10 | "linear": "8", 11 | "open": "open4.5" 12 | }, 13 | "reward": [ 14 | ["element", "Heat"] 15 | ] 16 | }, 17 | "Cold Pedestal": { 18 | "location": { 19 | "area": "heat-dng", 20 | "map": "heat-dng.f2.room-cold", 21 | "mapId": 78 22 | }, 23 | "region": { 24 | "linear": "16", 25 | "open": "open7.4" 26 | }, 27 | "reward": [ 28 | ["element", "Cold"] 29 | ] 30 | }, 31 | "Shock Pedestal": { 32 | "location": { 33 | "area": "wave-dng", 34 | "map": "wave-dng.b1.center-05-element", 35 | "mapId": 248 36 | }, 37 | "region": { 38 | "linear": "27", 39 | "open": "open14.5" 40 | }, 41 | "reward": [ 42 | ["element", "Shock"] 43 | ] 44 | }, 45 | "Wave Pedestal": { 46 | "location": { 47 | "area": "shock-dng", 48 | "map": "shock-dng.f2.room-element", 49 | "mapId": 86 50 | }, 51 | "region": { 52 | "linear": "25", 53 | "open": "open13.1" 54 | }, 55 | "reward": [ 56 | ["element", "Wave"] 57 | ] 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /data/in/item-pools/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemPools": { 3 | "fillerCommonCons": [ 4 | { 5 | "item": ["item", "Sandwich", 3], 6 | "quantity": 40 7 | }, 8 | { 9 | "item": ["item", "Hi-Sandwich", 3], 10 | "quantity": 40 11 | }, 12 | { 13 | "item": ["item", "Green Leaf Tea", 2], 14 | "quantity": 40 15 | }, 16 | { 17 | "item": ["item", "Spicy Bun", 3], 18 | "quantity": 30 19 | }, 20 | { 21 | "item": ["item", "Meaty Risotto", 1], 22 | "quantity": 30 23 | }, 24 | { 25 | "item": ["item", "Fruit Salad", 3], 26 | "quantity": 25 27 | }, 28 | { 29 | "item": ["item", "Veggie Wraps", 3], 30 | "quantity": 25 31 | } 32 | ], 33 | "fillerCommonDrop": [ 34 | { 35 | "item": ["item", "Autumn Leaves", 4], 36 | "quantity": 15 37 | }, 38 | { 39 | "item": ["item", "Gold Beetle", 4], 40 | "quantity": 15 41 | }, 42 | { 43 | "item": ["item", "Feather Leaf", 5], 44 | "quantity": 17 45 | }, 46 | { 47 | "item": ["item", "Pike Wood", 5], 48 | "quantity": 17 49 | }, 50 | { 51 | "item": ["item", "Bergen Ice", 5], 52 | "quantity": 9 53 | }, 54 | { 55 | "item": ["item", "Bug Shell", 5], 56 | "quantity": 13 57 | }, 58 | { 59 | "item": ["item", "Tough Sand", 4], 60 | "quantity": 13 61 | }, 62 | { 63 | "item": ["item", "Vivid Water", 5], 64 | "quantity": 20 65 | }, 66 | { 67 | "item": ["item", "Crystal Leek", 4], 68 | "quantity": 15 69 | }, 70 | { 71 | "item": ["item", "Old Bones", 4], 72 | "quantity": 12 73 | }, 74 | { 75 | "item": ["item", "Parched Leaves", 4], 76 | "quantity": 13 77 | }, 78 | { 79 | "item": ["item", "White Grain", 4], 80 | "quantity": 13 81 | }, 82 | { 83 | "item": ["item", "Curly Fern", 4], 84 | "quantity": 11 85 | }, 86 | { 87 | "item": ["item", "Common Planter", 4], 88 | "quantity": 11 89 | }, 90 | { 91 | "item": ["item", "Blue Mango", 4], 92 | "quantity": 11 93 | }, 94 | { 95 | "item": ["item", "Venom Shroom", 4], 96 | "quantity": 11 97 | }, 98 | { 99 | "item": ["item", "Green Arbor", 4], 100 | "quantity": 9 101 | }, 102 | { 103 | "item": ["item", "Blue Grass", 4], 104 | "quantity": 9 105 | }, 106 | { 107 | "item": ["item", "Dirty Rubble", 4], 108 | "quantity": 9 109 | } 110 | ] 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /data/in/item-pools/epic.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemPools": { 3 | "fillerEpicCons": [ 4 | { 5 | "item": ["item", "Cross Sandwich", 2 ], 6 | "quantity": 50 7 | }, 8 | { 9 | "item": ["item", "Pepper Night Tea", 1 ], 10 | "quantity": 40 11 | }, 12 | { 13 | "item": ["item", "Blazing Bun", 1 ], 14 | "quantity": 35 15 | }, 16 | { 17 | "item": ["item", "Chili Dog", 1 ], 18 | "quantity": 30 19 | }, 20 | { 21 | "item": ["item", "Gourmet Steak", 2 ], 22 | "quantity": 30 23 | }, 24 | { 25 | "item": ["item", "Cold Platter", 2 ], 26 | "quantity": 25 27 | }, 28 | { 29 | "item": ["item", "Green Risotto", 2 ], 30 | "quantity": 20 31 | }, 32 | { 33 | "item": ["item", "Bear Beer", 3 ], 34 | "quantity": 20 35 | }, 36 | { 37 | "item": ["item", "Crab Mead", 3 ], 38 | "quantity": 15 39 | }, 40 | { 41 | "item": ["item", "Whale Wine", 3 ], 42 | "quantity": 15 43 | } 44 | ], 45 | "fillerEpicDrop": [ 46 | { 47 | "item": ["item", "Bear Cicada", 3 ], 48 | "quantity": 14 49 | }, 50 | { 51 | "item": ["item", "Azure Dragonfly", 3 ], 52 | "quantity": 18 53 | }, 54 | { 55 | "item": ["item", "Frozen Tear", 3 ], 56 | "quantity": 18 57 | }, 58 | { 59 | "item": ["item", "Winter Thorn", 2 ], 60 | "quantity": 16 61 | }, 62 | { 63 | "item": ["item", "Blue Orb", 2 ], 64 | "quantity": 14 65 | }, 66 | { 67 | "item": ["item", "Rainbow Gem", 3 ], 68 | "quantity": 12 69 | }, 70 | { 71 | "item": ["item", "Maroon Chestnut", 4 ], 72 | "quantity": 15 73 | }, 74 | { 75 | "item": ["item", "Ancient Earth", 3 ], 76 | "quantity": 14 77 | }, 78 | { 79 | "item": ["item", "Lucid Shard", 3 ], 80 | "quantity": 16 81 | }, 82 | { 83 | "item": ["item", "Wolf Cicada", 4 ], 84 | "quantity": 15 85 | }, 86 | { 87 | "item": ["item", "Crimson Dragonfly", 3 ], 88 | "quantity": 14 89 | }, 90 | { 91 | "item": ["item", "Elder Wood", 3 ], 92 | "quantity": 18 93 | }, 94 | { 95 | "item": ["item", "Royal Hive", 2 ], 96 | "quantity": 13 97 | }, 98 | { 99 | "item": ["item", "Moon Fruit", 3 ], 100 | "quantity": 16 101 | }, 102 | { 103 | "item": ["item", "Star Fruit", 2 ], 104 | "quantity": 16 105 | }, 106 | { 107 | "item": ["item", "Virus Root", 2 ], 108 | "quantity": 16 109 | }, 110 | { 111 | "item": ["item", "Steel Bamboo", 4 ], 112 | "quantity": 12 113 | }, 114 | { 115 | "item": ["item", "Mystery Grape", 3 ], 116 | "quantity": 12 117 | }, 118 | { 119 | "item": ["item", "Cobalt Crystal", 4 ], 120 | "quantity": 12 121 | } 122 | ] 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /data/in/item-pools/legendary.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemPools": { 3 | "fillerLegendary": [ 4 | { 5 | "item": ["item", "Final Dinner", 1 ], 6 | "quantity": 30 7 | }, 8 | { 9 | "item": ["item", "Full Course", 1 ], 10 | "quantity": 30 11 | }, 12 | { 13 | "item": ["item", "One Up", 1 ], 14 | "quantity": 30 15 | }, 16 | { 17 | "item": ["item", "Rising Super Star", 1 ], 18 | "quantity": 25 19 | }, 20 | { 21 | "item": ["item", "Guacamole Toast", 1 ], 22 | "quantity": 25 23 | }, 24 | { 25 | "item": ["item", "Cheese Spaetzle", 1 ], 26 | "quantity": 20 27 | }, 28 | { 29 | "item": ["item", "Spicy Beat-0-Type", 1 ], 30 | "quantity": 20 31 | }, 32 | { 33 | "item": ["item", "Durian", 1 ], 34 | "quantity": 20 35 | }, 36 | { 37 | "item": ["item", "Dk Pepper", 1 ], 38 | "quantity": 15 39 | }, 40 | { 41 | "item": ["item", "Mooncake", 1 ], 42 | "quantity": 12 43 | } 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /data/in/item-pools/master.json: -------------------------------------------------------------------------------- 1 | { 2 | "_includes": [ 3 | "required.json", 4 | "common.json", 5 | "rare.json", 6 | "epic.json", 7 | "legendary.json", 8 | "pets.json" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /data/in/item-pools/pets.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemPools": { 3 | "pets": [ 4 | { 5 | "item": ["item", "Orange Twister"], 6 | "quantity": 1 7 | }, 8 | { 9 | "item": ["item", "Baby Peng"], 10 | "quantity": 1 11 | }, 12 | { 13 | "item": ["item", "FDNI Fox"], 14 | "quantity": 1 15 | }, 16 | { 17 | "item": ["item", "Good Boy"], 18 | "quantity": 1 19 | }, 20 | { 21 | "item": ["item", "S-Rex"], 22 | "quantity": 1 23 | }, 24 | { 25 | "item": ["item", "Lil' Reap"], 26 | "quantity": 1 27 | }, 28 | { 29 | "item": ["item", "Red Liz"], 30 | "quantity": 1 31 | }, 32 | { 33 | "item": ["item", "Shy Fly"], 34 | "quantity": 1 35 | }, 36 | { 37 | "item": ["item", "T.A.N.K."], 38 | "quantity": 1 39 | }, 40 | { 41 | "item": ["item", "Micro Crawler"], 42 | "quantity": 1 43 | }, 44 | { 45 | "item": ["item", "Cool Bar"], 46 | "quantity": 1 47 | }, 48 | { 49 | "item": ["item", "Batboy"], 50 | "quantity": 1 51 | }, 52 | { 53 | "item": ["item", "Butterfliege"], 54 | "quantity": 1 55 | }, 56 | { 57 | "item": ["item", "Piggybank"], 58 | "quantity": 1 59 | }, 60 | { 61 | "item": ["item", "Seedling"], 62 | "quantity": 1 63 | }, 64 | { 65 | "item": ["item", "Turbo"], 66 | "quantity": 1 67 | }, 68 | { 69 | "item": ["item", "I Rob-0T"], 70 | "quantity": 1 71 | }, 72 | { 73 | "item": ["item", "Baby of the East"], 74 | "quantity": 1 75 | }, 76 | { 77 | "item": ["item", "Goose"], 78 | "quantity": 1 79 | }, 80 | { 81 | "item": ["item", "Nuclear Roach"], 82 | "quantity": 1 83 | }, 84 | { 85 | "item": ["item", "Chunky"], 86 | "quantity": 1 87 | } 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /data/in/item-pools/rare.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemPools": { 3 | "fillerRareCons": [ 4 | { 5 | "item": ["item", "Chef Sandwich", 3 ], 6 | "quantity": 40 7 | }, 8 | { 9 | "item": ["item", "Mega-Sandwich", 2 ], 10 | "quantity": 35 11 | }, 12 | { 13 | "item": ["item", "Sweet Berry Tea", 2 ], 14 | "quantity": 40 15 | }, 16 | { 17 | "item": ["item", "Flaming Bun", 3 ], 18 | "quantity": 30 19 | }, 20 | { 21 | "item": ["item", "Steak, rare", 3 ], 22 | "quantity": 30 23 | }, 24 | { 25 | "item": ["item", "Chili Con Carne", 2 ], 26 | "quantity": 30 27 | }, 28 | { 29 | "item": ["item", "Shrimp Risotto", 1 ], 30 | "quantity": 20 31 | }, 32 | { 33 | "item": ["item", "Fruit Pie", 3 ], 34 | "quantity": 15 35 | }, 36 | { 37 | "item": ["item", "Ginger Tom. Salad", 3 ], 38 | "quantity": 10 39 | } 40 | ], 41 | "fillerRareDrop": [ 42 | { 43 | "item": ["item", "Season Apples", 5 ], 44 | "quantity": 18 45 | }, 46 | { 47 | "item": ["item", "Twilight Dew", 5 ], 48 | "quantity": 18 49 | }, 50 | { 51 | "item": ["item", "Rusty Bits", 5 ], 52 | "quantity": 18 53 | }, 54 | { 55 | "item": ["item", "Metal Gears", 3 ], 56 | "quantity": 15 57 | }, 58 | { 59 | "item": ["item", "Purple Ore Lump", 6 ], 60 | "quantity": 21 61 | }, 62 | { 63 | "item": ["item", "Arid Lumber", 4 ], 64 | "quantity": 17 65 | }, 66 | { 67 | "item": ["item", "Cactone Fruit", 4 ], 68 | "quantity": 15 69 | }, 70 | { 71 | "item": ["item", "Palmapple Seed", 3 ], 72 | "quantity": 16 73 | }, 74 | { 75 | "item": ["item", "Glaring Rock", 4 ], 76 | "quantity": 16 77 | }, 78 | { 79 | "item": ["item", "Helix Relic", 5 ], 80 | "quantity": 14 81 | }, 82 | { 83 | "item": ["item", "Ripe Apples", 5 ], 84 | "quantity": 17 85 | }, 86 | { 87 | "item": ["item", "Sunset Dew", 5 ], 88 | "quantity": 17 89 | }, 90 | { 91 | "item": ["item", "Steel Comb", 4 ], 92 | "quantity": 16 93 | }, 94 | { 95 | "item": ["item", "Spiky Nut", 4 ], 96 | "quantity": 18 97 | }, 98 | { 99 | "item": ["item", "Exotic Resin", 3 ], 100 | "quantity": 15 101 | }, 102 | { 103 | "item": ["item", "Glowing Sphere", 4 ], 104 | "quantity": 17 105 | }, 106 | { 107 | "item": ["item", "Catalop Pellet", 4 ], 108 | "quantity": 13 109 | }, 110 | { 111 | "item": ["item", "Pink Petal", 3 ], 112 | "quantity": 13 113 | }, 114 | { 115 | "item": ["item", "Spark Tin", 3 ], 116 | "quantity": 11 117 | } 118 | ] 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /data/in/item-pools/required.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemPools": { 3 | "required": [ 4 | { 5 | "item": ["item", "Heat"], 6 | "quantity": 1 7 | }, 8 | { 9 | "item": ["item", "Cold"], 10 | "quantity": 1 11 | }, 12 | { 13 | "item": ["item", "Shock"], 14 | "quantity": 1 15 | }, 16 | { 17 | "item": ["item", "Wave"], 18 | "quantity": 1 19 | }, 20 | { 21 | "item": ["item", "SP Upgrade"], 22 | "quantity": 2 23 | }, 24 | { 25 | "item": ["item", "Disc of Flora"], 26 | "quantity": 1 27 | }, 28 | { 29 | "item": ["item", "Disc of Insight"], 30 | "quantity": 1 31 | }, 32 | { 33 | "item": ["item", "Cursed Coin"], 34 | "quantity": 1 35 | }, 36 | { 37 | "item": ["item", "Mine Pass"], 38 | "quantity": 1 39 | }, 40 | { 41 | "item": ["item", "Guild Pass"], 42 | "quantity": 1 43 | }, 44 | { 45 | "item": ["item", "Maroon Cave Pass"], 46 | "quantity": 1 47 | }, 48 | { 49 | "item": ["item", "Pond Slums Pass"], 50 | "quantity": 1 51 | }, 52 | { 53 | "item": ["item", "Blue Ice Shade"], 54 | "quantity": 1 55 | }, 56 | { 57 | "item": ["item", "Meteor Shade"], 58 | "quantity": 1 59 | }, 60 | { 61 | "item": ["item", "Red Flame Shade"], 62 | "quantity": 1 63 | }, 64 | { 65 | "item": ["item", "Yellow Sand Shade"], 66 | "quantity": 1 67 | }, 68 | { 69 | "item": ["item", "Purple Bolt Shade"], 70 | "quantity": 1 71 | }, 72 | { 73 | "item": ["item", "Star Shade"], 74 | "quantity": 1 75 | }, 76 | { 77 | "item": ["item", "Azure Drop Shade"], 78 | "quantity": 1 79 | }, 80 | { 81 | "item": ["item", "Green Leaf Shade"], 82 | "quantity": 1 83 | }, 84 | { 85 | "item": ["item", "Green Seed Shade"], 86 | "quantity": 1 87 | }, 88 | { 89 | "item": ["item", "Mine Key"], 90 | "quantity": 5 91 | }, 92 | { 93 | "item": ["item", "Faj'ro Key"], 94 | "quantity": 9 95 | }, 96 | { 97 | "item": ["item", "Zir'vitar Key"], 98 | "quantity": 2 99 | }, 100 | { 101 | "item": ["item", "So'najiz Key"], 102 | "quantity": 4 103 | }, 104 | { 105 | "item": ["item", "Krys'kajo Key"], 106 | "quantity": 2 107 | }, 108 | { 109 | "item": ["item", "Thief's Key"], 110 | "quantity": 1 111 | }, 112 | { 113 | "item": ["item", "White Key"], 114 | "quantity": 1 115 | }, 116 | { 117 | "item": ["item", "Radiant Key"], 118 | "quantity": 1 119 | }, 120 | { 121 | "item": ["item", "Mine Master Key"], 122 | "quantity": 1 123 | }, 124 | { 125 | "item": ["item", "Faj'ro Master Key"], 126 | "quantity": 1 127 | }, 128 | { 129 | "item": ["item", "Kajo Master Key"], 130 | "quantity": 1 131 | }, 132 | { 133 | "item": ["item", "Old Dojo Key"], 134 | "quantity": 1 135 | }, 136 | { 137 | "item": ["item", "Broken Gauntlet"], 138 | "quantity": 1 139 | }, 140 | { 141 | "item": ["item", "Broken Shield"], 142 | "quantity": 1 143 | }, 144 | { 145 | "item": ["item", "Broken Chakrams"], 146 | "quantity": 1 147 | }, 148 | { 149 | "item": ["item", "Broken Sword"], 150 | "quantity": 1 151 | }, 152 | { 153 | "item": ["item", "Broken Deck"], 154 | "quantity": 1 155 | }, 156 | { 157 | "item": ["item", "Old Blueprint"], 158 | "quantity": 1 159 | }, 160 | { 161 | "item": ["item", "Very Large Ember"], 162 | "quantity": 1 163 | }, 164 | { 165 | "item": ["item", "Halcyon Droplet"], 166 | "quantity": 1 167 | }, 168 | { 169 | "item": ["item", "Portrait of Ruin"], 170 | "quantity": 1 171 | }, 172 | { 173 | "item": ["item", "Heaven's Seed"], 174 | "quantity": 1 175 | }, 176 | { 177 | "item": ["item", "Everlasting Amber"], 178 | "quantity": 1 179 | }, 180 | { 181 | "item": ["item", "Galaxy Berry"], 182 | "quantity": 1 183 | }, 184 | { 185 | "item": ["item", "Dream Globe"], 186 | "quantity": 1 187 | }, 188 | { 189 | "item": ["item", "Tremor Engine"], 190 | "quantity": 4 191 | }, 192 | { 193 | "item": ["item", "Geta Wood"], 194 | "quantity": 1 195 | }, 196 | { 197 | "item": ["item", "Geta Glue"], 198 | "quantity": 1 199 | }, 200 | { 201 | "item": ["item", "Hungry Salmon"], 202 | "quantity": 1 203 | }, 204 | { 205 | "item": ["item", "Mysterious Box"], 206 | "quantity": 1 207 | }, 208 | { 209 | "item": ["item", "Omni Lock"], 210 | "quantity": 1 211 | }, 212 | { 213 | "item": ["item", "Golden Triangle"], 214 | "quantity": 3 215 | }, 216 | { 217 | "item": ["item", "King's Ring"], 218 | "quantity": 1 219 | } 220 | ] 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /data/in/prog-items/areas.json: -------------------------------------------------------------------------------- 1 | { 2 | "progressiveChains": { 3 | "areaItemsAll": { 4 | "displayName": "Area Unlock", 5 | "id": 50, 6 | "classification": "progression", 7 | "description": { 8 | "en_US": "Unlocks the next dungeon or overworld area in story order." 9 | }, 10 | "items": [ 11 | { "item": ["item", "Green Leaf Shade", 1] }, 12 | { "item": ["item", "Mine Pass", 1] }, 13 | { "item": ["item", "Blue Ice Shade", 1] }, 14 | { "item": ["item", "Yellow Sand Shade", 1] }, 15 | { "item": ["item", "Red Flame Shade", 1] }, 16 | { "item": ["item", "Green Seed Shade", 1] }, 17 | { "item": ["item", "Purple Bolt Shade", 1] }, 18 | { "item": ["item", "Azure Drop Shade", 1] }, 19 | { "item": ["item", "Star Shade", 1] }, 20 | { "item": ["item", "Meteor Shade", 1] } 21 | ] 22 | }, 23 | "areaItemsOverworld": { 24 | "displayName": "Overworld Area Unlock", 25 | "id": 51, 26 | "classification": "progression", 27 | "description": { 28 | "en_US": "Unlocks the next overworld area in story order." 29 | }, 30 | "items": [ 31 | { "item": ["item", "Green Leaf Shade", 1] }, 32 | { "item": ["item", "Blue Ice Shade", 1] }, 33 | { "item": ["item", "Red Flame Shade", 1] }, 34 | { "item": ["item", "Green Seed Shade", 1] }, 35 | { "item": ["item", "Star Shade", 1] }, 36 | { "item": ["item", "Meteor Shade", 1] } 37 | ] 38 | }, 39 | "areaItemsDungeons": { 40 | "displayName": "Dungeon Unlock", 41 | "id": 52, 42 | "classification": "progression", 43 | "description": { 44 | "en_US": "Unlocks the next dungeon in story order." 45 | }, 46 | "items": [ 47 | { "item": ["item", "Mine Pass", 1] }, 48 | { "item": ["item", "Yellow Sand Shade", 1] }, 49 | { "item": ["item", "Purple Bolt Shade", 1] }, 50 | { "item": ["item", "Azure Drop Shade", 1] } 51 | ] 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /data/in/prog-items/master.json: -------------------------------------------------------------------------------- 1 | { 2 | "_includes": [ 3 | "areas.json", 4 | "heads.json", 5 | "arms.json", 6 | "torsos.json", 7 | "legs.json" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /data/in/quests-always.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "Quests that are always randomized, even if quest rando is off.", 3 | "quests": { 4 | "Chase the Hologram Frobbit": { 5 | "questid": "daft-frobbit", 6 | "region": { 7 | "linear": "20", 8 | "open": "open9" 9 | }, 10 | "condition": [ 11 | [ "item", "Heat" ], 12 | [ "item", "Cold" ] 13 | ], 14 | "properties": { 15 | "alwaysQuest": true 16 | }, 17 | "reward": [ 18 | [ "item", "Green Seed Shade", 1 ] 19 | ] 20 | }, 21 | "Mushroom Kingdom": { 22 | "questid": "basin-mush-2", 23 | "region": { 24 | "linear": "23", 25 | "open": "open10" 26 | }, 27 | "condition": [ 28 | [ "item", "Wave", 1 ] 29 | ], 30 | "properties": { 31 | "alwaysQuest": true 32 | }, 33 | "reward": [ 34 | [ "item", "Pond Slums Pass", 1 ] 35 | ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /data/in/quests-qr.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "Quests that are only randomized if quest rando is on.", 3 | "_global": { 4 | "/cutscenes|chests|quests/": { 5 | "*": { 6 | "metadata": { 7 | "quest": true 8 | } 9 | } 10 | } 11 | }, 12 | "cutscenes": { 13 | "Talatu Bergen 25%": { 14 | "location": { 15 | "area": "bergen", 16 | "map": "bergen.inner-questhub-1", 17 | "mapId": 160, 18 | "path": ".settings.npcStates.0.event.quest.14" 19 | }, 20 | "region": { 21 | "linear": "3", 22 | "open": "open3" 23 | }, 24 | "condition": [ 25 | [ "cutscene", "Talatu Introductions" ], 26 | [ "item", "Disc of Flora", 1 ] 27 | ], 28 | "reward": [ 29 | [ "item", "Strawhat", 1 ] 30 | ] 31 | }, 32 | "Talatu Ba'kii 50%": { 33 | "location": { 34 | "area": "heat-village", 35 | "map": "heat-village.interior.quest-hub-eg", 36 | "mapId": 134, 37 | "path": ".settings.npcStates.0.event.quest.11" 38 | }, 39 | "region": { 40 | "linear": "11", 41 | "open": "open5" 42 | }, 43 | "condition": [ 44 | [ "cutscene", "Talatu Bergen 25%" ] 45 | ], 46 | "reward": [ 47 | [ "item", "Dried Grass Hat", 1 ] 48 | ] 49 | }, 50 | "Talatu Basin 75%": { 51 | "location": { 52 | "area": "jungle-city", 53 | "map": "jungle-city.interior.questhub-01", 54 | "mapId": 576, 55 | "path": ".settings.npcStates.0.event.quest.11" 56 | }, 57 | "region": { 58 | "linear": "23", 59 | "open": "open10" 60 | }, 61 | "condition": [ 62 | [ "cutscene", "Talatu Ba'kii 50%" ] 63 | ], 64 | "reward": [ 65 | [ "item", "Strawberry Hat", 1 ] 66 | ] 67 | } 68 | }, 69 | "chests": { 70 | "Bergen: Omni Gilders Basement Left": { 71 | "location": { 72 | "area": "bergen", 73 | "map": "bergen.interior.guild-basement", 74 | "mapId": 42 75 | }, 76 | "clearance": "Bronze", 77 | "region": { 78 | "linear": "4", 79 | "open": "open4.1" 80 | }, 81 | "condition": [ 82 | [ "or", [ "item", "Heat" ], [ "item", "Mine Key", 1 ] ], 83 | [ "quest","The Legendary Bunny" ] 84 | ], 85 | "reward": [ 86 | [ 87 | "item", 88 | "Prickly Bracer", 89 | 1 90 | ] 91 | ] 92 | }, 93 | "Trail: Horncave": { 94 | "location": { 95 | "area": "bergen-trails", 96 | "map": "bergen-trail.cave.cave-7-1-1-goat2", 97 | "mapId": 142 98 | }, 99 | "clearance": "Default", 100 | "region": { 101 | "linear": "3", 102 | "open": "open3" 103 | }, 104 | "condition": [ 105 | [ "quest", "The Goatfather" ] 106 | ], 107 | "reward": [ 108 | [ "item", "Vanilla Ice Cream", 2 ] 109 | ] 110 | }, 111 | "Garden: Chilled Den": { 112 | "location": { 113 | "area": "jungle", 114 | "map": "jungle.caves.questcave-snowman-1", 115 | "mapId": 247 116 | }, 117 | "clearance": "Silver", 118 | "region": { 119 | "linear": "10", 120 | "open": "open10" 121 | }, 122 | "condition": [ 123 | [ "item", "Heat" ], 124 | [ "item", "Cold" ] 125 | ], 126 | "reward": [ 127 | [ "item", "Perforated Tophat", 5 ] 128 | ] 129 | } 130 | }, 131 | "quests": { 132 | "First Steps": { 133 | "questid": "my-first-quest", 134 | "region": { 135 | "linear": "3", 136 | "open": "open3" 137 | }, 138 | "reward": [ 139 | [ "item", "Edge o' All", 1 ], 140 | [ "item", "Grasswalkers", 1 ] 141 | ] 142 | }, 143 | "A Few More Steps": { 144 | "questid": "my-first-quest_2", 145 | "region": { 146 | "linear": "3", 147 | "open": "open3" 148 | }, 149 | "condition": [ 150 | [ "quest", "First Steps" ] 151 | ], 152 | "reward": [ 153 | [ "item", "Second Hide", 1 ], 154 | [ "item", "Explorer's Cap", 1 ] 155 | ] 156 | }, 157 | "Autumn's Rise Trailblazing": { 158 | "questid": "trailblaze-autumn", 159 | "region": { 160 | "linear": "3", 161 | "open": "open3" 162 | }, 163 | "condition": [ 164 | [ "quest", "Autumn's Rise Collect" ], 165 | [ "quest", "Autumn's Rise Defeat" ], 166 | [ "quest", "Autumn's Rise Landmarks" ], 167 | [ "quest", "Autumn's Rise Data Probe" ] 168 | ] 169 | }, 170 | "Autumn's Rise Collect": { 171 | "questid": "trailblaze-autumn-1", 172 | "region": { 173 | "linear": "3", 174 | "open": "open3" 175 | } 176 | }, 177 | "Autumn's Rise Defeat": { 178 | "questid": "trailblaze-autumn-2", 179 | "region": { 180 | "linear": "3", 181 | "open": "open3" 182 | } 183 | }, 184 | "Autumn's Rise Landmarks": { 185 | "questid": "trailblaze-autumn-3", 186 | "region": { 187 | "linear": "3", 188 | "open": "open3" 189 | } 190 | }, 191 | "Autumn's Rise Data Probe": { 192 | "questid": "trailblaze-autumn-4", 193 | "region": { 194 | "linear": "3", 195 | "open": "open3" 196 | } 197 | }, 198 | "Round and Round": { 199 | "questid": "round-and-round", 200 | "region": { 201 | "linear": "3", 202 | "open": "open3" 203 | }, 204 | "reward": [ 205 | [ "item", "Sandwich", 1 ], 206 | [ "item", "Hi-Sandwich", 1 ], 207 | [ "item", "Chef Sandwich", 1 ] 208 | ] 209 | }, 210 | "Crocus Pocus": { 211 | "questid": "botanics", 212 | "region": { 213 | "linear": "31", 214 | "open": "open3" 215 | }, 216 | "condition": [ 217 | [ "item", "Disc of Flora", 1 ], 218 | [ "item", "Heat" ], 219 | [ "cutscene", "Talatu Bergen 25%" ], 220 | [ "cutscene", "Talatu Ba'kii 50%" ], 221 | [ "cutscene", "Talatu Basin 75%" ], 222 | [ "region", "open", "open4.1" ], 223 | [ "region", "open", "open5" ], 224 | [ "region", "open", "open9" ], 225 | [ "region", "open", "open10" ], 226 | [ "region", "open", "open16" ], 227 | [ "region", "open", "open20" ] 228 | ], 229 | "reward": [ 230 | [ "item", "The Last Strawhat", 1 ] 231 | ], 232 | "_comment": "1 cp for each element" 233 | }, 234 | "The Observatory": { 235 | "questid": "observatory", 236 | "region": { 237 | "linear": "31", 238 | "open": "open18" 239 | }, 240 | "condition": [ 241 | [ "quest", "A Promise is a Promise 5" ] 242 | ], 243 | "_comment": "1 cp for each element" 244 | }, 245 | "Bergen Trailblazing": { 246 | "questid": "trailblaze-bergen", 247 | "region": { 248 | "linear": "3", 249 | "open": "open3" 250 | }, 251 | "condition": [ 252 | [ "quest", "Bergen Trailblazing Collect" ], 253 | [ "quest", "Bergen Trailblazing Defeat" ], 254 | [ "quest", "Bergen Trailblazing Landmarks" ], 255 | [ "quest", "Bergen Trailblazing Data Probe" ] 256 | ] 257 | }, 258 | "Bergen Trailblazing Collect": { 259 | "questid": "trailblaze-bergen-1", 260 | "region": { 261 | "linear": "3", 262 | "open": "open3" 263 | } 264 | }, 265 | "Bergen Trailblazing Defeat": { 266 | "questid": "trailblaze-bergen-2", 267 | "region": { 268 | "linear": "3", 269 | "open": "open3" 270 | } 271 | }, 272 | "Bergen Trailblazing Landmarks": { 273 | "questid": "trailblaze-bergen-3", 274 | "region": { 275 | "linear": "3", 276 | "open": "open3" 277 | } 278 | }, 279 | "Bergen Trailblazing Data Probe": { 280 | "questid": "trailblaze-bergen-4", 281 | "amount": 0, 282 | "region": { 283 | "linear": "3", 284 | "open": "open3" 285 | }, 286 | "condition": [ 287 | [ "item", "Heat" ] 288 | ] 289 | }, 290 | "You've Got Mail": { 291 | "questid": "you-got-mail", 292 | "region": { 293 | "linear": "3", 294 | "open": "open3" 295 | }, 296 | "condition": [ 297 | [ 298 | "or", 299 | [ "item", "Bronze Mail" ], 300 | [ "shop_slot", "rookieHarborWeapons", 30 ] 301 | ] 302 | ], 303 | "reward": [ 304 | [ "item", "Pike Wood", 7 ], 305 | [ "item", "Frozen Tear", 2 ] 306 | ] 307 | }, 308 | "The Legendary Bunny": { 309 | "questid": "legend-bunny", 310 | "region": { 311 | "linear": "3", 312 | "open": "open3" 313 | } 314 | }, 315 | "Round and Round 2": { 316 | "questid": "round-and-round-2", 317 | "region": { 318 | "linear": "3", 319 | "open": "open3" 320 | }, 321 | "condition": [ 322 | [ "quest", "Round and Round" ] 323 | ], 324 | "reward": [ 325 | [ "item", "Sandwich", 1 ], 326 | [ "item", "Hi-Sandwich", 1 ], 327 | [ "item", "Chef Sandwich", 1 ] 328 | ] 329 | }, 330 | "Digging Up the Past": { 331 | "questid": "bergen-mining_tools", 332 | "region": { 333 | "linear": "10", 334 | "open": "open4.7" 335 | }, 336 | "condition": [ 337 | [ "item", "Heat", 1 ] 338 | ], 339 | "_comment": "Gives 1 CP Neutral" 340 | }, 341 | "Filthy Frobbits": { 342 | "questid": "miners-1-dirtbunnies", 343 | "region": { 344 | "linear": "3", 345 | "open": "open3" 346 | }, 347 | "reward": [ 348 | [ "item", "Metal Gears", 2 ], 349 | [ "item", "Purple Ore Lump", 4 ] 350 | ] 351 | }, 352 | "Explosive Debugging": { 353 | "questid": "miners-2-mine_bombing_1", 354 | "region": { 355 | "linear": "3", 356 | "open": "open3" 357 | }, 358 | "condition": [ 359 | [ "quest", "Filthy Frobbits" ], 360 | [ "item", "Blue Ice Shade" ], 361 | [ "item", "Heat", 1 ] 362 | ], 363 | "reward": [ 364 | [ "item", "Metal Gears", 3 ], 365 | [ "item", "Blue Orb", 2 ] 366 | ] 367 | }, 368 | "Preemptive Debugging": { 369 | "questid": "miners-2-mine_challenge-1", 370 | "region": { 371 | "linear": "3", 372 | "open": "open3" 373 | }, 374 | "condition": [ 375 | [ "quest", "Explosive Debugging" ] 376 | ], 377 | "reward": [ 378 | [ "item", "Miner's Helmet", 1 ], 379 | [ "item", "Refined Metal", 3 ] 380 | ] 381 | }, 382 | "Kidding Around": { 383 | "questid": "bg-1-lost_pet", 384 | "region": { 385 | "linear": "3", 386 | "open": "open3" 387 | }, 388 | "reward": [ 389 | [ "item", "Frosted Carrot", 2 ], 390 | [ "item", "Winter Thorn", 4 ] 391 | ] 392 | }, 393 | "The Goatfather": { 394 | "questid": "bg-2-goat_cave_1", 395 | "region": { 396 | "linear": "3", 397 | "open": "open3" 398 | }, 399 | "condition": [ 400 | [ "quest", "Kidding Around" ], 401 | [ "item", "Heat" ], 402 | [ "item", "Blue Ice Shade", 1 ] 403 | ], 404 | "reward": [ 405 | [ "item", "Perforated Tophat", 2 ], 406 | [ "item", "Raw Meat", 5 ] 407 | ] 408 | }, 409 | "Fancy Tophat": { 410 | "questid": "bergen-fancy-tophat", 411 | "region": { 412 | "linear": "3", 413 | "open": "open3" 414 | }, 415 | "reward": [ 416 | [ "item", "Solid Bubble", 4 ], 417 | [ "item", "Diabolic Horns", 2 ] 418 | ] 419 | }, 420 | "Heating the Hermit": { 421 | "questid": "bergtraders-2-hermit_materials", 422 | "region": { 423 | "linear": "3.1", 424 | "open": "open4.1" 425 | }, 426 | "condition": [ 427 | [ "quest", "Fancy Tophat" ], 428 | [ "item", "Blue Ice Shade", 1 ], 429 | [ "item", "Heat", 1 ] 430 | ], 431 | "reward": [ 432 | [ "item", "Rusty Bits", 6 ], 433 | [ "item", "Winter Thorn", 3 ], 434 | [ "item", "Sweet Berry Tea", 5 ] 435 | ] 436 | }, 437 | "Trial of Aspiration": { 438 | "questid": "monks-1-jump_challenge", 439 | "region": { 440 | "linear": "3", 441 | "open": "open3" 442 | }, 443 | "reward": [ 444 | [ "item", "Rock Beak", 4 ], 445 | [ "item", "Pepper Night Tea", 3 ] 446 | ] 447 | }, 448 | "Trial of Progression": { 449 | "questid": "monks-2-jump_challenge_2", 450 | "region": { 451 | "linear": "3", 452 | "open": "open3" 453 | }, 454 | "condition": [ 455 | [ "quest", "Trial of Aspiration" ], 456 | [ "item", "Blue Ice Shade", 1 ], 457 | [ "item", "Heat", 1 ] 458 | ], 459 | "reward": [ 460 | [ "item", "Frozen Tear", 6 ], 461 | [ "item", "Crystal Water", 3 ], 462 | [ "item", "Hotsauce", 2 ] 463 | ] 464 | }, 465 | "Challenge of Aspiration": { 466 | "questid": "monks-jump_1_bonus", 467 | "region": { 468 | "linear": "3", 469 | "open": "open3" 470 | }, 471 | "condition": [ 472 | [ "quest", "Trial of Aspiration" ] 473 | ], 474 | "reward": [ 475 | [ "item", "Wooly Socks", 1 ] 476 | ] 477 | }, 478 | "Aspiration Timed Challenge": { 479 | "questid": "monks-jump_1_time", 480 | "region": { 481 | "linear": "3", 482 | "open": "open3" 483 | }, 484 | "condition": [ 485 | [ "quest", "Challenge of Aspiration" ] 486 | ] 487 | }, 488 | "Aspiration No Shield Challenge": { 489 | "questid": "monks-jump_1_noShield", 490 | "region": { 491 | "linear": "3", 492 | "open": "open3" 493 | }, 494 | "condition": [ 495 | [ "quest", "Challenge of Aspiration" ] 496 | ] 497 | }, 498 | "Challenge of Progression": { 499 | "questid": "monks-jump_2_bonus", 500 | "region": { 501 | "linear": "3", 502 | "open": "open3" 503 | }, 504 | "condition": [ 505 | [ "quest", "Trial of Progression" ] 506 | ], 507 | "reward": [ 508 | [ "item", "Weird Skates", 1 ] 509 | ] 510 | }, 511 | "Progression Timed Challenge": { 512 | "questid": "monks-jump_2_time", 513 | "region": { 514 | "linear": "3", 515 | "open": "open3" 516 | }, 517 | "condition": [ 518 | [ "quest", "Challenge of Progression" ] 519 | ] 520 | }, 521 | "Progression Frosty Challenge": { 522 | "questid": "monks-jump_2_frosty", 523 | "region": { 524 | "linear": "3", 525 | "open": "open3" 526 | }, 527 | "condition": [ 528 | [ "quest", "Challenge of Progression" ] 529 | ] 530 | }, 531 | "Building a Base": { 532 | "questid": "omni-1", 533 | "region": { 534 | "linear": "4", 535 | "open": "open4.1" 536 | }, 537 | "condition": [ 538 | [ "item", "Heat", 1 ] 539 | ] 540 | }, 541 | "Pushing Bases": { 542 | "questid": "omni-2", 543 | "region": { 544 | "linear": "4", 545 | "open": "open4.1" 546 | }, 547 | "condition": [ 548 | [ "quest", "Building a Base" ], 549 | [ "quest", "The Legendary Bunny" ] 550 | ], 551 | "reward": [ 552 | [ "item", "Disciple Gloves", 1 ] 553 | ] 554 | }, 555 | "A Promise is a Promise 2": { 556 | "questid": "unkown-warrior-2", 557 | "region": { 558 | "linear": "3", 559 | "open": "open3" 560 | }, 561 | "condition": [ 562 | [ "item", "Broken Shield", 1 ] 563 | ] 564 | }, 565 | "Henry Trailblazing": { 566 | "questid": "trailblaze-forest", 567 | "region": { 568 | "linear": "31", 569 | "open": "open9" 570 | }, 571 | "condition": [ 572 | [ "quest", "Autumn's Rise Trailblazing" ], 573 | [ "quest", "Bergen Trailblazing" ], 574 | [ "quest", "Maroon Valley Trailblazing" ], 575 | [ "quest", "Gaia's Garden Trailblazing" ], 576 | [ "item", "Shock" ] 577 | ], 578 | "reward": [ 579 | [ "item", "Circuit Override", 1 ] 580 | ] 581 | }, 582 | "Last Minute Help Needed": { 583 | "questid": "wervyn-quest", 584 | "region": { 585 | "linear": "31", 586 | "open": "open16" 587 | }, 588 | "condition": [ 589 | [ "item", "Meteor Shade", 1 ], 590 | [ "item", "Guild Pass", 1 ] 591 | ] 592 | }, 593 | "An Unfortunate Series of Features": { 594 | "questid": "dekay-quest", 595 | "region": { 596 | "linear": "31", 597 | "open": "open16" 598 | }, 599 | "condition": [ 600 | [ "item", "Heat", 1 ], 601 | [ "item", "Cold", 1 ], 602 | [ "item", "Wave", 1 ], 603 | [ "item", "Shock", 1 ] 604 | ], 605 | "reward": [ 606 | [ "item", "Lunatic Paws", 1 ] 607 | ] 608 | }, 609 | "Maroon Valley Trailblazing": { 610 | "questid": "trailblaze-maroon", 611 | "region": { 612 | "linear": "11", 613 | "open": "open5" 614 | }, 615 | "condition": [ 616 | [ "item", "Meteor Shade" ], 617 | [ "quest", "Maroon Valley Collect" ], 618 | [ "quest", "Maroon Valley Defeat" ], 619 | [ "quest", "Maroon Valley Landmarks" ], 620 | [ "quest", "Maroon Valley Data Probe" ] 621 | ] 622 | }, 623 | "Maroon Valley Collect": { 624 | "questid": "trailblaze-maroon-1", 625 | "region": { 626 | "linear": "11", 627 | "open": "open5" 628 | } 629 | }, 630 | "Maroon Valley Defeat": { 631 | "questid": "trailblaze-maroon-2", 632 | "region": { 633 | "linear": "11", 634 | "open": "open5" 635 | }, 636 | "condition": [ 637 | [ "item", "Heat"] 638 | ] 639 | }, 640 | "Maroon Valley Landmarks": { 641 | "questid": "trailblaze-maroon-3", 642 | "region": { 643 | "linear": "11", 644 | "open": "open5" 645 | } 646 | }, 647 | "Maroon Valley Data Probe": { 648 | "questid": "trailblaze-maroon-4", 649 | "region": { 650 | "linear": "11", 651 | "open": "open5" 652 | } 653 | }, 654 | "Maroon Tree Defender": { 655 | "questid": "maroon-tree-defend", 656 | "region": { 657 | "linear": "12", 658 | "open": "open6" 659 | }, 660 | "condition": [ 661 | [ "item", "Maroon Cave Pass", 1 ], 662 | [ "item", "Heat", 1 ] 663 | ], 664 | "reward": [ 665 | [ "item", "Shiny Orb", 1 ] 666 | ] 667 | }, 668 | "Blasting Shells": { 669 | "questid": "nomadAdv-1-turtle_bombing_1", 670 | "region": { 671 | "linear": "11", 672 | "open": "open5" 673 | }, 674 | "reward": [ 675 | [ "item", "Pepper Night Tea", 2 ], 676 | [ "item", "Old Bones", 10 ] 677 | ] 678 | }, 679 | "Hot Trail": { 680 | "questid": "nomadAdv-2-tomb_raiding_2", 681 | "region": { 682 | "linear": "11", 683 | "open": "open5" 684 | }, 685 | "condition": [ 686 | [ "quest", "Blasting Shells" ], 687 | [ "item", "Red Flame Shade", 1 ], 688 | [ "item", "Cold" ], 689 | [ "item", "Heat", 1 ] 690 | ], 691 | "reward": [ 692 | [ "item", "Laser Eyes", 2 ] 693 | ] 694 | }, 695 | "Waves of Sand": { 696 | "questid": "nomadAdv-3-sandman_tomb_1", 697 | "region": { 698 | "linear": "11", 699 | "open": "open5" 700 | }, 701 | "condition": [ 702 | [ "quest", "Hot Trail" ], 703 | [ "item", "Meteor Shade", 1 ] 704 | ], 705 | "reward": [ 706 | [ "item", "Pepper Night Tea", 5 ], 707 | [ "item", "Topaz", 4 ] 708 | ] 709 | }, 710 | "Shady Thief": { 711 | "questid": "bakiGuard-1-basar_thief_1", 712 | "region": { 713 | "linear": "11", 714 | "open": "open5" 715 | }, 716 | "reward": [ 717 | [ "item", "Glaring Rock", 6 ], 718 | [ "item", "Maroon Chestnut", 2 ] 719 | ] 720 | }, 721 | "Just Deserts": { 722 | "questid": "bakiGuard-2-evil_guard", 723 | "region": { 724 | "linear": "11", 725 | "open": "open5" 726 | }, 727 | "condition": [ 728 | [ "quest", "Shady Thief" ], 729 | [ "item", "Red Flame Shade", 1 ], 730 | [ "item", "Heat", 1 ], 731 | [ "item", "Cold", 1 ] 732 | ], 733 | "reward": [ 734 | [ "item", "Lucid Shard", 6 ], 735 | [ "item", "Jelly Extract", 5 ] 736 | ] 737 | }, 738 | "Crate Hate": { 739 | "questid": "bakiTrader-1-crate_defense_1", 740 | "region": { 741 | "linear": "11", 742 | "open": "open5" 743 | }, 744 | "reward": [ 745 | [ "item", "Cactone Fruit", 11 ], 746 | [ "item", "Ancient Earth", 3 ] 747 | ] 748 | }, 749 | "Steamy Booze": { 750 | "questid": "bakiTrader-2-brewing_fun_1", 751 | "region": { 752 | "linear": "20", 753 | "open": "open5" 754 | }, 755 | "condition": [ 756 | [ "quest", "Crate Hate" ], 757 | [ "item", "Red Flame Shade", 1 ], 758 | [ "item", "Cold", 1 ], 759 | [ "item", "Heat", 1 ] 760 | ], 761 | "reward": [ 762 | [ "item", "Bear Beer", 5 ] 763 | ] 764 | }, 765 | "Steamy Challenge": { 766 | "questid": "bakiTrader-2-brewing_challenge_1", 767 | "region": { 768 | "linear": "20", 769 | "open": "open5" 770 | }, 771 | "condition": [ 772 | [ "quest", "Steamy Booze" ] 773 | ], 774 | "reward": [ 775 | [ "item", "Brewing Kit: Crab", 2 ] 776 | ] 777 | }, 778 | "A Promise is a Promise 3": { 779 | "questid": "unknown-warrior-3", 780 | "region": { 781 | "linear": "11", 782 | "open": "open5" 783 | }, 784 | "condition": [ 785 | [ "item", "Broken Sword", 1 ], 786 | [ "item", "Cursed Coin", 1 ], 787 | [ "item", "White Key", 1 ], 788 | [ "item", "Heat", 1 ], 789 | [ "item", "Cold", 1 ] 790 | ] 791 | }, 792 | "Turret Defense": { 793 | "questid": "jungleAdv-1-turret_defense_1", 794 | "region": { 795 | "linear": "23", 796 | "open": "open10" 797 | }, 798 | "reward": [ 799 | [ "item", "Virus Root", 4 ], 800 | [ "item", "Stout Backplate", 2 ] 801 | ] 802 | }, 803 | "An Original Idea": { 804 | "questid": "jungleAdv-2-turret_defense_2", 805 | "region": { 806 | "linear": "23", 807 | "open": "open10" 808 | }, 809 | "condition": [ 810 | [ "item", "Purple Bolt Shade", 1 ], 811 | [ "item", "Azure Drop Shade", 1 ], 812 | [ "item", "Shock", 1 ], 813 | [ "item", "Wave", 1 ], 814 | [ "quest", "Turret Defense" ] 815 | ], 816 | "reward": [ 817 | [ "item", "Living Bolt", 2 ], 818 | [ "item", "Sea Disc", 2 ] 819 | ] 820 | }, 821 | "Hostage Situation": { 822 | "questid": "jungleGuard-1-hostage_1", 823 | "region": { 824 | "linear": "23", 825 | "open": "open10" 826 | }, 827 | "reward": [ 828 | [ "item", "Chili Dog", 4 ], 829 | [ "item", "Star Fruit", 5 ] 830 | ] 831 | }, 832 | "High Crating": { 833 | "questid": "jungleGuard-2-crate_rooftop_1", 834 | "region": { 835 | "linear": "23", 836 | "open": "open10" 837 | }, 838 | "condition": [ 839 | [ "item", "Purple Bolt Shade", 1 ], 840 | [ "item", "Azure Drop Shade", 1 ], 841 | [ "item", "Shock", 1 ], 842 | [ "item", "Wave", 1 ], 843 | [ "quest", "Hostage Situation" ] 844 | ], 845 | "reward": [ 846 | [ "item", "Plain Cube", 3 ], 847 | [ "item", "Metal Down", 4 ] 848 | ] 849 | }, 850 | "Tropical Chill": { 851 | "questid": "jungleTrader-1-snowman_thief_1", 852 | "region": { 853 | "linear": "23", 854 | "open": "open10" 855 | }, 856 | "condition": [ 857 | [ "item", "Cold" ], 858 | [ "item", "Heat" ] 859 | ], 860 | "reward": [ 861 | [ "item", "Moon Fruit", 5 ], 862 | [ "item", "Huge Nut", 3 ], 863 | [ "item", "Lazy Claw", 2 ] 864 | ] 865 | }, 866 | "Melting this Cold Fiend": { 867 | "questid": "jungleTrader-2-snowman_thief_2", 868 | "region": { 869 | "linear": "23", 870 | "open": "open10" 871 | }, 872 | "condition": [ 873 | [ "item", "Purple Bolt Shade", 1 ], 874 | [ "item", "Azure Drop Shade", 1 ], 875 | [ "item", "Shock", 1 ], 876 | [ "item", "Wave", 1 ], 877 | [ "quest", "Tropical Chill" ] 878 | ], 879 | "reward": [ 880 | [ "item", "Climate Cowl", 1 ] 881 | ] 882 | }, 883 | "Lakeside Escort": { 884 | "questid": "jungleGreens-1-escort_1", 885 | "region": { 886 | "linear": "23", 887 | "open": "open10" 888 | }, 889 | "reward": [ 890 | [ "item", "Green Risotto", 3 ], 891 | [ "item", "Wrap Roll", 3 ], 892 | [ "item", "Elder Wood", 8 ] 893 | ] 894 | }, 895 | "Rooting for Power": { 896 | "questid": "jungleGreens-2-plantSearch_1", 897 | "region": { 898 | "linear": "23", 899 | "open": "open10" 900 | }, 901 | "condition": [ 902 | [ "item", "Purple Bolt Shade", 1 ], 903 | [ "item", "Azure Drop Shade", 1 ], 904 | [ "item", "Wave" ], 905 | [ "quest", "Lakeside Escort" ] 906 | ], 907 | "reward": [ 908 | [ "item", "Echo Roll", 5 ], 909 | [ "item", "Glow Pear", 7 ], 910 | [ "item", "Power Spinach", 4 ] 911 | ] 912 | }, 913 | "Sprouting Business": { 914 | "questid": "basin-mush-1", 915 | "region": { 916 | "linear": "23", 917 | "open": "open10" 918 | }, 919 | "condition": [ 920 | [ "item", "Wave", 1 ] 921 | ] 922 | }, 923 | "Pumpkin Land Superfun": { 924 | "questid": "halloween", 925 | "region": { 926 | "linear": "23", 927 | "open": "open10" 928 | }, 929 | "reward": [ 930 | [ "item", "Scarecrown", 1 ] 931 | ] 932 | }, 933 | "A Promise is a Promise 4": { 934 | "questid": "unknown-warrior-4", 935 | "region": { 936 | "linear": "23", 937 | "open": "open10" 938 | }, 939 | "condition": [ 940 | [ "item", "Broken Deck", 1 ] 941 | ] 942 | }, 943 | "Gaia's Garden Trailblazing": { 944 | "questid": "trailblaze-jungle", 945 | "region": { 946 | "linear": "23", 947 | "open": "open10" 948 | }, 949 | "condition": [ 950 | [ "quest", "Gaia's Garden Collect" ], 951 | [ "quest", "Gaia's Garden Defeat" ], 952 | [ "quest", "Gaia's Garden Landmarks" ], 953 | [ "quest", "Gaia's Garden Data Probe" ] 954 | ] 955 | }, 956 | "Gaia's Garden Collect": { 957 | "questid": "trailblaze-jungle-1", 958 | "region": { 959 | "linear": "23", 960 | "open": "open10" 961 | } 962 | }, 963 | "Gaia's Garden Defeat": { 964 | "questid": "trailblaze-jungle-2", 965 | "region": { 966 | "linear": "23", 967 | "open": "open10" 968 | } 969 | }, 970 | "Gaia's Garden Landmarks": { 971 | "questid": "trailblaze-jungle-3", 972 | "region": { 973 | "linear": "23", 974 | "open": "open10" 975 | } 976 | }, 977 | "Gaia's Garden Data Probe": { 978 | "questid": "trailblaze-jungle-4", 979 | "region": { 980 | "linear": "23", 981 | "open": "open10" 982 | }, 983 | "condition": [ 984 | [ "item", "Wave", 1 ] 985 | ] 986 | }, 987 | "Turret Defense Challenge 1": { 988 | "questid": "jungleAdv-turret_defense_challenge-1", 989 | "region": { 990 | "linear": "23", 991 | "open": "open10" 992 | }, 993 | "condition": [ 994 | [ "quest", "Turret Defense" ] 995 | ], 996 | "reward": [ 997 | [ "item", "Turret Tokens", 7 ] 998 | ] 999 | }, 1000 | "Turret Defense Challenge 2": { 1001 | "questid": "jungleAdv-turret_defense_challenge-2", 1002 | "region": { 1003 | "linear": "23", 1004 | "open": "open10" 1005 | }, 1006 | "condition": [ 1007 | [ "quest", "An Original Idea" ] 1008 | ], 1009 | "reward": [ 1010 | [ "item", "Turret Tokens", 8 ] 1011 | ] 1012 | }, 1013 | "A Promise is a Promise 5": { 1014 | "questid": "unknown-warrior-5", 1015 | "region": { 1016 | "linear": "33", 1017 | "open": "open20" 1018 | }, 1019 | "condition": [ 1020 | [ "item", "Broken Chakrams" ], 1021 | [ "quest", "A Promise is a Promise" ], 1022 | [ "quest", "A Promise is a Promise 2" ], 1023 | [ "quest", "A Promise is a Promise 3" ], 1024 | [ "quest", "A Promise is a Promise 4" ] 1025 | ] 1026 | }, 1027 | "Railing Rider": { 1028 | "questid": "railing-rider-1", 1029 | "region": { 1030 | "linear": "2", 1031 | "open": "open2" 1032 | }, 1033 | "reward": [ 1034 | [ "item", "Veggie Sticks", 6 ] 1035 | ] 1036 | }, 1037 | "All Hail the Ice Cream": { 1038 | "questid": "ice-cream-hype", 1039 | "region": { 1040 | "linear": "3", 1041 | "open": "open3" 1042 | }, 1043 | "reward": [ 1044 | [ "item", "Bergen Ice Cream", 10 ] 1045 | ] 1046 | }, 1047 | "Training with the Master": { 1048 | "questid": "training-tutorial", 1049 | "region": { 1050 | "linear": "2", 1051 | "open": "open2" 1052 | }, 1053 | "condition": [ 1054 | [ "quest", "Training: VRP" ], 1055 | [ "quest", "Training: VPI" ] 1056 | ], 1057 | "reward": [ 1058 | [ "item", "Adept Gloves", 1 ] 1059 | ] 1060 | }, 1061 | "Training: VRP": { 1062 | "questid": "training-balls", 1063 | "region": { 1064 | "linear": "2", 1065 | "open": "open2" 1066 | }, 1067 | "condition": [ 1068 | [ "item", "Blue Ice Shade" ], 1069 | [ "item", "Red Flame Shade", 1 ] 1070 | ] 1071 | }, 1072 | "Training: VPI": { 1073 | "questid": "training-close", 1074 | "region": { 1075 | "linear": "2", 1076 | "open": "open2" 1077 | }, 1078 | "condition": [ 1079 | [ "item", "Blue Ice Shade" ], 1080 | [ "item", "Red Flame Shade", 1 ] 1081 | 1082 | ] 1083 | }, 1084 | "Faction Introduction": { 1085 | "questid": "faction-tutorial", 1086 | "region": { 1087 | "linear": "2", 1088 | "open": "open2" 1089 | }, 1090 | "reward": 1091 | [ 1092 | [ "item", "Edge o' All", 1 ] 1093 | ] 1094 | }, 1095 | "Digging for Data": { 1096 | "questid": "wa-1-digging", 1097 | "region": { 1098 | "linear": "3", 1099 | "open": "open3" 1100 | }, 1101 | "reward": [ 1102 | [ "item", "Bronze Edge", 1 ] 1103 | ] 1104 | }, 1105 | "It Can Dig But It Can't Hide": { 1106 | "questid": "fd-2-meerkat-cave", 1107 | "region": { 1108 | "linear": "3", 1109 | "open": "open3" 1110 | }, 1111 | "condition": [ 1112 | [ "quest", "Digging for Data" ] 1113 | ], 1114 | "reward": [ 1115 | [ "item", "Swiftspike", 1 ], 1116 | [ "item", "Tofu Cracker", 1 ] 1117 | ] 1118 | }, 1119 | "Points of Power": { 1120 | "questid": "fd-3-ball_egg_1", 1121 | "region": { 1122 | "linear": "20", 1123 | "open": "open9" 1124 | }, 1125 | "condition": [ 1126 | [ "item", "Blue Ice Shade", 1 ], 1127 | [ "item", "Red Flame Shade", 1 ], 1128 | [ "item", "Heat", 1 ], 1129 | [ "item", "Cold", 1 ], 1130 | [ "quest", "It Can Dig But It Can't Hide" ] 1131 | ], 1132 | "reward": [ 1133 | [ "item", "Twilight Dew", 9 ], 1134 | [ "item", "Crimson Dragonfly", 4 ] 1135 | ] 1136 | }, 1137 | "Petty Crime Hunter": { 1138 | "questid": "rhg-1-pickpocket", 1139 | "region": { 1140 | "linear": "3", 1141 | "open": "open3" 1142 | }, 1143 | "reward": [ 1144 | [ "item", "Hi-Sandwich", 3 ], 1145 | [ "item", "Just Water", 3 ] 1146 | ] 1147 | }, 1148 | "Smuggle Trouble": { 1149 | "questid": "rhg-2-crate_smuggle", 1150 | "region": { 1151 | "linear": "3", 1152 | "open": "open3" 1153 | }, 1154 | "condition": [ 1155 | [ "quest", "Petty Crime Hunter" ] 1156 | ], 1157 | "reward": [ 1158 | [ "item", "Battered Fist", 1 ], 1159 | [ "item", "Sweet Berry Tea", 3 ] 1160 | ] 1161 | }, 1162 | "Wet Work": { 1163 | "questid": "rhg-3-maritime_smuggle", 1164 | "region": { 1165 | "linear": "3", 1166 | "open": "open3" 1167 | }, 1168 | "condition": [ 1169 | [ "item", "Blue Ice Shade", 1 ], 1170 | [ "item", "Heat", 1 ], 1171 | [ "item", "Cold", 1 ], 1172 | [ "quest", "Smuggle Trouble" ] 1173 | ], 1174 | "reward": [ 1175 | [ "item", "Hunter's Bolt", 1 ], 1176 | [ "item", "Shrimp Risotto", 4 ] 1177 | ] 1178 | }, 1179 | "Small Time Delivery": { 1180 | "questid": "rftc-1-collect_trade", 1181 | "region": { 1182 | "linear": "3", 1183 | "open": "open3" 1184 | }, 1185 | "reward": [ 1186 | [ "item", "Gold Beetle", 5 ], 1187 | [ "item", "Veggie Wraps", 3 ] 1188 | ] 1189 | }, 1190 | "Bull Grab": { 1191 | "questid": "bull-arena-1", 1192 | "region": { 1193 | "linear": "3", 1194 | "open": "open3" 1195 | }, 1196 | "condition": [ 1197 | [ "quest", "Small Time Delivery" ] 1198 | ], 1199 | "reward": [ 1200 | [ "item", "Flaming Bun", 3 ], 1201 | [ "item", "Twilight Dew", 6 ], 1202 | [ "item", "Bear Cicada", 4 ] 1203 | ] 1204 | }, 1205 | "New Metal": { 1206 | "questid": "rftc-3-metalstuff", 1207 | "region": { 1208 | "linear": "20", 1209 | "open": "open9" 1210 | }, 1211 | "condition": [ 1212 | [ "item", "Red Flame Shade", 1 ], 1213 | [ "item", "Blue Ice Shade", 1 ], 1214 | [ "item", "Heat", 1 ], 1215 | [ "item", "Cold", 1 ], 1216 | [ "quest", "Bull Grab" ] 1217 | ], 1218 | "reward": [ 1219 | [ "item", "Refined Metal", 5 ], 1220 | [ "item", "Wolf Cicada", 2 ] 1221 | ] 1222 | }, 1223 | "Sickly Trees": { 1224 | "questid": "foa-1-treedoc", 1225 | "region": { 1226 | "linear": "3", 1227 | "open": "open3" 1228 | }, 1229 | "reward": [ 1230 | [ "item", "Season Apples", 4 ], 1231 | [ "item", "Fruit Salad", 3 ] 1232 | ] 1233 | }, 1234 | "Intensive Tree Care": { 1235 | "questid": "foa-2-tree_defense", 1236 | "region": { 1237 | "linear": "3", 1238 | "open": "open3" 1239 | }, 1240 | "condition": [ 1241 | [ "quest", "Sickly Trees" ] 1242 | ], 1243 | "reward": [ 1244 | [ "item", "Autumn Leaves", 13 ], 1245 | [ "item", "Azure Dragonfly", 4 ], 1246 | [ "item", "Chef Sandwich", 2 ] 1247 | ] 1248 | }, 1249 | "Bull on Fire": { 1250 | "questid": "foa-3-buffel_cave", 1251 | "region": { 1252 | "linear": "20", 1253 | "open": "open9" 1254 | }, 1255 | "condition": [ 1256 | [ "quest", "Intensive Tree Care" ], 1257 | [ "item", "Heat" ], 1258 | [ "item", "Cold" ] 1259 | ], 1260 | "reward": [ 1261 | [ "item", "Ginger Tom. Salad", 4 ], 1262 | [ "item", "Parched Leaves", 14 ], 1263 | [ "item", "Ripe Apples", 8 ] 1264 | ] 1265 | }, 1266 | "A Promise is a Promise": { 1267 | "questid": "unknown-warrior-1", 1268 | "region": { 1269 | "linear": "3", 1270 | "open": "open3" 1271 | }, 1272 | "condition": [ 1273 | [ "item", "Broken Gauntlet", 1 ] 1274 | ] 1275 | }, 1276 | "Wood 'n' Steaks": { 1277 | "questid": "steaks_1", 1278 | "region": { 1279 | "linear": "3", 1280 | "open": "open3" 1281 | }, 1282 | "reward": [ 1283 | [ "item", "Hi-Sandwich", 5 ], 1284 | [ "item", "Chili Con Carne", 2 ] 1285 | ] 1286 | }, 1287 | "Meating Expectations": { 1288 | "questid": "steaks_2", 1289 | "region": { 1290 | "linear": "11", 1291 | "open": "open5" 1292 | }, 1293 | "condition": [ 1294 | [ "quest", "Wood 'n' Steaks" ] 1295 | ], 1296 | "reward": [ 1297 | [ "item", "Steak, rare", 5 ] 1298 | ] 1299 | }, 1300 | "Raising the Steaks": { 1301 | "questid": "steaks_3", 1302 | "region": { 1303 | "linear": "21", 1304 | "open": "open10" 1305 | }, 1306 | "condition": [ 1307 | [ "quest", "Meating Expectations" ] 1308 | ], 1309 | "reward": [ 1310 | [ "item", "Gourmet Steak", 3 ] 1311 | ] 1312 | } 1313 | } 1314 | } 1315 | -------------------------------------------------------------------------------- /data/in/regions.json: -------------------------------------------------------------------------------- 1 | { 2 | "regions": { 3 | "linear": { 4 | "connections": [ 5 | { 6 | "from": "2", 7 | "to": "3", 8 | "condition": [ 9 | [ "item", "Green Leaf Shade", 1 ] 10 | ] 11 | }, 12 | { 13 | "from": "3", 14 | "to": "4", 15 | "condition": [ 16 | [ "item", "Mine Pass", 1 ], 17 | [ "item", "Guild Pass", 1 ] 18 | ] 19 | }, 20 | { 21 | "from": "3", 22 | "to": "3.1", 23 | "condition": [ 24 | [ "item", "Mine Pass", 1 ] 25 | ] 26 | }, 27 | { 28 | "from": "4", 29 | "to": "5", 30 | "condition": [ 31 | [ "item", "Mine Key", 1 ] 32 | ] 33 | }, 34 | { 35 | "from": "5", 36 | "to": "6", 37 | "condition": [ 38 | [ "item", "Mine Key", 2 ] 39 | ] 40 | }, 41 | { 42 | "from": "6", 43 | "to": "7", 44 | "condition": [ 45 | [ "item", "Mine Key", 3 ] 46 | ] 47 | }, 48 | { 49 | "from": "7", 50 | "to": "8", 51 | "condition": [ 52 | [ "item", "Mine Key", 4 ] 53 | ] 54 | }, 55 | { 56 | "from": "7", 57 | "to": "9", 58 | "condition": [ 59 | [ "item", "Thief's Key", 1 ], 60 | [ "item", "Heat", 1 ] 61 | ] 62 | }, 63 | { 64 | "from": "9", 65 | "to": "10", 66 | "condition": [ 67 | [ "item", "Mine Key", 5 ] 68 | ] 69 | }, 70 | { 71 | "from": "10", 72 | "to": "11", 73 | "condition": [ 74 | [ "item", "Mine Master Key", 1 ], 75 | [ "item", "Blue Ice Shade", 1 ] 76 | ] 77 | }, 78 | { 79 | "from": "11", 80 | "to": "12", 81 | "condition": [ 82 | [ "item", "Maroon Cave Pass", 1 ] 83 | ] 84 | }, 85 | { 86 | "from": "12", 87 | "to": "13", 88 | "condition": [ 89 | [ "item", "Yellow Sand Shade", 1 ], 90 | [ "item", "Heat", 1 ] 91 | ] 92 | }, 93 | { 94 | "from": "13", 95 | "to": "14", 96 | "condition": [ 97 | [ "item", "Faj'ro Key", 1 ] 98 | ] 99 | }, 100 | { 101 | "from": "14", 102 | "to": "15", 103 | "condition": [ 104 | [ "item", "Faj'ro Key", 3 ] 105 | ] 106 | }, 107 | { 108 | "from": "15", 109 | "to": "16", 110 | "condition": [ 111 | [ "item", "Faj'ro Key", 4 ] 112 | ] 113 | }, 114 | { 115 | "from": "16", 116 | "to": "17", 117 | "condition": [ 118 | [ "item", "Cold", 1 ] 119 | ] 120 | }, 121 | { 122 | "from": "17", 123 | "to": "17.5", 124 | "condition": [ 125 | [ "item", "White Key", 1 ] 126 | ] 127 | }, 128 | { 129 | "from": "17.5", 130 | "to": "18", 131 | "condition": [ 132 | [ "item", "Faj'ro Key", 9 ] 133 | ] 134 | }, 135 | { 136 | "from": "17.5", 137 | "to": "19", 138 | "condition": [ 139 | [ "item", "Faj'ro Master Key", 1 ] 140 | ] 141 | }, 142 | { 143 | "from": "19", 144 | "to": "20", 145 | "condition": [ 146 | [ "item", "Red Flame Shade", 1 ] 147 | ] 148 | }, 149 | { 150 | "from": "20", 151 | "to": "21", 152 | "condition": [ 153 | [ "item", "Green Seed Shade", 1 ] 154 | ] 155 | }, 156 | { 157 | "from": "21", 158 | "to": "23", 159 | "condition": [] 160 | }, 161 | { 162 | "from": "21", 163 | "to": "24", 164 | "condition": [ 165 | [ "item", "Pond Slums Pass", 1 ] 166 | ] 167 | }, 168 | { 169 | "from": "23", 170 | "to": "25", 171 | "condition": [ 172 | [ "item", "Zir'vitar Key", 2 ] 173 | ] 174 | }, 175 | { 176 | "from": "23", 177 | "to": "26", 178 | "condition": [ 179 | [ "item", "So'najiz Key", 1 ] 180 | ] 181 | }, 182 | { 183 | "from": "26", 184 | "to": "26.5", 185 | "condition": [ 186 | [ "item", "So'najiz Key", 3 ] 187 | ] 188 | }, 189 | { 190 | "from": "26", 191 | "to": "27", 192 | "condition": [ 193 | [ "item", "Radiant Key", 1 ], 194 | [ "item", "So'najiz Key", 4 ], 195 | [ "item", "Shock" ] 196 | ] 197 | }, 198 | { 199 | "from": "23", 200 | "to": "28", 201 | "condition": [ 202 | [ "item", "Azure Drop Shade", 1 ], 203 | [ "item", "Purple Bolt Shade", 1 ], 204 | [ "item", "Wave", 1 ], 205 | [ "item", "Shock", 1 ] 206 | ] 207 | }, 208 | { 209 | "from": "28", 210 | "to": "29", 211 | "condition": [ 212 | [ "item", "Krys'kajo Key", 2 ] 213 | ] 214 | }, 215 | { 216 | "from": "28", 217 | "to": "30", 218 | "condition": [ 219 | [ "item", "Kajo Master Key", 1 ] 220 | ] 221 | }, 222 | { 223 | "from": "30", 224 | "to": "31", 225 | "condition": [ 226 | [ "item", "Star Shade", 1 ] 227 | ] 228 | }, 229 | { 230 | "from": "31", 231 | "to": "32", 232 | "condition": [ 233 | [ "item", "Old Dojo Key", 1 ] 234 | ] 235 | }, 236 | { 237 | "from": "32", 238 | "to": "33", 239 | "condition": [ 240 | [ "item", "Meteor Shade", 1 ] 241 | ] 242 | }, 243 | { 244 | "from": "31", 245 | "to": "22" 246 | } 247 | ], 248 | "exclude": [ "1" ], 249 | "start": "2", 250 | "goal": "32" 251 | }, 252 | "open": { 253 | "connections": [ 254 | { 255 | "from": "open2", 256 | "to": "open3", 257 | "condition": [ 258 | [ "item", "Green Leaf Shade", 1 ] 259 | ] 260 | }, 261 | { 262 | "from": "open3", 263 | "to": "open4.1", 264 | "condition": [ 265 | [ "item", "Mine Pass", 1 ] 266 | ] 267 | }, 268 | { 269 | "from": "open3", 270 | "to": "open3.1", 271 | "condition": [ 272 | [ "item", "Mine Pass", 1 ] 273 | ] 274 | }, 275 | { 276 | "from": "open4.1", 277 | "to": "open4.2", 278 | "condition": [ 279 | [ "item", "Mine Key", 1 ] 280 | ] 281 | }, 282 | { 283 | "from": "open4.2", 284 | "to": "open4.3", 285 | "condition": [ 286 | [ "item", "Mine Key", 2 ] 287 | ] 288 | }, 289 | { 290 | "from": "open4.3", 291 | "to": "open4.4", 292 | "condition": [ 293 | [ "item", "Mine Key", 3 ] 294 | ] 295 | }, 296 | { 297 | "from": "open4.4", 298 | "to": "open4.5", 299 | "condition": [ 300 | [ "item", "Mine Key", 4 ] 301 | ] 302 | }, 303 | { 304 | "from": "open4.4", 305 | "to": "open4.6", 306 | "condition": [ 307 | [ "item", "Heat", 1 ] 308 | ] 309 | }, 310 | { 311 | "from": "open4.6", 312 | "to": "open4.7", 313 | "condition": [ 314 | [ "item", "Mine Key", 5 ] 315 | ] 316 | }, 317 | { 318 | "from": "open4.1", 319 | "to": "open4.8", 320 | "condition": [ 321 | [ "item", "Mine Master Key", 1 ] 322 | ] 323 | }, 324 | { 325 | "from": "open3", 326 | "to": "open5", 327 | "condition": [ 328 | [ "item", "Blue Ice Shade", 1 ] 329 | ] 330 | }, 331 | { 332 | "from": "open5", 333 | "to": "open6", 334 | "condition": [ 335 | [ "item", "Maroon Cave Pass", 1 ] 336 | ] 337 | }, 338 | { 339 | "from": "open5", 340 | "to": "open7.1", 341 | "condition": [ 342 | [ "item", "Yellow Sand Shade", 1 ], 343 | [ "item", "Heat", 1 ] 344 | ] 345 | }, 346 | { 347 | "from": "open7.1", 348 | "to": "open7.2", 349 | "condition": [ 350 | [ "item", "Faj'ro Key", 1 ] 351 | ] 352 | }, 353 | { 354 | "from": "open7.2", 355 | "to": "open7.3", 356 | "condition": [ 357 | [ "item", "Faj'ro Key", 3 ] 358 | ] 359 | }, 360 | { 361 | "from": "open7.3", 362 | "to": "open7.4", 363 | "condition": [ 364 | [ "item", "Faj'ro Key", 4 ] 365 | ] 366 | }, 367 | { 368 | "from": "open7.4", 369 | "to": "open7.5", 370 | "condition": [ 371 | [ "item", "Cold", 1 ] 372 | ] 373 | }, 374 | { 375 | "from": "open7.5", 376 | "to": "open7.6", 377 | "condition": [ 378 | [ "item", "White Key", 1 ] 379 | ] 380 | }, 381 | { 382 | "from": "open7.6", 383 | "to": "open7.7", 384 | "condition": [ 385 | [ "item", "Faj'ro Key", 9 ] 386 | ] 387 | }, 388 | { 389 | "from": "open7.6", 390 | "to": "open7.8", 391 | "condition": [ 392 | [ "item", "Faj'ro Master Key", 1 ] 393 | ] 394 | }, 395 | { 396 | "from": "open2", 397 | "to": "open8", 398 | "condition": [ 399 | [ "item", "Red Flame Shade", 1 ] 400 | ] 401 | }, 402 | { 403 | "from": "open2", 404 | "to": "open9", 405 | "condition": [ 406 | [ "item", "Red Flame Shade", 1 ] 407 | ] 408 | }, 409 | { 410 | "from": "open2", 411 | "to": "open20", 412 | "condition": [ 413 | [ "item", "Meteor Shade", 1 ] 414 | ] 415 | }, 416 | { 417 | "from": "open9", 418 | "to": "open9.1", 419 | "condition": [ 420 | [ "item", "Green Seed Shade", 1 ] 421 | ] 422 | }, 423 | { 424 | "from": "open9", 425 | "to": "open10", 426 | "condition": [ 427 | [ "item", "Green Seed Shade", 1 ] 428 | ] 429 | }, 430 | { 431 | "from": "open10", 432 | "to": "open11", 433 | "condition": [ 434 | [ "item", "Pond Slums Pass", 1 ] 435 | ] 436 | }, 437 | { 438 | "from": "open10", 439 | "to": "open13.1", 440 | "condition": [ 441 | [ "item", "Zir'vitar Key", 2 ], 442 | [ "any_element" ] 443 | ] 444 | }, 445 | { 446 | "from": "open13.1", 447 | "to": "open13.2", 448 | "condition": [ 449 | [ "item", "Wave", 1 ] 450 | ] 451 | }, 452 | { 453 | "from": "open10", 454 | "to": "open14.1", 455 | "condition": [ 456 | [ "item", "So'najiz Key", 1 ], 457 | [ "any_element" ] 458 | ] 459 | }, 460 | { 461 | "from": "open10", 462 | "to": "open14.2", 463 | "condition": [ 464 | [ "item", "So'najiz Key", 3 ], 465 | [ "item", "Heat" ] 466 | ] 467 | }, 468 | { 469 | "from": "open14.2", 470 | "to": "open14.3", 471 | "condition": [ 472 | [ "item", "Cold" ] 473 | ] 474 | }, 475 | { 476 | "from": "open14.3", 477 | "to": "open14.4", 478 | "condition": [ 479 | [ "item", "So'najiz Key", 4 ], 480 | [ "item", "Radiant Key", 1 ] 481 | ] 482 | }, 483 | { 484 | "from": "open14.4", 485 | "to": "open14.5", 486 | "condition": [ 487 | [ "item", "Shock" ] 488 | ] 489 | }, 490 | { 491 | "from": "open10", 492 | "to": "open15.1", 493 | "condition": [ 494 | [ "item", "Azure Drop Shade", 1 ], 495 | [ "item", "Purple Bolt Shade", 1 ], 496 | [ "item", "Heat", 1 ], 497 | [ "item", "Cold", 1 ], 498 | [ "item", "Wave", 1 ], 499 | [ "item", "Shock", 1 ] 500 | ] 501 | }, 502 | { 503 | "from": "open15.1", 504 | "to": "open15.2", 505 | "condition": [ 506 | [ "item", "Krys'kajo Key", 2 ] 507 | ] 508 | }, 509 | { 510 | "from": "open15.2", 511 | "to": "open15.3", 512 | "condition": [ 513 | [ "item", "Kajo Master Key", 1 ] 514 | ] 515 | }, 516 | { 517 | "from": "open9", 518 | "to": "open16", 519 | "condition": [ 520 | [ "item", "Star Shade", 1 ] 521 | ] 522 | }, 523 | { 524 | "from": "open16", 525 | "to": "open17", 526 | "condition": [ 527 | [ "item", "Old Dojo Key", 1 ] 528 | ] 529 | }, 530 | { 531 | "from": "open16", 532 | "to": "open16.1", 533 | "condition": [ 534 | [ "item", "Meteor Shade", 1 ], 535 | [ "item", "Shock", 1 ] 536 | ] 537 | }, 538 | { 539 | "from": "open16", 540 | "to": "open18", 541 | "condition": [ 542 | ["var", "vwPassage"] 543 | ] 544 | }, 545 | { 546 | "from": "open16.1", 547 | "to": "open19", 548 | "condition": [ 549 | [ "item", "Heat", 1 ], 550 | [ "item", "Cold", 1 ], 551 | [ "item", "Shock", 1 ], 552 | [ "item", "Wave", 1 ], 553 | [ "var", "vtShadeLock" ] 554 | ] 555 | } 556 | ], 557 | "exclude": [ "open1" ], 558 | "start": "open2", 559 | "goal": "open19" 560 | } 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /data/in/shops.json: -------------------------------------------------------------------------------- 1 | { 2 | "shops": { 3 | "Harbor Items": { 4 | "location": { 5 | "shop": "rookieHarborItems", 6 | "area": "rookie-harbor" 7 | }, 8 | "region": { 9 | "linear": "2", 10 | "open": "open2" 11 | }, 12 | "condition": [ 13 | [ "var", "canGrind" ] 14 | ], 15 | "slots": [ 16 | "Sandwich", 17 | "Hi-Sandwich", 18 | "Green Leaf Tea", 19 | "Just Water", 20 | "Spicy Bun", 21 | "Fruit Drink", 22 | "Rice Cracker", 23 | "Veggie Sticks" 24 | ] 25 | }, 26 | "Harbor Items + Blue Ice": { 27 | "location": { 28 | "shop": "rookieHarborItems", 29 | "area": "rookie-harbor" 30 | }, 31 | "region": { 32 | "linear": "2", 33 | "open": "open2" 34 | }, 35 | "condition": [ 36 | [ "var", "canGrind" ], 37 | [ "item", "Blue Ice Shade" ] 38 | ], 39 | "slots": [ 40 | "Kebab Roll", 41 | "Meaty Risotto" 42 | ] 43 | }, 44 | "Harbor Items + Red Flame": { 45 | "location": { 46 | "shop": "rookieHarborItems", 47 | "area": "rookie-harbor" 48 | }, 49 | "region": { 50 | "linear": "2", 51 | "open": "open2" 52 | }, 53 | "condition": [ 54 | [ "var", "canGrind" ], 55 | [ "item", "Red Flame Shade" ] 56 | ], 57 | "slots": [ 58 | "Bergen Ice Cream", 59 | "Sweet Lemonjuice" 60 | ] 61 | }, 62 | "Harbor Items + Green Seed": { 63 | "location": { 64 | "shop": "rookieHarborItems", 65 | "area": "rookie-harbor" 66 | }, 67 | "region": { 68 | "linear": "2", 69 | "open": "open2" 70 | }, 71 | "condition": [ 72 | [ "var", "canGrind" ], 73 | [ "item", "Green Seed Shade" ] 74 | ], 75 | "slots": [ 76 | "Salted Peanuts", 77 | "Cup o' Coffee", 78 | "Snack Mix" 79 | ] 80 | }, 81 | "Harbor Weapons": { 82 | "location": { 83 | "shop": "rookieHarborWeapons", 84 | "area": "rookie-harbor" 85 | }, 86 | "region": { 87 | "linear": "2", 88 | "open": "open2" 89 | }, 90 | "condition": [ 91 | [ "var", "canGrind" ] 92 | ], 93 | "slots": [ 94 | "Bronze Goggles", 95 | "Bronze Edge", 96 | "Bronze Mail", 97 | "Bronze Boots", 98 | "Iron Goggles", 99 | "Iron Edge", 100 | "Iron Mail", 101 | "Iron Boots" 102 | ] 103 | }, 104 | "Harbor Weapons + Red Flame": { 105 | "location": { 106 | "shop": "rookieHarborWeapons", 107 | "area": "rookie-harbor" 108 | }, 109 | "region": { 110 | "linear": "2", 111 | "open": "open2" 112 | }, 113 | "condition": [ 114 | [ "var", "canGrind" ], 115 | [ "item", "Red Flame Shade" ] 116 | ], 117 | "slots": [ 118 | "Silver Goggles", 119 | "Silver Edge", 120 | "Silver Mail", 121 | "Silver Boots" 122 | ] 123 | }, 124 | "Harbor North Weapons": { 125 | "location": { 126 | "shop": "rookieHarborWeaponsNorth", 127 | "area": "rookie-harbor" 128 | }, 129 | "region": { 130 | "linear": "19", 131 | "open": "open8" 132 | }, 133 | "slots": [ 134 | "Steel Goggles", 135 | "Steel Edge", 136 | "Steel Mail", 137 | "Steel Boots", 138 | "Silver Goggles", 139 | "Silver Edge", 140 | "Silver Mail", 141 | "Silver Boots" 142 | ] 143 | }, 144 | "Tara's Shop": { 145 | "location": { 146 | "shop": "rookieHarborTara", 147 | "area": "rookie-harbor" 148 | }, 149 | "region": { 150 | "linear": "2", 151 | "open": "open2" 152 | }, 153 | "condition": [ 154 | [ "var", "canGrind" ] 155 | ], 156 | "slots": [ 157 | "Sandwich" 158 | ] 159 | }, 160 | "Harbor Backer Items": { 161 | "location": { 162 | "shop": "rookieBacker", 163 | "area": "rookie-harbor" 164 | }, 165 | "region": { 166 | "linear": "19", 167 | "open": "open8" 168 | }, 169 | "slots": [ 170 | "Rising Super Star", 171 | "Dk Pepper", 172 | "Cheese Spaetzle", 173 | "Maultasche", 174 | "Durian", 175 | "PengoPop", 176 | "Spicy Beat-0-Type", 177 | "Werewolf Stick", 178 | "Mooncake", 179 | "Guacamole Toast", 180 | "Willis Waldmahl", 181 | "Pumpkin Spice Cof." 182 | ] 183 | }, 184 | "Bergen Items": { 185 | "location": { 186 | "shop": "bergenVillageItems", 187 | "area": "bergen" 188 | }, 189 | "region": { 190 | "linear": "3", 191 | "open": "open3" 192 | }, 193 | "slots": [ 194 | "Sandwich", 195 | "Hi-Sandwich", 196 | "Green Leaf Tea", 197 | "Just Water", 198 | "Spicy Bun", 199 | "Fruit Drink", 200 | "Rice Cracker", 201 | "Veggie Sticks" 202 | ] 203 | }, 204 | "Bergen Items + Blue Ice": { 205 | "location": { 206 | "shop": "bergenVillageItems", 207 | "area": "bergen" 208 | }, 209 | "region": { 210 | "linear": "3", 211 | "open": "open3" 212 | }, 213 | "condition": [ 214 | [ "item", "Blue Ice Shade" ] 215 | ], 216 | "slots": [ 217 | "Kebab Roll", 218 | "Meaty Risotto" 219 | ] 220 | }, 221 | "Bergen Items + Red Flame": { 222 | "location": { 223 | "shop": "bergenVillageItems", 224 | "area": "bergen" 225 | }, 226 | "region": { 227 | "linear": "3", 228 | "open": "open3" 229 | }, 230 | "condition": [ 231 | [ "item", "Red Flame Shade" ] 232 | ], 233 | "slots": [ 234 | "Bergen Ice Cream", 235 | "Sweet Lemonjuice" 236 | ] 237 | }, 238 | "Bergen Items + Green Seed": { 239 | "location": { 240 | "shop": "bergenVillageItems", 241 | "area": "bergen" 242 | }, 243 | "region": { 244 | "linear": "3", 245 | "open": "open3" 246 | }, 247 | "condition": [ 248 | [ "item", "Green Seed Shade" ] 249 | ], 250 | "slots": [ 251 | "Salted Peanuts", 252 | "Cup o' Coffee", 253 | "Snack Mix" 254 | ] 255 | }, 256 | "Bergen Weapons": { 257 | "location": { 258 | "shop": "bergenVillageWeapons", 259 | "area": "bergen" 260 | }, 261 | "region": { 262 | "linear": "3", 263 | "open": "open3" 264 | }, 265 | "slots": [ 266 | "Iron Goggles", 267 | "Iron Edge", 268 | "Iron Mail", 269 | "Iron Boots" 270 | ] 271 | }, 272 | "Hermit's Hut": { 273 | "location": { 274 | "shop": "bergenHermit", 275 | "area": "bergen-trails" 276 | }, 277 | "region": { 278 | "linear": "3", 279 | "open": "open3" 280 | }, 281 | "slots": [ 282 | "Chili Con Carne", 283 | "Sweet Lemonjuice", 284 | "Sweet Berry Tea" 285 | ] 286 | }, 287 | "Ba'kii Items": { 288 | "location": { 289 | "shop": "bakiItems", 290 | "area": "heat-village" 291 | }, 292 | "region": { 293 | "linear": "11", 294 | "open": "open5" 295 | }, 296 | "slots": [ 297 | "Sandwich", 298 | "Hi-Sandwich", 299 | "Green Leaf Tea", 300 | "Just Water", 301 | "Spicy Bun", 302 | "Fruit Drink", 303 | "Rice Cracker", 304 | "Veggie Sticks", 305 | "Bergen Ice Cream", 306 | "Sweet Lemonjuice" 307 | ] 308 | }, 309 | "Ba'kii Items + Blue Ice": { 310 | "location": { 311 | "shop": "bakiItems", 312 | "area": "heat-village" 313 | }, 314 | "region": { 315 | "linear": "11", 316 | "open": "open5" 317 | }, 318 | "condition": [ 319 | [ "item", "Blue Ice Shade" ] 320 | ], 321 | "slots": [ 322 | "Kebab Roll", 323 | "Meaty Risotto" 324 | ] 325 | }, 326 | "Ba'kii Items + Green Seed": { 327 | "location": { 328 | "shop": "bakiItems", 329 | "area": "heat-village" 330 | }, 331 | "region": { 332 | "linear": "11", 333 | "open": "open5" 334 | }, 335 | "condition": [ 336 | [ "item", "Green Seed Shade" ] 337 | ], 338 | "slots": [ 339 | "Salted Peanuts", 340 | "Cup o' Coffee", 341 | "Snack Mix" 342 | ] 343 | }, 344 | "Ba'kii Weapons": { 345 | "location": { 346 | "shop": "bakiWeapons", 347 | "area": "heat-village" 348 | }, 349 | "region": { 350 | "linear": "11", 351 | "open": "open5" 352 | }, 353 | "slots": [ 354 | "Iron Goggles", 355 | "Iron Edge", 356 | "Iron Mail", 357 | "Iron Boots", 358 | "Steel Goggles", 359 | "Steel Edge", 360 | "Steel Mail", 361 | "Steel Boots" 362 | ] 363 | }, 364 | "Basin Vending Machine": { 365 | "location": { 366 | "shop": "basinVendingMachines", 367 | "area": "jungle-city" 368 | }, 369 | "region": { 370 | "linear": "23", 371 | "open": "open10" 372 | }, 373 | "slots": [ 374 | "Sandwich", 375 | "Hi-Sandwich", 376 | "Green Leaf Tea", 377 | "Just Water", 378 | "Spicy Bun", 379 | "Fruit Drink", 380 | "Rice Cracker", 381 | "Veggie Sticks", 382 | "Bergen Ice Cream", 383 | "Sweet Lemonjuice", 384 | "Salted Peanuts", 385 | "Cup o' Coffee", 386 | "Snack Mix", 387 | "Kebab Roll", 388 | "Meaty Risotto" 389 | ] 390 | }, 391 | "Basin Items": { 392 | "location": { 393 | "shop": "basinItems", 394 | "area": "jungle-city" 395 | }, 396 | "region": { 397 | "linear": "23", 398 | "open": "open10" 399 | }, 400 | "slots": [ 401 | "Sandwich", 402 | "Hi-Sandwich", 403 | "Green Leaf Tea", 404 | "Just Water", 405 | "Kebab Roll", 406 | "Meaty Risotto", 407 | "Spicy Bun", 408 | "Fruit Drink", 409 | "Rice Cracker", 410 | "Veggie Sticks", 411 | "Bergen Ice Cream", 412 | "Sweet Lemonjuice", 413 | "Salted Peanuts", 414 | "Cup o' Coffee", 415 | "Snack Mix" 416 | ] 417 | }, 418 | "Basin Weapons": { 419 | "location": { 420 | "shop": "basinWeapons", 421 | "area": "jungle-city" 422 | }, 423 | "region": { 424 | "linear": "23", 425 | "open": "open10" 426 | }, 427 | "slots": [ 428 | "Silver Goggles", 429 | "Silver Edge", 430 | "Silver Mail", 431 | "Silver Boots", 432 | "Titan Goggles", 433 | "Titan Edge", 434 | "Titan Mail", 435 | "Titan Boots" 436 | ] 437 | }, 438 | "Calzone Shop": { 439 | "location": { 440 | "shop": "basinMushroom", 441 | "area": "jungle-city" 442 | }, 443 | "region": { 444 | "linear": "23", 445 | "open": "open10" 446 | }, 447 | "condition": [ 448 | [ "item", "Wave", 1 ] 449 | ], 450 | "slots": [ 451 | "One Up" 452 | ] 453 | }, 454 | "Ridge Weapons": { 455 | "location": { 456 | "shop": "sapphireWeapons", 457 | "area": "forest" 458 | }, 459 | "region": { 460 | "linear": "31", 461 | "open": "open16" 462 | }, 463 | "slots": [ 464 | "Titan Goggles", 465 | "Titan Edge", 466 | "Titan Mail", 467 | "Titan Boots", 468 | "Cobalt Goggles", 469 | "Cobalt Edge", 470 | "Cobalt Mail", 471 | "Cobalt Boots" 472 | ] 473 | }, 474 | "Ridge Items": { 475 | "location": { 476 | "shop": "sapphireItems", 477 | "area": "forest" 478 | }, 479 | "region": { 480 | "linear": "31", 481 | "open": "open16" 482 | }, 483 | "slots": [ 484 | "Sandwich", 485 | "Hi-Sandwich", 486 | "Green Leaf Tea", 487 | "Just Water", 488 | "Kebab Roll", 489 | "Meaty Risotto", 490 | "Spicy Bun", 491 | "Fruit Drink", 492 | "Rice Cracker", 493 | "Veggie Sticks", 494 | "Bergen Ice Cream", 495 | "Sweet Lemonjuice", 496 | "Salted Peanuts", 497 | "Cup o' Coffee", 498 | "Snack Mix" 499 | ] 500 | }, 501 | "Rhombus Weapons": { 502 | "location": { 503 | "shop": "rhombusWeapons1", 504 | "area": "rhombus-sqr" 505 | }, 506 | "region": { 507 | "linear": "33", 508 | "open": "open20" 509 | }, 510 | "condition": [ 511 | [ "var", "canGrind" ] 512 | ], 513 | "slots": [ 514 | "Cobalt Goggles", 515 | "Cobalt Edge", 516 | "Cobalt Mail", 517 | "Cobalt Boots", 518 | "Laser Goggles", 519 | "Laser Edge", 520 | "Laser Mail", 521 | "Laser Boots" 522 | ] 523 | }, 524 | "Rhombus Items": { 525 | "location": { 526 | "shop": "rhombusItems1", 527 | "area": "rhombus-sqr" 528 | }, 529 | "region": { 530 | "linear": "33", 531 | "open": "open20" 532 | }, 533 | "condition": [ 534 | [ "var", "canGrind" ] 535 | ], 536 | "slots": [ 537 | "Sandwich", 538 | "Hi-Sandwich", 539 | "Green Leaf Tea", 540 | "Just Water", 541 | "Kebab Roll", 542 | "Meaty Risotto", 543 | "Spicy Bun", 544 | "Fruit Drink", 545 | "Rice Cracker", 546 | "Veggie Sticks", 547 | "Bergen Ice Cream", 548 | "Sweet Lemonjuice", 549 | "Salted Peanuts", 550 | "Cup o' Coffee", 551 | "Snack Mix" 552 | ] 553 | }, 554 | "Rhombus Curios": { 555 | "location": { 556 | "shop": "rhombusCurios", 557 | "area": "rhombus-sqr" 558 | }, 559 | "region": { 560 | "linear": "33", 561 | "open": "open20" 562 | }, 563 | "condition": [ 564 | [ "var", "canGrind" ] 565 | ], 566 | "slots": [ 567 | "Chest Detector" 568 | ] 569 | }, 570 | "Rhombus Backer Items": { 571 | "location": { 572 | "shop": "rhombusBacker", 573 | "area": "rhombus-sqr" 574 | }, 575 | "region": { 576 | "linear": "33", 577 | "open": "open20" 578 | }, 579 | "slots": [ 580 | "Rising Super Star", 581 | "Dk Pepper", 582 | "Cheese Spaetzle", 583 | "Maultasche", 584 | "Durian", 585 | "PengoPop", 586 | "Spicy Beat-0-Type", 587 | "Werewolf Stick", 588 | "Mooncake", 589 | "Guacamole Toast", 590 | "Willis Waldmahl", 591 | "Pumpkin Spice Cof." 592 | ] 593 | }, 594 | "Vermillion Weapons": { 595 | "location": { 596 | "shop": "aridWeapons", 597 | "area": "arid" 598 | }, 599 | "region": { 600 | "linear": "22", 601 | "open": "open18" 602 | }, 603 | "slots": [ 604 | "Steel Goggles", 605 | "Steel Edge", 606 | "Steel Mail", 607 | "Steel Boots", 608 | "Silver Goggles", 609 | "Silver Edge", 610 | "Silver Mail", 611 | "Silver Boots" 612 | ] 613 | }, 614 | "Vermillion Items": { 615 | "location": { 616 | "shop": "aridItems", 617 | "area": "arid" 618 | }, 619 | "region": { 620 | "linear": "22", 621 | "open": "open18" 622 | }, 623 | "slots": [ 624 | "Sandwich", 625 | "Hi-Sandwich", 626 | "Green Leaf Tea", 627 | "Just Water", 628 | "Spicy Bun", 629 | "Fruit Drink", 630 | "Rice Cracker", 631 | "Veggie Sticks" 632 | ] 633 | } 634 | } 635 | } 636 | -------------------------------------------------------------------------------- /data/in/vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "vars": { 3 | "vtShadeLock": { 4 | "shades": [ 5 | [ "item", "Blue Ice Shade" ], 6 | [ "item", "Red Flame Shade" ], 7 | [ "item", "Purple Bolt Shade" ], 8 | [ "item", "Azure Drop Shade" ] 9 | ], 10 | "bosses": [ 11 | [ "cutscene", "Temple Mine Shade Statue"], 12 | [ "cutscene", "Faj'ro Shade Statue"], 13 | [ "cutscene", "Zir'vitar Shade Statue"], 14 | [ "cutscene", "So'najiz Shade Statue"] 15 | ] 16 | }, 17 | "vwPassage": { 18 | "meteor": [ 19 | [ "item", "Meteor Shade"] 20 | ] 21 | }, 22 | "canGrind": { 23 | "noShadeWarp": [ 24 | [ "or", 25 | [ "item", "Green Leaf Shade" ], 26 | [ "item", "Red Flame Shade" ] 27 | ] 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /data/out/items.json: -------------------------------------------------------------------------------- 1 | { 2 | "Shop Unlock: Harbor Items": 3235924225, 3 | "Slot Unlock: Sandwich (Harbor Items)": 3235924226, 4 | "Global Slot Unlock: Sandwich": 3235924227, 5 | "Slot Unlock: Hi-Sandwich (Harbor Items)": 3235924228, 6 | "Global Slot Unlock: Hi-Sandwich": 3235924229, 7 | "Slot Unlock: Green Leaf Tea (Harbor Items)": 3235924230, 8 | "Global Slot Unlock: Green Leaf Tea": 3235924231, 9 | "Slot Unlock: Just Water (Harbor Items)": 3235924232, 10 | "Global Slot Unlock: Just Water": 3235924233, 11 | "Slot Unlock: Spicy Bun (Harbor Items)": 3235924234, 12 | "Global Slot Unlock: Spicy Bun": 3235924235, 13 | "Slot Unlock: Fruit Drink (Harbor Items)": 3235924236, 14 | "Global Slot Unlock: Fruit Drink": 3235924237, 15 | "Slot Unlock: Rice Cracker (Harbor Items)": 3235924238, 16 | "Global Slot Unlock: Rice Cracker": 3235924239, 17 | "Slot Unlock: Veggie Sticks (Harbor Items)": 3235924240, 18 | "Global Slot Unlock: Veggie Sticks": 3235924241, 19 | "Slot Unlock: Kebab Roll (Harbor Items + Blue Ice)": 3235924242, 20 | "Global Slot Unlock: Kebab Roll": 3235924243, 21 | "Slot Unlock: Meaty Risotto (Harbor Items + Blue Ice)": 3235924244, 22 | "Global Slot Unlock: Meaty Risotto": 3235924245, 23 | "Slot Unlock: Bergen Ice Cream (Harbor Items + Red Flame)": 3235924246, 24 | "Global Slot Unlock: Bergen Ice Cream": 3235924247, 25 | "Slot Unlock: Sweet Lemonjuice (Harbor Items + Red Flame)": 3235924248, 26 | "Global Slot Unlock: Sweet Lemonjuice": 3235924249, 27 | "Slot Unlock: Salted Peanuts (Harbor Items + Green Seed)": 3235924250, 28 | "Global Slot Unlock: Salted Peanuts": 3235924251, 29 | "Slot Unlock: Cup o' Coffee (Harbor Items + Green Seed)": 3235924252, 30 | "Global Slot Unlock: Cup o' Coffee": 3235924253, 31 | "Slot Unlock: Snack Mix (Harbor Items + Green Seed)": 3235924254, 32 | "Global Slot Unlock: Snack Mix": 3235924255, 33 | "Shop Unlock: Harbor Weapons": 3235924256, 34 | "Slot Unlock: Bronze Goggles (Harbor Weapons)": 3235924257, 35 | "Global Slot Unlock: Bronze Goggles": 3235924258, 36 | "Slot Unlock: Bronze Edge (Harbor Weapons)": 3235924259, 37 | "Global Slot Unlock: Bronze Edge": 3235924260, 38 | "Slot Unlock: Bronze Mail (Harbor Weapons)": 3235924261, 39 | "Global Slot Unlock: Bronze Mail": 3235924262, 40 | "Slot Unlock: Bronze Boots (Harbor Weapons)": 3235924263, 41 | "Global Slot Unlock: Bronze Boots": 3235924264, 42 | "Slot Unlock: Iron Goggles (Harbor Weapons)": 3235924265, 43 | "Global Slot Unlock: Iron Goggles": 3235924266, 44 | "Slot Unlock: Iron Edge (Harbor Weapons)": 3235924267, 45 | "Global Slot Unlock: Iron Edge": 3235924268, 46 | "Slot Unlock: Iron Mail (Harbor Weapons)": 3235924269, 47 | "Global Slot Unlock: Iron Mail": 3235924270, 48 | "Slot Unlock: Iron Boots (Harbor Weapons)": 3235924271, 49 | "Global Slot Unlock: Iron Boots": 3235924272, 50 | "Slot Unlock: Silver Goggles (Harbor Weapons + Red Flame)": 3235924273, 51 | "Global Slot Unlock: Silver Goggles": 3235924274, 52 | "Slot Unlock: Silver Edge (Harbor Weapons + Red Flame)": 3235924275, 53 | "Global Slot Unlock: Silver Edge": 3235924276, 54 | "Slot Unlock: Silver Mail (Harbor Weapons + Red Flame)": 3235924277, 55 | "Global Slot Unlock: Silver Mail": 3235924278, 56 | "Slot Unlock: Silver Boots (Harbor Weapons + Red Flame)": 3235924279, 57 | "Global Slot Unlock: Silver Boots": 3235924280, 58 | "Shop Unlock: Harbor North Weapons": 3235924281, 59 | "Slot Unlock: Steel Goggles (Harbor North Weapons)": 3235924282, 60 | "Global Slot Unlock: Steel Goggles": 3235924283, 61 | "Slot Unlock: Steel Edge (Harbor North Weapons)": 3235924284, 62 | "Global Slot Unlock: Steel Edge": 3235924285, 63 | "Slot Unlock: Steel Mail (Harbor North Weapons)": 3235924286, 64 | "Global Slot Unlock: Steel Mail": 3235924287, 65 | "Slot Unlock: Steel Boots (Harbor North Weapons)": 3235924288, 66 | "Global Slot Unlock: Steel Boots": 3235924289, 67 | "Slot Unlock: Silver Goggles (Harbor North Weapons)": 3235924290, 68 | "Slot Unlock: Silver Edge (Harbor North Weapons)": 3235924291, 69 | "Slot Unlock: Silver Mail (Harbor North Weapons)": 3235924292, 70 | "Slot Unlock: Silver Boots (Harbor North Weapons)": 3235924293, 71 | "Shop Unlock: Tara's Shop": 3235924294, 72 | "Slot Unlock: Sandwich (Tara's Shop)": 3235924295, 73 | "Shop Unlock: Harbor Backer Items": 3235924296, 74 | "Slot Unlock: Rising Super Star (Harbor Backer Items)": 3235924297, 75 | "Global Slot Unlock: Rising Super Star": 3235924298, 76 | "Slot Unlock: Dk Pepper (Harbor Backer Items)": 3235924299, 77 | "Global Slot Unlock: Dk Pepper": 3235924300, 78 | "Slot Unlock: Cheese Spaetzle (Harbor Backer Items)": 3235924301, 79 | "Global Slot Unlock: Cheese Spaetzle": 3235924302, 80 | "Slot Unlock: Maultasche (Harbor Backer Items)": 3235924303, 81 | "Global Slot Unlock: Maultasche": 3235924304, 82 | "Slot Unlock: Durian (Harbor Backer Items)": 3235924305, 83 | "Global Slot Unlock: Durian": 3235924306, 84 | "Slot Unlock: PengoPop (Harbor Backer Items)": 3235924307, 85 | "Global Slot Unlock: PengoPop": 3235924308, 86 | "Slot Unlock: Spicy Beat-0-Type (Harbor Backer Items)": 3235924309, 87 | "Global Slot Unlock: Spicy Beat-0-Type": 3235924310, 88 | "Slot Unlock: Werewolf Stick (Harbor Backer Items)": 3235924311, 89 | "Global Slot Unlock: Werewolf Stick": 3235924312, 90 | "Slot Unlock: Mooncake (Harbor Backer Items)": 3235924313, 91 | "Global Slot Unlock: Mooncake": 3235924314, 92 | "Slot Unlock: Guacamole Toast (Harbor Backer Items)": 3235924315, 93 | "Global Slot Unlock: Guacamole Toast": 3235924316, 94 | "Slot Unlock: Willis Waldmahl (Harbor Backer Items)": 3235924317, 95 | "Global Slot Unlock: Willis Waldmahl": 3235924318, 96 | "Slot Unlock: Pumpkin Spice Cof. (Harbor Backer Items)": 3235924319, 97 | "Global Slot Unlock: Pumpkin Spice Cof.": 3235924320, 98 | "Shop Unlock: Bergen Items": 3235924321, 99 | "Slot Unlock: Sandwich (Bergen Items)": 3235924322, 100 | "Slot Unlock: Hi-Sandwich (Bergen Items)": 3235924323, 101 | "Slot Unlock: Green Leaf Tea (Bergen Items)": 3235924324, 102 | "Slot Unlock: Just Water (Bergen Items)": 3235924325, 103 | "Slot Unlock: Spicy Bun (Bergen Items)": 3235924326, 104 | "Slot Unlock: Fruit Drink (Bergen Items)": 3235924327, 105 | "Slot Unlock: Rice Cracker (Bergen Items)": 3235924328, 106 | "Slot Unlock: Veggie Sticks (Bergen Items)": 3235924329, 107 | "Slot Unlock: Kebab Roll (Bergen Items + Blue Ice)": 3235924330, 108 | "Slot Unlock: Meaty Risotto (Bergen Items + Blue Ice)": 3235924331, 109 | "Slot Unlock: Bergen Ice Cream (Bergen Items + Red Flame)": 3235924332, 110 | "Slot Unlock: Sweet Lemonjuice (Bergen Items + Red Flame)": 3235924333, 111 | "Slot Unlock: Salted Peanuts (Bergen Items + Green Seed)": 3235924334, 112 | "Slot Unlock: Cup o' Coffee (Bergen Items + Green Seed)": 3235924335, 113 | "Slot Unlock: Snack Mix (Bergen Items + Green Seed)": 3235924336, 114 | "Shop Unlock: Bergen Weapons": 3235924337, 115 | "Slot Unlock: Iron Goggles (Bergen Weapons)": 3235924338, 116 | "Slot Unlock: Iron Edge (Bergen Weapons)": 3235924339, 117 | "Slot Unlock: Iron Mail (Bergen Weapons)": 3235924340, 118 | "Slot Unlock: Iron Boots (Bergen Weapons)": 3235924341, 119 | "Shop Unlock: Hermit's Hut": 3235924342, 120 | "Slot Unlock: Chili Con Carne (Hermit's Hut)": 3235924343, 121 | "Global Slot Unlock: Chili Con Carne": 3235924344, 122 | "Slot Unlock: Sweet Lemonjuice (Hermit's Hut)": 3235924345, 123 | "Slot Unlock: Sweet Berry Tea (Hermit's Hut)": 3235924346, 124 | "Global Slot Unlock: Sweet Berry Tea": 3235924347, 125 | "Shop Unlock: Ba'kii Items": 3235924348, 126 | "Slot Unlock: Sandwich (Ba'kii Items)": 3235924349, 127 | "Slot Unlock: Hi-Sandwich (Ba'kii Items)": 3235924350, 128 | "Slot Unlock: Green Leaf Tea (Ba'kii Items)": 3235924351, 129 | "Slot Unlock: Just Water (Ba'kii Items)": 3235924352, 130 | "Slot Unlock: Spicy Bun (Ba'kii Items)": 3235924353, 131 | "Slot Unlock: Fruit Drink (Ba'kii Items)": 3235924354, 132 | "Slot Unlock: Rice Cracker (Ba'kii Items)": 3235924355, 133 | "Slot Unlock: Veggie Sticks (Ba'kii Items)": 3235924356, 134 | "Slot Unlock: Bergen Ice Cream (Ba'kii Items)": 3235924357, 135 | "Slot Unlock: Sweet Lemonjuice (Ba'kii Items)": 3235924358, 136 | "Slot Unlock: Kebab Roll (Ba'kii Items + Blue Ice)": 3235924359, 137 | "Slot Unlock: Meaty Risotto (Ba'kii Items + Blue Ice)": 3235924360, 138 | "Slot Unlock: Salted Peanuts (Ba'kii Items + Green Seed)": 3235924361, 139 | "Slot Unlock: Cup o' Coffee (Ba'kii Items + Green Seed)": 3235924362, 140 | "Slot Unlock: Snack Mix (Ba'kii Items + Green Seed)": 3235924363, 141 | "Shop Unlock: Ba'kii Weapons": 3235924364, 142 | "Slot Unlock: Iron Goggles (Ba'kii Weapons)": 3235924365, 143 | "Slot Unlock: Iron Edge (Ba'kii Weapons)": 3235924366, 144 | "Slot Unlock: Iron Mail (Ba'kii Weapons)": 3235924367, 145 | "Slot Unlock: Iron Boots (Ba'kii Weapons)": 3235924368, 146 | "Slot Unlock: Steel Goggles (Ba'kii Weapons)": 3235924369, 147 | "Slot Unlock: Steel Edge (Ba'kii Weapons)": 3235924370, 148 | "Slot Unlock: Steel Mail (Ba'kii Weapons)": 3235924371, 149 | "Slot Unlock: Steel Boots (Ba'kii Weapons)": 3235924372, 150 | "Shop Unlock: Basin Vending Machine": 3235924373, 151 | "Slot Unlock: Sandwich (Basin Vending Machine)": 3235924374, 152 | "Slot Unlock: Hi-Sandwich (Basin Vending Machine)": 3235924375, 153 | "Slot Unlock: Green Leaf Tea (Basin Vending Machine)": 3235924376, 154 | "Slot Unlock: Just Water (Basin Vending Machine)": 3235924377, 155 | "Slot Unlock: Spicy Bun (Basin Vending Machine)": 3235924378, 156 | "Slot Unlock: Fruit Drink (Basin Vending Machine)": 3235924379, 157 | "Slot Unlock: Rice Cracker (Basin Vending Machine)": 3235924380, 158 | "Slot Unlock: Veggie Sticks (Basin Vending Machine)": 3235924381, 159 | "Slot Unlock: Bergen Ice Cream (Basin Vending Machine)": 3235924382, 160 | "Slot Unlock: Sweet Lemonjuice (Basin Vending Machine)": 3235924383, 161 | "Slot Unlock: Salted Peanuts (Basin Vending Machine)": 3235924384, 162 | "Slot Unlock: Cup o' Coffee (Basin Vending Machine)": 3235924385, 163 | "Slot Unlock: Snack Mix (Basin Vending Machine)": 3235924386, 164 | "Slot Unlock: Kebab Roll (Basin Vending Machine)": 3235924387, 165 | "Slot Unlock: Meaty Risotto (Basin Vending Machine)": 3235924388, 166 | "Shop Unlock: Basin Items": 3235924389, 167 | "Slot Unlock: Sandwich (Basin Items)": 3235924390, 168 | "Slot Unlock: Hi-Sandwich (Basin Items)": 3235924391, 169 | "Slot Unlock: Green Leaf Tea (Basin Items)": 3235924392, 170 | "Slot Unlock: Just Water (Basin Items)": 3235924393, 171 | "Slot Unlock: Kebab Roll (Basin Items)": 3235924394, 172 | "Slot Unlock: Meaty Risotto (Basin Items)": 3235924395, 173 | "Slot Unlock: Spicy Bun (Basin Items)": 3235924396, 174 | "Slot Unlock: Fruit Drink (Basin Items)": 3235924397, 175 | "Slot Unlock: Rice Cracker (Basin Items)": 3235924398, 176 | "Slot Unlock: Veggie Sticks (Basin Items)": 3235924399, 177 | "Slot Unlock: Bergen Ice Cream (Basin Items)": 3235924400, 178 | "Slot Unlock: Sweet Lemonjuice (Basin Items)": 3235924401, 179 | "Slot Unlock: Salted Peanuts (Basin Items)": 3235924402, 180 | "Slot Unlock: Cup o' Coffee (Basin Items)": 3235924403, 181 | "Slot Unlock: Snack Mix (Basin Items)": 3235924404, 182 | "Shop Unlock: Basin Weapons": 3235924405, 183 | "Slot Unlock: Silver Goggles (Basin Weapons)": 3235924406, 184 | "Slot Unlock: Silver Edge (Basin Weapons)": 3235924407, 185 | "Slot Unlock: Silver Mail (Basin Weapons)": 3235924408, 186 | "Slot Unlock: Silver Boots (Basin Weapons)": 3235924409, 187 | "Slot Unlock: Titan Goggles (Basin Weapons)": 3235924410, 188 | "Global Slot Unlock: Titan Goggles": 3235924411, 189 | "Slot Unlock: Titan Edge (Basin Weapons)": 3235924412, 190 | "Global Slot Unlock: Titan Edge": 3235924413, 191 | "Slot Unlock: Titan Mail (Basin Weapons)": 3235924414, 192 | "Global Slot Unlock: Titan Mail": 3235924415, 193 | "Slot Unlock: Titan Boots (Basin Weapons)": 3235924416, 194 | "Global Slot Unlock: Titan Boots": 3235924417, 195 | "Shop Unlock: Calzone Shop": 3235924418, 196 | "Slot Unlock: One Up (Calzone Shop)": 3235924419, 197 | "Global Slot Unlock: One Up": 3235924420, 198 | "Shop Unlock: Ridge Weapons": 3235924421, 199 | "Slot Unlock: Titan Goggles (Ridge Weapons)": 3235924422, 200 | "Slot Unlock: Titan Edge (Ridge Weapons)": 3235924423, 201 | "Slot Unlock: Titan Mail (Ridge Weapons)": 3235924424, 202 | "Slot Unlock: Titan Boots (Ridge Weapons)": 3235924425, 203 | "Slot Unlock: Cobalt Goggles (Ridge Weapons)": 3235924426, 204 | "Global Slot Unlock: Cobalt Goggles": 3235924427, 205 | "Slot Unlock: Cobalt Edge (Ridge Weapons)": 3235924428, 206 | "Global Slot Unlock: Cobalt Edge": 3235924429, 207 | "Slot Unlock: Cobalt Mail (Ridge Weapons)": 3235924430, 208 | "Global Slot Unlock: Cobalt Mail": 3235924431, 209 | "Slot Unlock: Cobalt Boots (Ridge Weapons)": 3235924432, 210 | "Global Slot Unlock: Cobalt Boots": 3235924433, 211 | "Shop Unlock: Ridge Items": 3235924434, 212 | "Slot Unlock: Sandwich (Ridge Items)": 3235924435, 213 | "Slot Unlock: Hi-Sandwich (Ridge Items)": 3235924436, 214 | "Slot Unlock: Green Leaf Tea (Ridge Items)": 3235924437, 215 | "Slot Unlock: Just Water (Ridge Items)": 3235924438, 216 | "Slot Unlock: Kebab Roll (Ridge Items)": 3235924439, 217 | "Slot Unlock: Meaty Risotto (Ridge Items)": 3235924440, 218 | "Slot Unlock: Spicy Bun (Ridge Items)": 3235924441, 219 | "Slot Unlock: Fruit Drink (Ridge Items)": 3235924442, 220 | "Slot Unlock: Rice Cracker (Ridge Items)": 3235924443, 221 | "Slot Unlock: Veggie Sticks (Ridge Items)": 3235924444, 222 | "Slot Unlock: Bergen Ice Cream (Ridge Items)": 3235924445, 223 | "Slot Unlock: Sweet Lemonjuice (Ridge Items)": 3235924446, 224 | "Slot Unlock: Salted Peanuts (Ridge Items)": 3235924447, 225 | "Slot Unlock: Cup o' Coffee (Ridge Items)": 3235924448, 226 | "Slot Unlock: Snack Mix (Ridge Items)": 3235924449, 227 | "Shop Unlock: Rhombus Weapons": 3235924450, 228 | "Slot Unlock: Cobalt Goggles (Rhombus Weapons)": 3235924509, 229 | "Slot Unlock: Cobalt Edge (Rhombus Weapons)": 3235924510, 230 | "Slot Unlock: Cobalt Mail (Rhombus Weapons)": 3235924511, 231 | "Slot Unlock: Cobalt Boots (Rhombus Weapons)": 3235924512, 232 | "Slot Unlock: Laser Goggles (Rhombus Weapons)": 3235924451, 233 | "Global Slot Unlock: Laser Goggles": 3235924452, 234 | "Slot Unlock: Laser Edge (Rhombus Weapons)": 3235924453, 235 | "Global Slot Unlock: Laser Edge": 3235924454, 236 | "Slot Unlock: Laser Mail (Rhombus Weapons)": 3235924455, 237 | "Global Slot Unlock: Laser Mail": 3235924456, 238 | "Slot Unlock: Laser Boots (Rhombus Weapons)": 3235924457, 239 | "Global Slot Unlock: Laser Boots": 3235924458, 240 | "Shop Unlock: Rhombus Items": 3235924459, 241 | "Slot Unlock: Sandwich (Rhombus Items)": 3235924460, 242 | "Slot Unlock: Hi-Sandwich (Rhombus Items)": 3235924461, 243 | "Slot Unlock: Green Leaf Tea (Rhombus Items)": 3235924462, 244 | "Slot Unlock: Just Water (Rhombus Items)": 3235924463, 245 | "Slot Unlock: Kebab Roll (Rhombus Items)": 3235924464, 246 | "Slot Unlock: Meaty Risotto (Rhombus Items)": 3235924465, 247 | "Slot Unlock: Spicy Bun (Rhombus Items)": 3235924466, 248 | "Slot Unlock: Fruit Drink (Rhombus Items)": 3235924467, 249 | "Slot Unlock: Rice Cracker (Rhombus Items)": 3235924468, 250 | "Slot Unlock: Veggie Sticks (Rhombus Items)": 3235924469, 251 | "Slot Unlock: Bergen Ice Cream (Rhombus Items)": 3235924470, 252 | "Slot Unlock: Sweet Lemonjuice (Rhombus Items)": 3235924471, 253 | "Slot Unlock: Salted Peanuts (Rhombus Items)": 3235924472, 254 | "Slot Unlock: Cup o' Coffee (Rhombus Items)": 3235924473, 255 | "Slot Unlock: Snack Mix (Rhombus Items)": 3235924474, 256 | "Shop Unlock: Rhombus Curios": 3235924475, 257 | "Slot Unlock: Chest Detector (Rhombus Curios)": 3235924476, 258 | "Global Slot Unlock: Chest Detector": 3235924477, 259 | "Shop Unlock: Rhombus Backer Items": 3235924478, 260 | "Slot Unlock: Rising Super Star (Rhombus Backer Items)": 3235924479, 261 | "Slot Unlock: Dk Pepper (Rhombus Backer Items)": 3235924480, 262 | "Slot Unlock: Cheese Spaetzle (Rhombus Backer Items)": 3235924481, 263 | "Slot Unlock: Maultasche (Rhombus Backer Items)": 3235924482, 264 | "Slot Unlock: Durian (Rhombus Backer Items)": 3235924483, 265 | "Slot Unlock: PengoPop (Rhombus Backer Items)": 3235924484, 266 | "Slot Unlock: Spicy Beat-0-Type (Rhombus Backer Items)": 3235924485, 267 | "Slot Unlock: Werewolf Stick (Rhombus Backer Items)": 3235924486, 268 | "Slot Unlock: Mooncake (Rhombus Backer Items)": 3235924487, 269 | "Slot Unlock: Guacamole Toast (Rhombus Backer Items)": 3235924488, 270 | "Slot Unlock: Willis Waldmahl (Rhombus Backer Items)": 3235924489, 271 | "Slot Unlock: Pumpkin Spice Cof. (Rhombus Backer Items)": 3235924490, 272 | "Shop Unlock: Vermillion Weapons": 3235924491, 273 | "Slot Unlock: Steel Goggles (Vermillion Weapons)": 3235924492, 274 | "Slot Unlock: Steel Edge (Vermillion Weapons)": 3235924493, 275 | "Slot Unlock: Steel Mail (Vermillion Weapons)": 3235924494, 276 | "Slot Unlock: Steel Boots (Vermillion Weapons)": 3235924495, 277 | "Slot Unlock: Silver Goggles (Vermillion Weapons)": 3235924496, 278 | "Slot Unlock: Silver Edge (Vermillion Weapons)": 3235924497, 279 | "Slot Unlock: Silver Mail (Vermillion Weapons)": 3235924498, 280 | "Slot Unlock: Silver Boots (Vermillion Weapons)": 3235924499, 281 | "Shop Unlock: Vermillion Items": 3235924500, 282 | "Slot Unlock: Sandwich (Vermillion Items)": 3235924501, 283 | "Slot Unlock: Hi-Sandwich (Vermillion Items)": 3235924502, 284 | "Slot Unlock: Green Leaf Tea (Vermillion Items)": 3235924503, 285 | "Slot Unlock: Just Water (Vermillion Items)": 3235924504, 286 | "Slot Unlock: Spicy Bun (Vermillion Items)": 3235924505, 287 | "Slot Unlock: Fruit Drink (Vermillion Items)": 3235924506, 288 | "Slot Unlock: Rice Cracker (Vermillion Items)": 3235924507, 289 | "Slot Unlock: Veggie Sticks (Vermillion Items)": 3235924508 290 | } -------------------------------------------------------------------------------- /icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeTriangle/CCMultiworldRandomizer/5af9f12650420ea4b4a5f28de2bd7a2618ff29d7/icon-24.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mw-rando", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "node ./build.mjs", 7 | "watch": "nodemon -i mw-rando/ -e ts,js,mjs,cjs,json" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/node": "^20.4.9", 13 | "esbuild": "^0.18.15", 14 | "nax-ccuilib": "naxane/nax-ccuilib", 15 | "nax-module-cache": "github:naxane/nax-module-cache", 16 | "nodemon": "^3.0.1", 17 | "typescript": "^5.1.6", 18 | "ultimate-crosscode-typedefs": "github:krypciak/ultimate-crosscode-typedefs" 19 | }, 20 | "dependencies": { 21 | "archipelago.js": "file:extern/apjs", 22 | "esbuild-plugin-polyfill-node": "^0.3.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | export const BASE_ID = 300000 2 | export const BASE_NORMAL_ITEM_ID = 300100 3 | -------------------------------------------------------------------------------- /src/item-data.model.ts: -------------------------------------------------------------------------------- 1 | export interface WorldData { 2 | items: RawItems; 3 | quests: RawQuests; 4 | shops: RawShops; 5 | descriptions: { [itemId: number]: { [locale: string]: string } 6 | } 7 | 8 | export type RawItems = { [mapName: string]: RawMapItems }; 9 | 10 | export interface RawMapItems { 11 | chests?: RawChests; 12 | cutscenes?: RawEvents; 13 | elements?: RawElements; 14 | } 15 | 16 | export type RawChests = { [mapId: string]: RawChest }; 17 | 18 | export interface RawChest { 19 | name: string; 20 | mwids: number[]; 21 | } 22 | 23 | export type RawEvents = { [mapId: string]: RawEvent[] }; 24 | 25 | export interface RawEvent { 26 | mwids: number[]; 27 | path: string; 28 | } 29 | 30 | export type RawElements = { [mapId: string]: RawElement }; 31 | 32 | export interface RawElement { 33 | mwids: number[]; 34 | } 35 | 36 | export type RawQuests = { [questName: string]: RawQuest }; 37 | 38 | export interface RawQuest { 39 | mwids: number[]; 40 | } 41 | 42 | export interface RawShopLocations { 43 | perItemType: Record; 44 | perShop: Record>; 45 | } 46 | 47 | export interface RawShopUnlocks { 48 | byId: Record; 49 | byShop: Record; 50 | byShopAndId: Record>; 51 | } 52 | 53 | export type RawShops = { 54 | locations: RawShopLocations; 55 | unlocks: RawShopUnlocks; 56 | } 57 | 58 | export interface ItemInfo { 59 | icon: string; 60 | label: string; 61 | player: string; 62 | level: number; 63 | isScalable: boolean; 64 | shops?: RawShops; 65 | } 66 | -------------------------------------------------------------------------------- /src/items.ts: -------------------------------------------------------------------------------- 1 | import {BASE_NORMAL_ITEM_ID} from "./common.js" 2 | 3 | -------------------------------------------------------------------------------- /src/patches/chest.ts: -------------------------------------------------------------------------------- 1 | import * as ap from "archipelago.js"; 2 | import type MwRandomizer from "../plugin"; 3 | import {RawChest} from "../item-data.model"; 4 | 5 | declare global { 6 | namespace ig.ENTITY { 7 | interface Chest { 8 | mwCheck?: RawChest; 9 | analyzeColor: sc.ANALYSIS_COLORS; 10 | analyzeLabel: string; 11 | rawChest: ap.NetworkItem; 12 | } 13 | } 14 | namespace sc { 15 | namespace QUICK_MENU_TYPES { 16 | namespace Chest { 17 | interface Settings extends sc.QuickMenuTypesBase { 18 | type: "Chest"; 19 | entity: ig.ENTITY.Chest; 20 | } 21 | } 22 | interface Chest extends sc.QuickMenuTypesBase {} 23 | interface ChestConstructor extends ImpactClass { 24 | new ( 25 | type: string, 26 | settings: sc.QUICK_MENU_TYPES.Chest.Settings, 27 | screen: sc.QuickFocusScreen 28 | ): Chest; 29 | } 30 | var Chest: ChestConstructor; 31 | } 32 | namespace QUICK_INFO_BOXES { 33 | interface Chest extends ig.BoxGui { 34 | areaGui: sc.TextGui; 35 | locationGui: sc.TextGui; 36 | line: sc.LineGui; 37 | clearance: sc.TextGui; 38 | arrow: sc.QuickItemArrow; 39 | typeGui: sc.TextGui; 40 | active: boolean; 41 | 42 | show(this: this, tooltip: sc.QuickMenuTypesBase): void; 43 | setData(this: this, chest: ig.ENTITY.Chest): boolean; 44 | alignToBase(this: this, otherHook: ig.GuiHook): void; 45 | } 46 | interface ChestConstructor extends ImpactClass { 47 | new (): Chest; 48 | } 49 | var Chest: ChestConstructor; 50 | } 51 | } 52 | } 53 | 54 | export function patch(plugin: MwRandomizer) { 55 | ig.ENTITY.Chest.inject({ 56 | init(...args) { 57 | this.parent(...args); 58 | 59 | const map = plugin.randoData?.items[ig.game.mapName]; 60 | if (!map) { 61 | return; 62 | } 63 | 64 | this.mwCheck = map.chests?.[this.mapId]; 65 | if (!this.mwCheck) { 66 | return; 67 | } 68 | 69 | if (!sc.multiworld.locationInfo) { 70 | return; 71 | } 72 | 73 | const clearance = sc.multiworld.options.chestClearanceLevels?.[this.mwCheck.mwids[0]]; 74 | 75 | if (clearance != undefined) { 76 | this.chestType = sc.CHEST_TYPE[clearance]; 77 | } 78 | 79 | const anims = this.animSheet.anims as unknown as { 80 | idleKey: ig.MultiDirAnimationSet 81 | idleMasterKey: ig.MultiDirAnimationSet 82 | idle: ig.MultiDirAnimationSet 83 | open: ig.MultiDirAnimationSet 84 | end: ig.MultiDirAnimationSet 85 | } 86 | const keyLayer = anims.idleKey.animations[1]; 87 | const masterKeyLayer = anims.idleMasterKey.animations[1]; 88 | let layerToAdd = null; 89 | 90 | this.analyzeColor = sc.ANALYSIS_COLORS.GREY; 91 | this.analyzeLabel = "Filler"; 92 | 93 | let newOffY = 0; 94 | this.rawChest = sc.multiworld.locationInfo[this.mwCheck.mwids[0]]; 95 | 96 | if (this.rawChest == undefined) { 97 | return; 98 | } 99 | 100 | let flags = this.rawChest.flags; 101 | if (flags & (ap.ITEM_FLAGS.NEVER_EXCLUDE | ap.ITEM_FLAGS.TRAP)) { 102 | // USEFUL and TRAP items get a blue chest 103 | newOffY = 80; 104 | layerToAdd = keyLayer; 105 | this.analyzeColor = sc.ANALYSIS_COLORS.BLUE; 106 | this.analyzeLabel = "Useful"; 107 | } else if (flags & ap.ITEM_FLAGS.PROGRESSION) { 108 | // PROGRESSION items get a green chest 109 | newOffY = 136; 110 | layerToAdd = masterKeyLayer; 111 | this.analyzeColor = sc.ANALYSIS_COLORS.GREEN; 112 | this.analyzeLabel = "Progression"; 113 | } 114 | 115 | anims.idleKey = anims.idleMasterKey = anims.idle; 116 | 117 | if (newOffY == 0) { 118 | return; 119 | } 120 | 121 | for (const name of Object.keys(anims) as (keyof typeof anims)[]) { 122 | let animations = anims[name].animations; 123 | 124 | if (name.startsWith("idle")) { 125 | animations[0].sheet.offY = newOffY; 126 | layerToAdd && animations.splice(1, 0, layerToAdd); 127 | } 128 | if (name == "open" || name == "end") { 129 | anims[name].animations[0].sheet.offY = newOffY + 24; 130 | } 131 | } 132 | }, 133 | 134 | getQuickMenuSettings() { 135 | let disabled = this.isOpen || (this.hideManager && this.hideManager.hidden); 136 | if (this.mwCheck && this.rawChest) { 137 | return { 138 | type: "Chest", 139 | disabled: disabled, 140 | }; 141 | } else { 142 | return { 143 | type: "Analyzable", 144 | disabled: disabled, 145 | color: this.analyzeColor ?? 0, 146 | text: "\\c[1]Not in logic", 147 | }; 148 | } 149 | }, 150 | 151 | isQuickMenuVisible() { 152 | return this.mwCheck && this.rawChest; 153 | }, 154 | 155 | _reallyOpenUp() { 156 | if ( 157 | this.mwCheck === undefined || 158 | this.mwCheck.mwids === undefined || 159 | this.mwCheck.mwids.length == 0 || 160 | sc.multiworld.locationInfo[this.mwCheck.mwids[0]] === undefined 161 | ) { 162 | console.warn("Chest not in logic"); 163 | return this.parent(); 164 | } 165 | 166 | const old = sc.ItemDropEntity.spawnDrops; 167 | try { 168 | if (this.mwCheck) { 169 | sc.multiworld.reallyCheckLocations(this.mwCheck.mwids); 170 | } 171 | 172 | this.amount = 0; 173 | return this.parent(); 174 | } finally { 175 | sc.ItemDropEntity.spawnDrops = old; 176 | } 177 | }, 178 | }); 179 | 180 | sc.QUICK_MENU_TYPES.Chest = sc.QuickMenuTypesBase.extend({ 181 | init(type, settings, screen) { 182 | this.parent(type, settings, screen); 183 | this.setIconColor(settings.entity.analyzeColor); 184 | }, 185 | }); 186 | 187 | sc.QUICK_INFO_BOXES.Chest = ig.BoxGui.extend({ 188 | ninepatch: new ig.NinePatch("media/gui/menu.png", { 189 | width: 8, 190 | height: 8, 191 | left: 8, 192 | top: 8, 193 | right: 8, 194 | bottom: 8, 195 | offsets: {default: {x: 432, y: 304}, flipped: {x: 456, y: 304}}, 196 | }), 197 | transitions: { 198 | HIDDEN: { 199 | state: {alpha: 0}, 200 | time: 0.2, 201 | timeFunction: KEY_SPLINES.LINEAR, 202 | }, 203 | DEFAULT: {state: {}, time: 0.2, timeFunction: KEY_SPLINES.EASE}, 204 | }, 205 | 206 | init() { 207 | this.parent(127, 100); 208 | this.areaGui = new sc.TextGui("", {font: sc.fontsystem.tinyFont}); 209 | this.areaGui.setPos(0, 6); 210 | this.areaGui.setAlign(ig.GUI_ALIGN.X_CENTER, ig.GUI_ALIGN.Y_TOP); 211 | this.addChildGui(this.areaGui); 212 | 213 | this.locationGui = new sc.TextGui("", { 214 | font: sc.fontsystem.smallFont, 215 | maxWidth: 115, 216 | linePadding: -2, 217 | }); 218 | this.locationGui.setPos(8, 19); 219 | this.addChildGui(this.locationGui); 220 | 221 | this.line = new sc.LineGui(117); 222 | this.line.setPos(5, 16); 223 | this.addChildGui(this.line); 224 | 225 | this.clearance = new sc.TextGui("", {font: sc.fontsystem.tinyFont}); 226 | this.clearance.setPos(5, 13); 227 | this.clearance.setAlign(ig.GUI_ALIGN.X_RIGHT, ig.GUI_ALIGN.Y_TOP); 228 | this.addChildGui(this.clearance); 229 | 230 | this.arrow = new sc.QuickItemArrow(); 231 | this.addChildGui(this.arrow); 232 | 233 | this.typeGui = new sc.TextGui("", {font: sc.fontsystem.tinyFont}); 234 | this.typeGui.setPos(8, 5); 235 | this.typeGui.setAlign(ig.GUI_ALIGN.X_LEFT, ig.GUI_ALIGN.Y_BOTTOM); 236 | this.addChildGui(this.typeGui); 237 | }, 238 | 239 | show(tooltip) { 240 | let chest = tooltip.entity; 241 | if (!this.setData(chest)) { 242 | return; 243 | } 244 | this.alignToBase(tooltip.hook); 245 | this.doStateTransition("DEFAULT"); 246 | this.active = true; 247 | }, 248 | 249 | hide() { 250 | this.doStateTransition("HIDDEN"); 251 | this.active = false; 252 | }, 253 | 254 | setData(chest) { 255 | if (!chest.mwCheck) { 256 | return false; 257 | } 258 | 259 | const area = ig.LangLabel.getText(sc.map.areas[sc.map.currentArea.cacheKey].name); 260 | const [shortArea, location] = chest.mwCheck.name.split(": ", 2); 261 | 262 | let level = null; 263 | 264 | if (chest.chestType == sc.CHEST_TYPE.Bronze) { 265 | level = "Bronze"; 266 | } 267 | if (chest.chestType == sc.CHEST_TYPE.Silver) { 268 | level = "Silver"; 269 | } 270 | if (chest.chestType == sc.CHEST_TYPE.Gold) { 271 | level = "Gold"; 272 | } 273 | 274 | this.areaGui.setText(`\\c[4]${shortArea}\\c[0]`); 275 | this.locationGui.setText(location); 276 | if (level) { 277 | this.clearance.setText(`\\c[3]${level}\\c[0]`); 278 | this.line.hook.size.x = 114 - this.clearance.hook.size.x; 279 | } else { 280 | this.clearance.setText(""); 281 | this.line.hook.size.x = 117; 282 | } 283 | 284 | this.hook.size.y = 34 + this.locationGui.hook.size.y; 285 | 286 | this.typeGui.setText(`Type: \\c[3]${chest.analyzeLabel}\\c[0]`); 287 | 288 | return true; 289 | }, 290 | 291 | alignToBase: function (otherHook) { 292 | let hook = this.hook; 293 | let invisible = hook.currentState.alpha == 0; 294 | 295 | let vec = Vec2.createC(0, 0); 296 | vec.x = otherHook.pos.x + Math.floor(otherHook.size.x / 2); 297 | vec.y = otherHook.pos.y + Math.floor(otherHook.size.y / 2); 298 | 299 | let above = vec.y - 25; 300 | 301 | vec.y = Math.max(10, Math.min(ig.system.height - this.hook.size.y - 10, above)); 302 | 303 | if (invisible) { 304 | hook.pos.y = vec.y; 305 | } 306 | 307 | var arrowY = 17 + (above - vec.y); 308 | if (vec.x + 173 < ig.system.width) { 309 | this.currentTileOffset = "default"; 310 | if (invisible) hook.pos.x = vec.x + 20 + 10; 311 | hook.doPosTranstition(vec.x + 20, vec.y, 0.2, KEY_SPLINES.EASE); 312 | this.arrow.setPosition(-10, Math.max(7, Math.min(hook.size.y - 15, arrowY)), false); 313 | } else { 314 | this.currentTileOffset = "flipped"; 315 | if (invisible) hook.pos.x = vec.x - hook.size.x - 20 - 10 - 1; 316 | hook.doPosTranstition( 317 | vec.x - hook.size.x - 20 - 1, 318 | vec.y, 319 | 0.2, 320 | KEY_SPLINES.EASE, 321 | ); 322 | this.arrow.setPosition( 323 | hook.size.x + 1, 324 | Math.max(7, Math.min(hook.size.y - 15, arrowY)), 325 | true, 326 | ); 327 | } 328 | 329 | this.arrow.bottomAnchor = false; 330 | this.arrow.flipY = false; 331 | if (arrowY < 7) { 332 | this.arrow.bottomAnchor = true; 333 | this.arrow.flipY = true; 334 | } else if (arrowY > hook.size.y - 15) { 335 | this.arrow.bottomAnchor = true; 336 | } 337 | }, 338 | }); 339 | } 340 | -------------------------------------------------------------------------------- /src/patches/entity.ts: -------------------------------------------------------------------------------- 1 | import type MwRandomizer from "../plugin"; 2 | 3 | function set(root: Record, value: any, path: string[], offset = 0) { 4 | if (path.length <= offset) { 5 | return; 6 | } 7 | while (offset < path.length - 1) { 8 | root = root[path[offset]]; 9 | offset++; 10 | } 11 | 12 | if (path.length - 1 === offset) { 13 | root[path[offset]] = value; 14 | } 15 | } 16 | 17 | export function patch(plugin: MwRandomizer) { 18 | let maps = plugin.randoData?.items; 19 | 20 | ig.Game.inject({ 21 | loadLevel(map, ...args) { 22 | const mapOverrides = maps?.[map.name.replace(/[\\\/]/g, '.')]; 23 | if (mapOverrides) { 24 | for (const entity of map.entities) { 25 | if ( 26 | entity 27 | && entity.settings 28 | && entity.settings.mapId 29 | && mapOverrides.cutscenes 30 | && mapOverrides.cutscenes[entity.settings.mapId] 31 | ) { 32 | for (const check of mapOverrides.cutscenes[entity.settings.mapId]) { 33 | const path = check.path.slice(1).split(/\./g); 34 | 35 | set(entity, 'SEND_ITEM', [...path, 'type']); 36 | set(entity, check.mwids, [...path, 'mwids']); 37 | } 38 | } 39 | } 40 | } 41 | 42 | return this.parent(map, ...args); 43 | } 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/patches/event.ts: -------------------------------------------------------------------------------- 1 | import * as ap from 'archipelago.js'; 2 | import type MwRandomizer from "../plugin"; 3 | import {RawElement} from '../item-data.model'; 4 | 5 | export function patch(plugin: MwRandomizer) { 6 | let maps = plugin.randoData?.items; 7 | 8 | ig.EVENT_STEP.SET_PLAYER_CORE.inject({ 9 | start() { 10 | if ( 11 | this.core != sc.PLAYER_CORE.ELEMENT_HEAT && 12 | this.core != sc.PLAYER_CORE.ELEMENT_COLD && 13 | this.core != sc.PLAYER_CORE.ELEMENT_SHOCK && 14 | this.core != sc.PLAYER_CORE.ELEMENT_WAVE 15 | ) { 16 | return this.parent(); 17 | } 18 | 19 | const map = maps?.[ig.game.mapName]; 20 | if (!map || !map.elements) { 21 | return this.parent(); 22 | } 23 | 24 | const check = Object.values(map.elements)[0] as RawElement; 25 | sc.multiworld.reallyCheckLocations(check.mwids); 26 | } 27 | }); 28 | 29 | ig.EVENT_STEP.RESET_SKILL_TREE.inject({ 30 | start() { 31 | if (maps?.[ig.game.mapName]) { 32 | return; // do not reset the skilltree if there is a check in the room 33 | } 34 | return this.parent(); 35 | } 36 | }); 37 | 38 | ig.EVENT_STEP.SEND_ITEM = ig.EventStepBase.extend({ 39 | mwids: [], 40 | oldItem: undefined, 41 | init(settings) { 42 | this.mwids = settings.mwids.filter(x => sc.multiworld.locationInfo[x] != undefined); 43 | this.oldItem = { 44 | "item": settings.item, 45 | "amount": settings.amount, 46 | } 47 | }, 48 | start() { 49 | if (this.mwids.length == 0) { 50 | let amount = ig.Event.getExpressionValue(this.oldItem.amount); 51 | sc.model.player.addItem(this.oldItem.item, amount, false); 52 | } 53 | 54 | sc.multiworld.reallyCheckLocations(this.mwids); 55 | } 56 | }); 57 | 58 | ig.EVENT_STEP.MW_GOAL_COMPLETED = ig.EventStepBase.extend({ 59 | init(settings) { 60 | // In the future, goal will only update client status if it checks off 61 | // a specific goal specified by their yaml. For now, there's only one 62 | // goal. 63 | this.goal = settings.goal; 64 | }, 65 | start() { 66 | sc.multiworld.client.updateStatus(ap.CLIENT_STATUS.GOAL); 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/patches/gui-misc.ts: -------------------------------------------------------------------------------- 1 | import * as ap from 'archipelago.js'; 2 | import type MwRandomizer from '../plugin'; 3 | 4 | import type * as _ from 'nax-ccuilib/src/headers/nax/input-field.d.ts' 5 | 6 | declare global { 7 | namespace sc { 8 | interface APConnectionStatusGui extends sc.TextGui, sc.Model.Observer { 9 | updateText(this: this): void; 10 | } 11 | interface APConnectionStatusGuiConstructor extends ImpactClass { 12 | new (): APConnectionStatusGui; 13 | } 14 | var APConnectionStatusGui: APConnectionStatusGuiConstructor; 15 | 16 | interface PauseScreenGui { 17 | apConnectionStatusGui: sc.APConnectionStatusGui; 18 | apSettingsButton: sc.ButtonGui; 19 | } 20 | enum MENU_SUBMENU { 21 | AP_CONNECTION, 22 | } 23 | 24 | interface APConnectionBox extends sc.BaseMenu, sc.Model.Observer { 25 | fields: {key: string; label: string, obscure?: boolean}[]; 26 | textGuis: sc.TextGui[]; 27 | inputGuis: nax.ccuilib.InputField[]; 28 | boundExitCallback: () => void; 29 | buttongroup: sc.ButtonGroup; 30 | inputList: ig.GuiElementBase; 31 | textColumnWidth: number; 32 | vSpacer: number; 33 | hSpacer: number; 34 | content: ig.GuiElementBase; 35 | msgBox: sc.BlackWhiteBox; 36 | apConnectionStatusGui: sc.APConnectionStatusGui; 37 | msgBoxBox: ig.GuiElementBase; 38 | connect: sc.ButtonGui; 39 | disconnect: sc.ButtonGui; 40 | buttonHolder: ig.GuiElementBase; 41 | back: null; 42 | keepOpen: boolean; 43 | 44 | getOptions(this: this): Record; 45 | onBackButtonPress(this: this): void; 46 | connectFromInput(this: this): void; 47 | } 48 | interface APConnectionBoxConstructor extends ImpactClass { 49 | new (): APConnectionBox; 50 | } 51 | var APConnectionBox: APConnectionBoxConstructor; 52 | } 53 | } 54 | 55 | 56 | export function patch(plugin: MwRandomizer) { 57 | sc.QuickMenuAnalysis.inject({ 58 | limitCursorPos() { 59 | sc.quickmodel.cursor.x = sc.quickmodel.cursor.x.limit(0, ig.system.width); 60 | sc.quickmodel.cursor.y = sc.quickmodel.cursor.y.limit(0, ig.system.height); 61 | } 62 | }); 63 | 64 | sc.APConnectionStatusGui = sc.TextGui.extend({ 65 | init: function () { 66 | this.parent("", {font: sc.fontsystem.tinyFont}); 67 | this.updateText(); 68 | 69 | sc.Model.addObserver(sc.multiworld, this); 70 | }, 71 | 72 | updateText: function () { 73 | this.setText(`AP: ${plugin.getColoredStatus(sc.multiworld.client.status.toUpperCase())}`); 74 | }, 75 | 76 | modelChanged(model: any, msg: number, data: any) { 77 | if (model == sc.multiworld && msg == sc.MULTIWORLD_MSG.CONNECTION_STATUS_CHANGED) { 78 | this.updateText(); 79 | } 80 | }, 81 | }); 82 | 83 | sc.PauseScreenGui.inject({ 84 | init(...args) { 85 | this.parent(...args); 86 | 87 | this.apConnectionStatusGui = new sc.APConnectionStatusGui(); 88 | 89 | this.apConnectionStatusGui.setAlign(this.versionGui.hook.align.x, this.versionGui.hook.align.y); 90 | this.apConnectionStatusGui.setPos(0, this.versionGui.hook.size.y * 2); 91 | 92 | this.versionGui.addChildGui(this.apConnectionStatusGui); 93 | 94 | this.apSettingsButton = new sc.ButtonGui("\\i[ap-logo] Archipelago Settings"); 95 | this.apSettingsButton.setPos(3, 3); 96 | this.buttonGroup.addFocusGui(this.apSettingsButton, 1000, 1000 /* makes it unfocusable by gamepad */); 97 | this.apSettingsButton.onButtonPress = function () { 98 | sc.menu.setDirectMode(true, sc.MENU_SUBMENU.AP_CONNECTION); 99 | sc.model.enterMenu(true); 100 | }.bind(this); 101 | 102 | this.addChildGui(this.apSettingsButton); 103 | }, 104 | }); 105 | 106 | sc.APConnectionBox = sc.BaseMenu.extend({ 107 | gfx: new ig.Image("media/gui/menu.png"), 108 | 109 | fields: [ 110 | { 111 | key: "hostname", 112 | label: "Hostname", 113 | }, 114 | { 115 | key: "port", 116 | label: "Port", 117 | }, 118 | { 119 | key: "name", 120 | label: "Slot Name", 121 | }, 122 | { 123 | key: "password", 124 | label: "Password", 125 | obscure: true, 126 | } 127 | ], 128 | 129 | transitions: { 130 | DEFAULT: { state: {}, time: 0.25, timeFunction: KEY_SPLINES.LINEAR }, 131 | HIDDEN: { state: { alpha: 0 }, time: 0.25, timeFunction: KEY_SPLINES.LINEAR }, 132 | }, 133 | 134 | textGuis: [], 135 | inputGuis: [], 136 | 137 | textColumnWidth: 0, 138 | hSpacer: 5, 139 | vSpacer: 3, 140 | 141 | msgBox: null, 142 | msgBoxBox: null, 143 | inputList: null, 144 | content: null, 145 | connect: null, 146 | disconnect: null, 147 | buttonHolder: null, 148 | 149 | buttongroup: null, 150 | back: null, 151 | keepOpen: false, 152 | 153 | init: function () { 154 | this.parent(); 155 | 156 | this.boundExitCallback = () => {} 157 | 158 | this.hook.zIndex = 9999999; 159 | this.hook.localAlpha = 0.0; 160 | this.hook.pauseGui = true; 161 | this.hook.size.x = ig.system.width; 162 | this.hook.size.y = ig.system.height; 163 | 164 | this.buttongroup = new sc.ButtonGroup(); 165 | sc.menu.buttonInteract.pushButtonGroup(this.buttongroup); 166 | 167 | this.buttongroup.addPressCallback(() => {}); 168 | 169 | sc.menu.pushBackCallback(this.onBackButtonPress.bind(this)); 170 | 171 | this.inputList = new ig.GuiElementBase(); 172 | 173 | for (let i = 0; i < this.fields.length; i++) { 174 | let textGui = new sc.TextGui(this.fields[i].label); 175 | this.textColumnWidth = Math.max(this.textColumnWidth, textGui.hook.size.x); 176 | textGui.hook.pos.y = (textGui.hook.size.y + this.vSpacer) * i; 177 | this.inputList.addChildGui(textGui); 178 | this.textGuis.push(textGui); 179 | 180 | let inputGui = new nax.ccuilib.InputField( 181 | 200, 182 | textGui.hook.size.y, 183 | nax.ccuilib.INPUT_FIELD_TYPE.DEFAULT, 184 | this.fields[i].obscure ?? false 185 | ); 186 | this.buttongroup.addFocusGui(inputGui, 0, i); 187 | inputGui.hook.pos.y = (textGui.hook.size.y + this.vSpacer) * i; 188 | 189 | if (sc.multiworld.connectionInfo) { 190 | //@ts-ignore 191 | let prefill = "" + sc.multiworld.connectionInfo[this.fields[i].key]; 192 | inputGui.value = prefill.split(""); 193 | inputGui.setObscure(this.fields[i].obscure ?? false); 194 | inputGui.cursorPos = prefill.length; 195 | inputGui.cursor.hook.pos.x = inputGui.calculateCursorPos(); 196 | } 197 | 198 | this.inputList.addChildGui(inputGui); 199 | this.inputGuis.push(inputGui); 200 | } 201 | 202 | for (const gui of this.inputGuis) { 203 | gui.hook.pos.x = this.textColumnWidth + this.hSpacer; 204 | } 205 | 206 | this.inputList.setSize( 207 | this.textColumnWidth + this.hSpacer + 200, 208 | this.textGuis[0].hook.size.y * this.textGuis.length + this.vSpacer * (this.textGuis.length - 1) 209 | ); 210 | 211 | this.content = new ig.GuiElementBase(); 212 | 213 | this.msgBox = new sc.BlackWhiteBox(this.inputList.hook.size.x, this.inputList.hook.size.y); 214 | this.msgBox.setSize(this.inputList.hook.size.x + 22, this.inputList.hook.size.y + 10); 215 | this.msgBox.addChildGui(this.inputList); 216 | 217 | this.inputList.setAlign(ig.GUI_ALIGN.X_CENTER, ig.GUI_ALIGN.Y_CENTER); 218 | 219 | this.apConnectionStatusGui = new sc.APConnectionStatusGui(); 220 | this.apConnectionStatusGui.setPos(7, 0); 221 | 222 | this.msgBoxBox = new ig.GuiElementBase(); 223 | this.msgBoxBox.setSize( 224 | this.msgBox.hook.size.x, 225 | this.msgBox.hook.size.y + this.apConnectionStatusGui.hook.size.y 226 | ); 227 | 228 | this.msgBox.setPos(0, this.apConnectionStatusGui.hook.size.y); 229 | 230 | this.msgBoxBox.addChildGui(this.apConnectionStatusGui); 231 | this.msgBoxBox.addChildGui(this.msgBox); 232 | this.msgBoxBox.setAlign(ig.GUI_ALIGN.X_CENTER, ig.GUI_ALIGN.Y_TOP); 233 | 234 | this.connect = new sc.ButtonGui("Connect", sc.BUTTON_MENU_WIDTH); 235 | this.connect.onButtonPress = this.connectFromInput.bind(this); 236 | this.buttongroup.addFocusGui(this.connect, 0, this.fields.length); 237 | 238 | this.disconnect = new sc.ButtonGui("Disconnect", sc.BUTTON_MENU_WIDTH); 239 | this.disconnect.onButtonPress = () => { sc.multiworld.client.disconnect() }; 240 | this.disconnect.setPos(sc.BUTTON_MENU_WIDTH + this.hSpacer); 241 | this.buttongroup.addFocusGui(this.disconnect, 1, this.fields.length); 242 | 243 | this.buttonHolder = new ig.GuiElementBase(); 244 | 245 | this.buttonHolder.addChildGui(this.connect); 246 | this.buttonHolder.addChildGui(this.disconnect); 247 | this.buttonHolder.setSize(sc.BUTTON_MENU_WIDTH * 2 + this.hSpacer, sc.BUTTON_TYPE.DEFAULT.height); 248 | this.buttonHolder.setAlign(ig.GUI_ALIGN.X_CENTER, ig.GUI_ALIGN.Y_BOTTOM); 249 | 250 | this.content.addChildGui(this.msgBoxBox); 251 | this.content.addChildGui(this.buttonHolder); 252 | this.content.setAlign(ig.GUI_ALIGN.X_CENTER, ig.GUI_ALIGN.Y_CENTER); 253 | 254 | this.content.setSize( 255 | Math.max( 256 | this.msgBoxBox.hook.size.x, 257 | this.buttonHolder.hook.size.x 258 | ), 259 | this.msgBoxBox.hook.size.y + this.buttonHolder.hook.size.y + this.vSpacer, 260 | ); 261 | this.addChildGui(this.content); 262 | 263 | this.doStateTransition("HIDDEN", true); 264 | }, 265 | 266 | getOptions() { 267 | let result: Record = {}; 268 | for (let i = 0; i < this.fields.length; i++) { 269 | result[this.fields[i].key] = this.inputGuis[i].value.join(""); 270 | } 271 | 272 | return result; 273 | }, 274 | 275 | connectFromInput() { 276 | let options = this.getOptions(); 277 | if (isNaN(options.port as unknown as number)) { 278 | sc.Dialogs.showErrorDialog( 279 | "Port is not a number", 280 | true, 281 | ); 282 | return; 283 | } 284 | let portNumber = Number(options.port); 285 | 286 | if (portNumber > 65535 || portNumber < 1) { 287 | sc.Dialogs.showErrorDialog( 288 | "Port must be between 1 and 65535", 289 | true 290 | ); 291 | return; 292 | } 293 | 294 | sc.multiworld.login({ 295 | game: 'CrossCode', 296 | hostname: options.hostname, 297 | password: options.password, 298 | port: portNumber, 299 | items_handling: ap.ITEMS_HANDLING_FLAGS.REMOTE_ALL, 300 | name: options.name, 301 | }); 302 | }, 303 | 304 | showMenu: function () { 305 | this.parent(); 306 | ig.interact.setBlockDelay(0.1); 307 | this.addObservers(); 308 | this.msgBox.doStateTransition("DEFAULT"); 309 | this.doStateTransition("DEFAULT"); 310 | }, 311 | 312 | exitMenu: function () { 313 | this.parent(); 314 | ig.interact.setBlockDelay(0.1); 315 | this.removeObservers(); 316 | this.doStateTransition("HIDDEN", false); 317 | 318 | }, 319 | 320 | onBackButtonPress: function () { 321 | sc.menu.popBackCallback(); 322 | sc.menu.popMenu(); 323 | }, 324 | 325 | addObservers: function () { 326 | sc.Model.addObserver(sc.model, this); 327 | sc.Model.addObserver(sc.multiworld, this); 328 | }, 329 | 330 | removeObservers: function () { 331 | sc.Model.removeObserver(sc.model, this); 332 | sc.Model.removeObserver(sc.multiworld, this); 333 | }, 334 | 335 | modelChanged: function(model: any, msg: number, data: any) { 336 | if (model == sc.multiworld && msg == sc.MULTIWORLD_MSG.OPTIONS_PRESENT) { 337 | // if we launched from the title screen that means we are in a context 338 | // where we want to put in our login info and start the game. 339 | // so we wait for options to be present and when they are, we exit the menu. 340 | // exiting the menu automatically activates a bit of code 341 | // set by the new game mode select callback. 342 | // if connection details are available, it starts the game. 343 | if (sc.model.isTitle()) { 344 | this.doStateTransition("HIDDEN"); 345 | sc.menu.pushMenu(sc.MENU_SUBMENU.NEW_GAME); 346 | } 347 | } 348 | }, 349 | 350 | onDetach: function () {}, 351 | }); 352 | 353 | // @ts-expect-error 354 | sc.MENU_SUBMENU.AP_CONNECTION = 300000; 355 | sc.SUB_MENU_INFO[sc.MENU_SUBMENU.AP_CONNECTION] = { 356 | Clazz: sc.APConnectionBox, 357 | name: "apConnection", 358 | }; 359 | } 360 | -------------------------------------------------------------------------------- /src/patches/index.ts: -------------------------------------------------------------------------------- 1 | import type MwRandomizer from "../plugin"; 2 | 3 | import { patch as patchMwModel } from "./multiworld-model"; 4 | import { patch as patchChest } from "./chest"; 5 | import { patch as patchEntities } from "./entity"; 6 | import { patch as patchEvent } from "./event"; 7 | import { patch as patchMarquee } from "./marquee"; 8 | import { patch as patchGui } from "./gui-misc"; 9 | import { patch as patchMWHud } from "./multiworld-hud"; 10 | import { patch as patchQuest } from "./quest"; 11 | import { patch as patchShop } from "./shop"; 12 | import { patch as patchNewGame } from "./new-game"; 13 | 14 | export function applyPatches(plugin: MwRandomizer) { 15 | patchMwModel(plugin); 16 | patchChest(plugin); 17 | patchEntities(plugin); 18 | patchEvent(plugin); 19 | patchMarquee(plugin); 20 | patchGui(plugin); 21 | patchMWHud(plugin); 22 | patchQuest(plugin); 23 | patchShop(plugin); 24 | patchNewGame(plugin); 25 | } 26 | -------------------------------------------------------------------------------- /src/patches/marquee.ts: -------------------------------------------------------------------------------- 1 | import * as ap from "archipelago.js"; 2 | import type MwRandomizer from "../plugin"; 3 | import type { ItemInfo, RawQuest } from "../item-data.model"; 4 | import { getElementIconString } from "../utils"; 5 | 6 | declare global { 7 | namespace sc { 8 | namespace TextMarqueeGui { 9 | interface Settings extends sc.TextGui.Settings { 10 | autoScroll?: boolean; 11 | scrollSpeed?: number; 12 | holdTime?: number; 13 | holdOnReset?: boolean; 14 | } 15 | } 16 | 17 | interface TextMarqueeGui extends ig.GuiElementBase { 18 | textGui: sc.TextGui; 19 | active: boolean; 20 | scrollSpeed: number; 21 | holdTime: number; 22 | holdOnReset: boolean; 23 | currentPosition: number; 24 | direction: number; 25 | timer: number; 26 | group: sc.MarqueeGroup; 27 | 28 | setText(this: this, text: string): void; 29 | addToGroup(this: this, group: sc.MarqueeGroup): void; 30 | activate(this: this): void; 31 | deactivate(this: this): void; 32 | reset(this: this): void; 33 | startTimer(this: this): void; 34 | } 35 | 36 | interface TextMarqueeGuiConstructor extends ImpactClass { 37 | new(text: string, width: number, settings: sc.TextMarqueeGui.Settings | undefined): TextMarqueeGui; 38 | } 39 | 40 | var TextMarqueeGui: sc.TextMarqueeGuiConstructor; 41 | 42 | interface ItemMarqueeGui extends ig.GuiElementBase { 43 | iconGui: sc.TextGui; 44 | labelGui: sc.TextMarqueeGui; 45 | settings: sc.TextMarqueeGui.Settings; 46 | 47 | setText(this: this, text: string): void; 48 | addToGroup(this: this, group: sc.MarqueeGroup): void; 49 | } 50 | 51 | interface ItemMarqueeGuiConstructor extends ImpactClass { 52 | new(icon: string, label: string, width: number, settings?: sc.TextMarqueeGui.Settings): ItemMarqueeGui; 53 | } 54 | 55 | var ItemMarqueeGui: sc.ItemMarqueeGuiConstructor; 56 | 57 | interface MultiWorldItemMarqueeGui extends sc.ItemMarqueeGui { 58 | worldGui: sc.TextGui; 59 | itemInfo: ItemInfo; 60 | 61 | setText(this: this, text: string): void; 62 | } 63 | 64 | interface MultiWorldItemMarqueeGuiConstructor extends ImpactClass { 65 | new(data: ItemInfo, width: number, settings?: sc.TextMarqueeGui.Settings): MultiWorldItemMarqueeGui; 66 | } 67 | 68 | var MultiWorldItemMarqueeGui: sc.MultiWorldItemMarqueeGuiConstructor; 69 | 70 | interface MarqueeGroup extends ig.Class { 71 | elements: TextMarqueeGui[]; 72 | active: boolean; 73 | finished: TextMarqueeGui[]; 74 | 75 | add(this: this, gui: sc.TextMarqueeGui): void; 76 | setDone(this: this, gui: sc.TextMarqueeGui): void; 77 | activate(this: this): void; 78 | deactivate(this: this): void; 79 | reset(this: this): void; 80 | } 81 | 82 | interface MarqueeGroupConstructor extends ImpactClass { 83 | new(active: boolean): MarqueeGroup; 84 | } 85 | 86 | var MarqueeGroup: sc.MarqueeGroupConstructor; 87 | } 88 | } 89 | 90 | export function patch(plugin: MwRandomizer) { 91 | const DEFAULT_SCROLL_SPEED = 30; 92 | const DEFAULT_HOLD_TIME = 1; 93 | 94 | sc.TextMarqueeGui = ig.GuiElementBase.extend({ 95 | init(text: string, width: number, settings: sc.TextMarqueeGui.Settings | undefined) { 96 | this.parent(); 97 | 98 | this.textGui = new sc.TextGui(text, settings); 99 | 100 | this.addChildGui(this.textGui); 101 | 102 | this.setSize(width, this.textGui.hook.size.y); 103 | this.hook.clip = true; 104 | 105 | this.active = settings?.autoScroll || false; 106 | this.scrollSpeed = settings?.scrollSpeed || DEFAULT_SCROLL_SPEED; 107 | this.holdTime = settings?.holdTime || DEFAULT_HOLD_TIME; 108 | this.holdOnReset = settings?.holdOnReset == undefined ? true : settings.holdOnReset; 109 | 110 | this.reset(); 111 | }, 112 | 113 | setText(text) { 114 | this.textGui.setText(text); 115 | }, 116 | 117 | activate() { 118 | this.active = true; 119 | }, 120 | 121 | deactivate() { 122 | this.active = false; 123 | }, 124 | 125 | reset() { 126 | this.currentPosition = 0; 127 | if (this.holdOnReset) { 128 | this.timer = this.holdTime; 129 | this.direction = 1; 130 | } else { 131 | this.timer = 0; 132 | this.direction = -1; 133 | } 134 | this.textGui.hook.pos.x = 0; 135 | }, 136 | 137 | startTimer() { 138 | if (this.group) { 139 | this.group.setDone(this); 140 | } else { 141 | this.timer = this.holdTime; 142 | } 143 | }, 144 | 145 | addToGroup(group) { 146 | group.add(this); 147 | this.group = group; 148 | }, 149 | 150 | update() { 151 | this.parent(); 152 | 153 | if (!this.active) { 154 | return; 155 | } 156 | 157 | if (this.textGui.hook.size.x < this.hook.size.x) { 158 | this.textGui.hook.pos.x = 0; 159 | this.group?.setDone(this); 160 | return; 161 | } 162 | 163 | const prevPos = this.textGui.hook.pos.x; 164 | if (this.timer > 0) { 165 | this.timer -= ig.system.actualTick; 166 | if (this.timer > 0) { 167 | return; 168 | } else { 169 | this.direction = -this.direction; 170 | } 171 | } 172 | 173 | let nextPos = prevPos + Math.sign(this.direction) * this.scrollSpeed * ig.system.actualTick; 174 | 175 | if (this.direction < 0 && nextPos < this.hook.size.x - this.textGui.hook.size.x) { 176 | nextPos = this.hook.size.x - this.textGui.hook.size.x; 177 | this.startTimer(); 178 | } else if (this.direction > 0 && nextPos > 0) { 179 | nextPos = 0; 180 | this.startTimer(); 181 | } 182 | 183 | this.textGui.hook.pos.x = nextPos; 184 | }, 185 | }); 186 | 187 | sc.ItemMarqueeGui = ig.GuiElementBase.extend({ 188 | init(icon: string, label: string, width: number, settings?: sc.TextMarqueeGui.Settings) { 189 | this.parent(); 190 | 191 | this.iconGui = new sc.TextGui(`\\i[${icon}]`); 192 | this.addChildGui(this.iconGui); 193 | this.settings = settings || { autoScroll: true }; 194 | 195 | this.labelGui = new sc.TextMarqueeGui(label, width - 15, this.settings); 196 | this.labelGui.hook.pos.x = 15; 197 | this.addChildGui(this.labelGui); 198 | 199 | this.hook.size.x = width; 200 | this.hook.size.y = this.iconGui.hook.size.y; 201 | }, 202 | 203 | setText(text) { 204 | this.labelGui.setText(text); 205 | }, 206 | 207 | addToGroup(group) { 208 | this.labelGui.addToGroup(group); 209 | }, 210 | }); 211 | 212 | sc.MultiWorldItemMarqueeGui = sc.ItemMarqueeGui.extend({ 213 | init(itemInfo: ItemInfo, width: number, settings?: sc.TextMarqueeGui.Settings) { 214 | this.parent(itemInfo.icon, itemInfo.label, width, settings); 215 | 216 | this.itemInfo = itemInfo; 217 | 218 | this.worldGui = new sc.TextGui(itemInfo.player, { "font": sc.fontsystem.tinyFont }); 219 | this.worldGui.setPos(17, this.iconGui.hook.size.y - 2); 220 | this.addChildGui(this.worldGui); 221 | 222 | this.hook.size.y += 4; 223 | } 224 | }); 225 | 226 | sc.MarqueeGroup = ig.Class.extend({ 227 | init(active) { 228 | this.elements = []; 229 | this.active = active; 230 | this.finished = []; 231 | }, 232 | 233 | add(gui) { 234 | if (this.elements.includes(gui)) { 235 | return; 236 | } 237 | 238 | gui.active = this.active; 239 | this.elements.push(gui); 240 | }, 241 | 242 | setDone(gui) { 243 | if (this.elements.includes(gui) && !this.finished.includes(gui)) { 244 | this.finished.push(gui); 245 | } 246 | 247 | if (this.finished.length != 0 && this.finished.length != this.elements.length) { 248 | gui.deactivate(); 249 | return; 250 | } 251 | 252 | for (const el of this.elements) { 253 | el.activate(); 254 | el.timer = el.holdTime; 255 | } 256 | 257 | this.finished = []; 258 | }, 259 | 260 | activate() { 261 | for (const el of this.elements) { 262 | el.activate(); 263 | } 264 | }, 265 | 266 | deactivate() { 267 | for (const el of this.elements) { 268 | el.deactivate(); 269 | } 270 | }, 271 | 272 | reset() { 273 | for (const el of this.elements) { 274 | el.reset(); 275 | } 276 | }, 277 | }); 278 | } 279 | -------------------------------------------------------------------------------- /src/patches/multiworld-hud.ts: -------------------------------------------------------------------------------- 1 | import type MwRandomizer from '../plugin'; 2 | import { ItemInfo } from '../item-data.model'; 3 | 4 | 5 | export function patch(plugin: MwRandomizer) { 6 | // And for my next trick I will rip off ItemContent and ItemHudGui from the base game 7 | // pls don't sue 8 | sc.MultiWorldItemContent = ig.GuiElementBase.extend({ 9 | timer: 0, 10 | id: -1, 11 | player: -1, 12 | textGui: null, 13 | init: function (item: ItemInfo, receive: boolean) { 14 | this.parent(this); 15 | this.timer = 5; 16 | 17 | let verb = receive ? "Received" : "Sent"; 18 | let prep = receive ? "from": "to"; 19 | let text = `${verb} \\c[3]${plugin.getGuiString(item)}\\c[0] ${prep} \\c[3]${item.player}\\c[0]`; 20 | let isNormalSize = sc.options.get("item-hud-size") == sc.ITEM_HUD_SIZE.NORMAL; 21 | 22 | this.textGui = new sc.TextGui(text, { 23 | speed: ig.TextBlock.SPEED.IMMEDIATE, 24 | font: isNormalSize ? sc.fontsystem.font : sc.fontsystem.smallFont, 25 | }); 26 | this.textGui.setAlign(ig.GUI_ALIGN.X_LEFT, ig.GUI_ALIGN.Y_CENTER); 27 | this.addChildGui(this.textGui); 28 | 29 | this.setSize( 30 | this.textGui.hook.size.x + 4, 31 | isNormalSize ? 18 : 8 32 | ); 33 | 34 | this.hook.pivot.x = this.hook.size.x; 35 | this.hook.pivot.y = 0; 36 | }, 37 | 38 | updateOption: function (isNormalSize: boolean) { 39 | if (isNormalSize) { 40 | if (this.textGui.font == sc.fontsystem.font) return; 41 | this.textGui.setFont(sc.fontsystem.font); 42 | } else { 43 | if (this.textGui.font == sc.fontsystem.smallFont) return; 44 | this.textGui.setFont(sc.fontsystem.smallFont); 45 | } 46 | 47 | this.setSize( 48 | this.textGui.hook.size.x + 4, 49 | isNormalSize ? 18 : 8 50 | ); 51 | }, 52 | 53 | updateTimer: function () { 54 | if (this.timer > 0) this.timer = this.timer - ig.system.tick; 55 | }, 56 | }); 57 | 58 | sc.MultiWorldHudBox = sc.RightHudBoxGui.extend({ 59 | contentEntries: [], 60 | delayedStack: [], 61 | size: 0, 62 | 63 | init: function() { 64 | this.parent("Archipelago"); 65 | this.size = sc.options.get("item-hud-size"); 66 | sc.Model.addObserver(sc.multiworld, this); 67 | sc.Model.addObserver(sc.model, this); 68 | sc.Model.addObserver(sc.options, this); 69 | }, 70 | 71 | addEntry: function (itemInfo: ItemInfo, receive: boolean) { 72 | let entry = new sc.MultiWorldItemContent(itemInfo, receive); 73 | if (this.contentEntries.length >= 5) { 74 | this.delayedStack.push(entry); 75 | } else { 76 | this.pushContent(entry, true); 77 | } 78 | this.hidden && this.show(); 79 | }, 80 | 81 | update: function () { 82 | if (!sc.model.isPaused() && !sc.model.isMenu() && !this.hidden) { 83 | for (let i = this.contentEntries.length, gui = null; i--; ) { 84 | gui = this.contentEntries[i].subGui; 85 | gui.updateTimer(); 86 | 87 | if (gui.timer <= 0) { 88 | gui = this.removeContent(i); 89 | if (i == 0 && this.contentEntries.length == 0) 90 | gui.hook.pivot.y = gui.hook.size.y / 2; 91 | else { 92 | gui.hook.pivot.y = 0; 93 | gui.hook.anim.timeFunction = KEY_SPLINES.EASE_OUT; 94 | } 95 | this._popDelayed(); 96 | } 97 | } 98 | 99 | !this.hidden && this.contentEntries.length == 0 && this.hide(); 100 | } 101 | }, 102 | 103 | _popDelayed: function () { 104 | if (this.delayedStack.length != 0) { 105 | var b = this.delayedStack.splice(0, 1)[0]; 106 | this.pushContent(b, true); 107 | } 108 | }, 109 | 110 | _updateSizes: function (isNormalSize: boolean) { 111 | for (var i = this.contentEntries.length, gui = null; i--; ) { 112 | gui = this.contentEntries[i]; 113 | gui.subGui.updateOption(isNormalSize); 114 | gui.setContent(gui.subGui); 115 | } 116 | this.rearrangeContent(); 117 | }, 118 | 119 | modelChanged: function (model, msg: number, data: any) { 120 | if (model == sc.multiworld) { 121 | if ( 122 | msg == sc.MULTIWORLD_MSG.ITEM_SENT && 123 | sc.options.get("show-items") 124 | ) { 125 | this.addEntry(plugin.getItemInfo(data), false); 126 | } else if ( 127 | msg == sc.MULTIWORLD_MSG.ITEM_RECEIVED && 128 | sc.options.get("show-items") 129 | ) { 130 | this.addEntry(plugin.getItemInfo(data), true); 131 | } 132 | } else if (model == sc.model) { 133 | if (sc.model.isReset()) { 134 | this.clearContent(); 135 | this.delayedStack.length = 0; 136 | this.hide(); 137 | } else if ( 138 | sc.model.isCutscene() || 139 | sc.model.isHUDBlocked() || 140 | sc.quests.hasQuestSolvedDialogs() 141 | ) { 142 | this.hide() 143 | } else if ( 144 | !sc.model.isCutscene() && 145 | !sc.model.isHUDBlocked() && 146 | this.contentEntries.length > 0 && 147 | !sc.quests.hasQuestSolvedDialogs() 148 | ) { 149 | this.show(); 150 | } 151 | } else if (model == sc.options && msg == sc.OPTIONS_EVENT.OPTION_CHANGED) { 152 | const itemHudSize = sc.options.get("item-hud-size"); 153 | if (itemHudSize != this.size) { 154 | this._updateSizes(itemHudSize == sc.ITEM_HUD_SIZE.NORMAL); 155 | this.size = itemHudSize; 156 | } 157 | } 158 | }, 159 | }); 160 | } 161 | -------------------------------------------------------------------------------- /src/patches/multiworld-model.ts: -------------------------------------------------------------------------------- 1 | import { defineVarProperty } from "../utils"; 2 | import * as ap from "archipelago.js"; 3 | import { MultiworldOptions } from "../types/multiworld-model"; 4 | import MwRandomizer from "../plugin"; 5 | 6 | export function patch(plugin: MwRandomizer) { 7 | sc.MULTIWORLD_MSG = { 8 | CONNECTION_STATUS_CHANGED: 0, 9 | ITEM_SENT: 1, 10 | ITEM_RECEIVED: 2, 11 | OPTIONS_PRESENT: 3, 12 | }; 13 | 14 | sc.MultiWorldModel = ig.GameAddon.extend({ 15 | observers: [], 16 | client: null, 17 | previousConnectionStatus: ap.CONNECTION_STATUS.DISCONNECTED, 18 | 19 | baseId: 3235824000, 20 | baseNormalItemId: 3235824100, 21 | dynamicItemAreaOffset: 100000, 22 | baseDynamicItemId: 3235924000, 23 | numItems: 0, 24 | 25 | init() { 26 | this.client = new ap.Client(); 27 | ig.storage.register(this); 28 | this.numItems = 676; 29 | 30 | defineVarProperty(this, "connectionInfo", "mw.connectionInfo"); 31 | defineVarProperty(this, "lastIndexSeen", "mw.lastIndexSeen"); 32 | defineVarProperty(this, "locationInfo", "mw.locationInfo"); 33 | defineVarProperty(this, "localCheckedLocations", "mw.checkedLocations"); 34 | defineVarProperty(this, "mode", "mw.mode"); 35 | defineVarProperty(this, "options", "mw.options"); 36 | defineVarProperty(this, "progressiveChainProgress", "mw.progressiveChainProgress"); 37 | defineVarProperty(this, "receivedItemMap", "mw.received"); 38 | 39 | window.setInterval(this.updateConnectionStatus.bind(this), 300); 40 | }, 41 | 42 | getElementConstantFromComboId(comboId: number): number | null { 43 | switch (comboId) { 44 | case this.baseId: 45 | return sc.PLAYER_CORE.ELEMENT_HEAT; 46 | case this.baseId + 1: 47 | return sc.PLAYER_CORE.ELEMENT_COLD; 48 | case this.baseId + 2: 49 | return sc.PLAYER_CORE.ELEMENT_SHOCK; 50 | case this.baseId + 3: 51 | return sc.PLAYER_CORE.ELEMENT_WAVE; 52 | default: 53 | return null; 54 | } 55 | }, 56 | 57 | getShopLabelsFromItemData(item: ap.NetworkItem): sc.ListBoxButton.Data { 58 | let rarityString = "Looks like junk..."; 59 | 60 | if (item.flags & ap.ITEM_FLAGS.NEVER_EXCLUDE) { 61 | rarityString = "\\c[2]Looks helpful\\c[0]."; 62 | } 63 | 64 | if (item.flags & ap.ITEM_FLAGS.PROGRESSION) { 65 | rarityString = "\\c[3]Looks important\\c[0]!"; 66 | } 67 | 68 | if (item.flags & ap.ITEM_FLAGS.TRAP) { 69 | rarityString = "\\c[1]Looks dangerous\\c[0]."; 70 | } 71 | 72 | if (sc.multiworld.client.players.get(item.player)?.game == "CrossCode") { 73 | if (item.item >= sc.multiworld.baseNormalItemId && item.item < sc.multiworld.baseDynamicItemId) { 74 | const [internalItem, internalQty] = sc.multiworld.getItemDataFromComboId(item.item); 75 | const internalData = sc.inventory.getItem(internalItem); 76 | if (internalData != undefined) { 77 | return { 78 | id: internalItem, 79 | description: ig.LangLabel.getText(internalData.description), 80 | }; 81 | } 82 | } 83 | 84 | if (sc.randoData.descriptions[item.item] != undefined) { 85 | return { 86 | id: 0, 87 | description: ig.LangLabel.getText(sc.randoData.descriptions[item.item]), 88 | } 89 | } 90 | 91 | return { 92 | id: 0, 93 | description: "An unknown CrossCode item. " + rarityString, 94 | }; 95 | } 96 | 97 | return { 98 | id: 0, 99 | description: "An item for another world. " + rarityString, 100 | }; 101 | }, 102 | 103 | getItemDataFromComboId(comboId: number): [itemId: number, quantity: number] { 104 | if (this.numItems == 0) { 105 | throw "Can't fetch item data before item database is loaded"; 106 | } 107 | 108 | comboId -= this.baseNormalItemId; 109 | return [comboId % this.numItems, (comboId / this.numItems + 1) | 0]; 110 | }, 111 | 112 | onStoragePostLoad() { 113 | if (this.client.status != "Connected") { 114 | if (this.connectionInfo) { 115 | console.log("Reading connection info from save file"); 116 | this.login(this.connectionInfo); 117 | } else { 118 | sc.Dialogs.showInfoDialog( 119 | "This save file has no Archipelago connection associated with it. " + 120 | "To play online, open the pause menu and enter the details.", 121 | true, 122 | ); 123 | } 124 | } 125 | }, 126 | 127 | onLevelLoaded() { 128 | if (this.lastIndexSeen == null) { 129 | this.lastIndexSeen = -1; 130 | } 131 | 132 | if (!this.localCheckedLocations) { 133 | this.localCheckedLocations = []; 134 | } 135 | 136 | if (!this.progressiveChainProgress) { 137 | this.progressiveChainProgress = {}; 138 | } 139 | 140 | if (!this.receivedItemMap) { 141 | this.receivedItemMap = {}; 142 | } 143 | 144 | if (sc.model.isTitle() || ig.game.mapName == "newgame") { 145 | return; 146 | } 147 | 148 | for (let i = this.lastIndexSeen + 1; i < this.client.items.received.length; i++) { 149 | let item = this.client.items.received[i]; 150 | this.addMultiworldItem(item, i); 151 | } 152 | 153 | let area = ig.game.mapName.split(".")[0]; 154 | 155 | if (this.client.status == ap.CONNECTION_STATUS.CONNECTED) { 156 | this.client.send({ 157 | cmd: "Set", 158 | key: "area", 159 | default: "rookie-harbor", 160 | want_reply: false, 161 | operations: [ 162 | { 163 | operation: "replace", 164 | value: area, 165 | } 166 | ] 167 | }); 168 | } 169 | }, 170 | 171 | notifyItemsSent(items: ap.NetworkItem[]) { 172 | for (const item of items) { 173 | if (item.player == this.client.data.slot) { 174 | continue; 175 | } 176 | sc.Model.notifyObserver(this, sc.MULTIWORLD_MSG.ITEM_SENT, item); 177 | } 178 | }, 179 | 180 | updateConnectionStatus() { 181 | if (this.previousConnectionStatus == this.client.status) { 182 | return; 183 | } 184 | 185 | this.previousConnectionStatus = this.client.status; 186 | 187 | sc.Model.notifyObserver(this, sc.MULTIWORLD_MSG.CONNECTION_STATUS_CHANGED, this.client.status); 188 | }, 189 | 190 | addMultiworldItem(itemInfo: ap.NetworkItem, index: number): void { 191 | if (index <= this.lastIndexSeen) { 192 | return; 193 | } 194 | 195 | const foreign = itemInfo.player != this.client.data.slot; 196 | 197 | let displayMessage = foreign || itemInfo.item < this.baseNormalItemId; 198 | 199 | if (this.receivedItemMap[itemInfo.item]) { 200 | this.receivedItemMap[itemInfo.item] += 1; 201 | } else { 202 | this.receivedItemMap[itemInfo.item] = 1; 203 | } 204 | 205 | if (itemInfo.item < this.baseId + 4) { 206 | if (!sc.model.player.getCore(sc.PLAYER_CORE.ELEMENT_CHANGE)) { 207 | sc.model.player.setCore(sc.PLAYER_CORE.ELEMENT_CHANGE, true); 208 | sc.model.player.setCore(sc.PLAYER_CORE.ELEMENT_HEAT, false); 209 | sc.model.player.setCore(sc.PLAYER_CORE.ELEMENT_COLD, false); 210 | sc.model.player.setCore(sc.PLAYER_CORE.ELEMENT_WAVE, false); 211 | sc.model.player.setCore(sc.PLAYER_CORE.ELEMENT_SHOCK, false); 212 | } 213 | let elementConstant = this.getElementConstantFromComboId(itemInfo.item); 214 | if (elementConstant != null) { 215 | sc.model.player.setCore(elementConstant, true); 216 | } 217 | } else if (this.options.progressiveChains[itemInfo.item]) { 218 | if (!this.progressiveChainProgress[itemInfo.item]) { 219 | this.progressiveChainProgress[itemInfo.item] = 0; 220 | } 221 | const chain = this.options.progressiveChains[itemInfo.item]; 222 | const itemIdToGive = chain[this.progressiveChainProgress[itemInfo.item]++]; 223 | if (itemIdToGive != undefined) { 224 | const copiedItem = {...itemInfo}; 225 | copiedItem.item = itemIdToGive; 226 | this.addMultiworldItem(copiedItem, index); 227 | } 228 | 229 | displayMessage = false; 230 | } else if (itemInfo.item < this.baseNormalItemId) { 231 | switch (this.gamepackage.item_id_to_name[itemInfo.item]) { 232 | case "SP Upgrade": 233 | sc.model.player.setSpLevel(Number(sc.model.player.spLevel) + 1); 234 | sc.party.currentParty.forEach((name: string) => { 235 | sc.party.getPartyMemberModel(name).setSpLevel(sc.model.player.spLevel); 236 | }); 237 | 238 | break; 239 | } 240 | } else if (itemInfo.item < this.baseDynamicItemId) { 241 | let [itemId, quantity] = this.getItemDataFromComboId(itemInfo.item); 242 | if (this.options.keyrings && this.options.keyrings.includes(itemId)) { 243 | quantity = 99; 244 | } 245 | sc.model.player.addItem(Number(itemId), quantity, foreign); 246 | } else { 247 | displayMessage = true; 248 | } 249 | 250 | if (displayMessage) { 251 | sc.Model.notifyObserver(this, sc.MULTIWORLD_MSG.ITEM_RECEIVED, itemInfo); 252 | } 253 | 254 | this.lastIndexSeen = index; 255 | }, 256 | 257 | getLocationInfo(mode: ap.CreateAsHintMode, locations: number[], callback: (info: ap.NetworkItem[]) => void) { 258 | let listener = (packet: ap.LocationInfoPacket) => { 259 | let matches = true; 260 | for (let i = 0; i < locations.length; i++) { 261 | if (packet.locations[i].location != locations[i]) { 262 | matches = false; 263 | break; 264 | } 265 | } 266 | 267 | if (!matches) { 268 | return; 269 | } 270 | 271 | this.client.removeListener("LocationInfo", listener); 272 | 273 | callback(packet.locations); 274 | }; 275 | 276 | this.client.addListener('LocationInfo', listener); 277 | 278 | // The following function's definition is broken, so I ignore the error. 279 | // @ts-ignore 280 | this.client.locations.scout(mode, ...locations); 281 | }, 282 | 283 | async storeAllLocationInfo() { 284 | let listener = (packet: ap.LocationInfoPacket) => { 285 | let locationInfoMap = ig.vars.get("mw.locationInfo"); 286 | packet.locations.forEach((item: any) => { 287 | let mwid: number = item.location; 288 | 289 | // cut down on save file space by not storing unimportant parts 290 | // item.location is redundant because you'll have the key whenever that's relevant 291 | // item.class is a string which is the same for every instance 292 | // together this saves several kilobytes of space in the save data 293 | delete item.location; 294 | delete item.class; 295 | // @ts-ignore 296 | locationInfoMap[mwid] = item; 297 | }); 298 | 299 | this.client.removeListener("LocationInfo", listener); 300 | }; 301 | 302 | this.client.addListener('LocationInfo', listener); 303 | 304 | // In case the file was loaded on a previous version, we need to add the checked locations too. 305 | // This might be able to go away once there is version checking. 306 | let toScout: number[] = this.client.locations.missing 307 | .concat(this.client.locations.checked); 308 | 309 | if (!this.locationInfo) { 310 | this.locationInfo = {}; 311 | } else { 312 | toScout = toScout.filter((mwid: number) => !this.locationInfo.hasOwnProperty(mwid)); 313 | 314 | if (toScout.length >= 1) { 315 | console.warn(`Need to scout following locations:\n${toScout.join('\n')}`); 316 | } 317 | } 318 | 319 | this.client.locations.scout( 320 | ap.CREATE_AS_HINT_MODE.NO_HINT, 321 | ...toScout 322 | ); 323 | }, 324 | 325 | async reallyCheckLocation(mwid: number) { 326 | this.client.locations.check(mwid); 327 | 328 | let loc = this.locationInfo[mwid]; 329 | if (loc == undefined) { 330 | this.getLocationInfo(ap.CREATE_AS_HINT_MODE.NO_HINT, [mwid], sc.multiworld.notifyItemsSent.bind(sc.multiworld)); 331 | } else { 332 | sc.multiworld.notifyItemsSent([loc]); 333 | } 334 | 335 | if (this.localCheckedLocations.indexOf(mwid) >= 0) { 336 | return; 337 | } 338 | 339 | this.localCheckedLocations.push(mwid); 340 | }, 341 | 342 | async reallyCheckLocations(mwids: number[]) { 343 | for (const mwid of mwids) { 344 | this.reallyCheckLocation(mwid); 345 | } 346 | }, 347 | 348 | async login(info: ap.ConnectionInformation) { 349 | try { 350 | info.version = { 351 | major: 0, 352 | minor: 5, 353 | build: 0, 354 | }; 355 | await this.client.connect(info); 356 | } catch (e) { 357 | sc.Dialogs.showErrorDialog( 358 | "Could not connect to Archipelago server. " + 359 | "You may still be able to play if you have logged in to this server before, " + 360 | "but your progress will not be uploaded until " + 361 | "you connect to the server.", 362 | true 363 | ); 364 | console.error("Could not connect to Archipelago server: ", e); 365 | 366 | return; 367 | } 368 | 369 | this.gamepackage = this.client.data.package.get("CrossCode")!; 370 | 371 | this.client.addListener('ReceivedItems', (packet: ap.ReceivedItemsPacket) => { 372 | if (!ig.game.mapName || ig.game.mapName == "newgame") { 373 | return; 374 | } 375 | let index = packet.index; 376 | for (const [offset, itemInfo] of packet.items.entries()) { 377 | this.addMultiworldItem(itemInfo, index + offset); 378 | } 379 | }); 380 | 381 | this.connectionInfo = info; 382 | 383 | // this is always going to be a string 384 | this.mode = this.client.data.slotData.mode as unknown as string; 385 | this.options = this.client.data.slotData.options as unknown as MultiworldOptions; 386 | 387 | const obfuscationLevel = this.options.hiddenQuestObfuscationLevel; 388 | 389 | this.questSettings = { 390 | hidePlayer: obfuscationLevel == "hide_text" || obfuscationLevel == "hide_all", 391 | hideIcon: obfuscationLevel == "hide_all" 392 | }; 393 | 394 | sc.multiworld.onLevelLoaded(); 395 | 396 | sc.Model.notifyObserver(sc.multiworld, sc.MULTIWORLD_MSG.OPTIONS_PRESENT); 397 | 398 | this.storeAllLocationInfo(); 399 | 400 | let checkedSet = new Set(this.client.locations.checked); 401 | 402 | for (const location of this.localCheckedLocations) { 403 | if (!checkedSet.has(location)) { 404 | this.reallyCheckLocation(location); 405 | } 406 | } 407 | }, 408 | }); 409 | 410 | ig.addGameAddon(() => { 411 | return (sc.multiworld = new sc.MultiWorldModel()); 412 | }); 413 | } 414 | -------------------------------------------------------------------------------- /src/patches/new-game.ts: -------------------------------------------------------------------------------- 1 | import type MwRandomizer from "../plugin"; 2 | import "ultimate-crosscode-typedefs"; 3 | 4 | declare global { 5 | namespace sc { 6 | interface NewGameModeSelectDialog { 7 | apGfx: ig.Image; 8 | apGui: sc.NewGameModeDialogButton; 9 | oldCallback: this["callback"]; 10 | } 11 | } 12 | } 13 | 14 | export function patch(plugin: MwRandomizer) { 15 | sc.NewGameModeSelectDialog.inject({ 16 | init(...args) { 17 | this.parent(...args); 18 | 19 | this.content.hook.size.x += 110; 20 | this.msgBox.hook.size.x += 110; 21 | this.msgBox.centerBox.hook.size.x += 110; 22 | 23 | this.apGfx = new ig.Image("media/gui/archipelago-start.png"); 24 | 25 | this.apGui = new sc.NewGameModeDialogButton( 26 | "Archipelago", 27 | 2 28 | ); 29 | 30 | this.apGui.hook.pos.y = 27; 31 | this.apGui.hook.align.x = ig.GUI_ALIGN.X_CENTER; 32 | this.apGui.image.setImage(this.apGfx, 0, 0, 110, 90); 33 | this.content.addChildGui(this.apGui); 34 | 35 | this.buttongroup.removeFocusGui(1, 0); 36 | this.buttongroup.addFocusGui(this.apGui, 1, 0); 37 | this.buttongroup.addFocusGui(this.plus, 2, 0); 38 | 39 | this.buttongroup.addSelectionCallback((gui) => { 40 | if ((gui as sc.ButtonGui)?.data == 2) { 41 | this.info.doStateTransition("DEFAULT", true); 42 | this.info.setText("Connect to a multiworld and play with friends!"); 43 | } 44 | }); 45 | 46 | this.oldCallback = this.callback 47 | this.callback = (gui) => { 48 | this.oldCallback(gui, this); 49 | if (gui.data == 2) { // gui.data == 2 means Archipelago Start 50 | // First, open the AP connection screen 51 | sc.menu.setDirectMode(true, sc.MENU_SUBMENU.AP_CONNECTION); 52 | // Set a callback for when we exit that screen. 53 | sc.menu.exitCallback = () => { 54 | if (sc.multiworld.connectionInfo && sc.newgame.active) { 55 | // Because the exit callback is created in the scope of an instance of sc.TitleScreenButtonGui, and because 56 | // that instance is not given an identifier anywhere, we have to search for it! 57 | // The way we do this is by looping through the nameless GUIs and checking if any of them have a child named 58 | // buttons and then whether that has a child named changelogGui. This is unique enough to single it down to one 59 | // instance. 60 | const titleScreenButtons = (ig.gui.guiHooks.filter( 61 | x => (x.gui as sc.TitleScreenGui).buttons?.changelogGui 62 | )[0].gui as sc.TitleScreenGui).buttons; 63 | 64 | titleScreenButtons.changelogGui.clearLogs(); 65 | ig.bgm.clear("MEDIUM_OUT"); 66 | // c && c.stop(); 67 | // c = null; 68 | ig.interact.removeEntry(titleScreenButtons.buttonInteract); 69 | ig.game.start(sc.START_MODE.NEW_GAME_PLUS, 1); 70 | } else { 71 | sc.newgame.onReset?.(); 72 | } 73 | } 74 | sc.model.enterMenu(true); 75 | } 76 | } 77 | } 78 | }); 79 | 80 | sc.NewGamePlusModel.inject({ 81 | options: { "rhombus-start": true }, 82 | onReset() { 83 | this.options = { "rhombus-start": true }, 84 | this.active = false; 85 | } 86 | }); 87 | 88 | sc.NewGamePlusMenu.inject({ 89 | init() { 90 | this.parent(); 91 | this.button.setActive(sc.newgame.hasAnyOptions()); 92 | }, 93 | }); 94 | 95 | sc.NewGameToggleSet.inject({ 96 | updateActiveState( 97 | totalPoints: number, 98 | newGameCost: number, 99 | remainingCredits: number, 100 | ) { 101 | this.parent(totalPoints, newGameCost, remainingCredits); 102 | 103 | for (const button of this.buttons) { 104 | if (button.data.id == "rhombus-start") { 105 | button.setActive(false); 106 | } 107 | } 108 | } 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /src/patches/quest.ts: -------------------------------------------------------------------------------- 1 | import * as ap from "archipelago.js"; 2 | import type MwRandomizer from "../plugin"; 3 | import { RawQuest, RawQuests } from "../item-data.model"; 4 | import { getElementIconString } from "../utils"; 5 | 6 | declare global { 7 | namespace sc { 8 | interface QuestDialog extends sc.Model.Observer { 9 | finished: boolean; 10 | mwQuest: RawQuest; 11 | newItemsGui: sc.MultiWorldQuestItemBox; 12 | } 13 | interface QuestDetailsView { 14 | newItemsGui: sc.MultiWorldQuestItemBox; 15 | } 16 | } 17 | } 18 | 19 | export function patch(plugin: MwRandomizer) { 20 | let mwQuests: RawQuests = plugin.randoData?.quests; 21 | 22 | function getRawQuestFromQuestId(questId: string) { 23 | let mwQuest = mwQuests[questId]; 24 | if ( 25 | mwQuest === undefined || 26 | mwQuest.mwids === undefined || 27 | mwQuest.mwids.length === 0 || 28 | sc.multiworld.locationInfo[mwQuest.mwids[0]] === undefined 29 | ) { 30 | return undefined; 31 | } 32 | return mwQuest; 33 | } 34 | 35 | sc.QuestModel.inject({ 36 | _collectRewards(quest: sc.Quest) { 37 | const previousItemAmounts: Record = {}; 38 | 39 | if (quest.rewards.items) { 40 | for (const item of quest.rewards.items) { 41 | previousItemAmounts[item.id] = sc.model.player.getItemAmount(item.id); 42 | } 43 | } 44 | 45 | this.parent(quest); 46 | 47 | if (quest.rewards.items) { 48 | for (const item of quest.rewards.items) { 49 | const toTakeAway = sc.model.player.getItemAmount(item.id) - previousItemAmounts[item.id]; 50 | 51 | if (toTakeAway > 0) { 52 | sc.model.player.removeItem(item.id, toTakeAway); 53 | } 54 | } 55 | } 56 | 57 | const check = getRawQuestFromQuestId(quest.id); 58 | if (check == undefined) return this.parent(quest); 59 | 60 | sc.multiworld.reallyCheckLocations(check.mwids); 61 | }, 62 | }); 63 | 64 | sc.QuestDialogWrapper.inject({ 65 | init( 66 | quest: sc.Quest, 67 | callback, 68 | finished, 69 | characterName, 70 | mapName, 71 | ) { 72 | this.parent(quest, callback, finished, characterName, mapName); 73 | 74 | this.buttons.hook.pos.y = finished ? 22 : 23; 75 | this.questBox.hook.pos.y -= 1; 76 | this.questBox.hook.size.y += 10; 77 | 78 | if (this.overlay) { 79 | this.questBox.removeChildGui(this.overlay); 80 | this.overlay = new ig.BoxGui(281, this.questBox.hook.size.y, false, this.questBox.ninepatch); 81 | this.overlay.hook.transitions = { 82 | DEFAULT: { state: {}, time: 0.2, timeFunction: KEY_SPLINES.LINEAR }, 83 | HIDDEN: { 84 | state: { alpha: 0 }, 85 | time: 0.2, 86 | timeFunction: KEY_SPLINES.LINEAR, 87 | }, 88 | }; 89 | this.overlay.doStateTransition("HIDDEN", true); 90 | this.questBox.addChildGui(this.overlay); 91 | } 92 | 93 | sc.Model.addObserver(sc.multiworld, this.questBox); 94 | }, 95 | 96 | _close(a) { 97 | this.parent(a); 98 | 99 | sc.Model.removeObserver(sc.multiworld, this.questBox); 100 | }, 101 | }); 102 | 103 | sc.QuestDialog.inject({ 104 | init(quest: sc.Quest, finished: boolean) { 105 | this.parent(quest, finished); 106 | 107 | this.finished = finished; 108 | }, 109 | 110 | setQuestRewards(quest: sc.Quest, hideRewards: boolean, finished: boolean) { 111 | this.parent(quest, hideRewards, finished); 112 | let mwQuest = getRawQuestFromQuestId(quest.id); 113 | if (mwQuest == undefined) return; 114 | 115 | this.removeChildGui(this.itemsGui); 116 | if (this.newItemsGui) { 117 | this.removeChildGui(this.newItemsGui); 118 | } 119 | 120 | this.newItemsGui = new sc.MultiWorldQuestItemBox( 121 | 142, 122 | finished ? 65 : 88, 123 | quest, 124 | mwQuest, 125 | finished, 126 | false 127 | ); 128 | 129 | this.newItemsGui.setPos(124, finished ? 181 : 158); 130 | this.addChildGui(this.newItemsGui); 131 | }, 132 | 133 | modelChanged(model: sc.Model, msg: number, data: any) { 134 | if ( 135 | model == sc.multiworld && 136 | msg == sc.MULTIWORLD_MSG.CONNECTION_STATUS_CHANGED && 137 | this.mwQuest && 138 | sc.multiworld.locationInfo[this.mwQuest.mwids[0]] === undefined 139 | ) { 140 | this.newItemsGui.setQuest(this.mwQuest); 141 | } 142 | } 143 | }); 144 | 145 | sc.QuestDetailsView.inject({ 146 | _setQuest(quest: sc.Quest) { 147 | this.parent(quest); 148 | 149 | let mwQuest = getRawQuestFromQuestId(quest.id); 150 | if (mwQuest == undefined) return; 151 | 152 | this.removeChildGui(this.itemsGui); 153 | if (this.newItemsGui) { 154 | this.removeChildGui(this.newItemsGui); 155 | } 156 | this.atCurLevelGui.doStateTransition("HIDDEN", true); 157 | 158 | this.newItemsGui = new sc.MultiWorldQuestItemBox( 159 | 150, 160 | 110, 161 | quest, 162 | mwQuest, 163 | false, 164 | true 165 | ); 166 | 167 | let y = 160; 168 | if (quest.rewards.exp) { 169 | y += 16; 170 | } 171 | if (quest.rewards.money) { 172 | y += 16; 173 | } 174 | if (quest.rewards.cp) { 175 | y += 16; 176 | } 177 | 178 | this.newItemsGui.setPos(21, y); 179 | this.addChildGui(this.newItemsGui); 180 | } 181 | }); 182 | 183 | sc.QuestHubListEntry.inject({ 184 | init(questId: string, tabIndex: number) { 185 | this.parent(questId, tabIndex); 186 | 187 | // Remove vanilla text icons from the rewards hook, and 188 | // offset AP icons' X position with the image icons we're keeping 189 | let rewardIcons = this.rewards.hook.children; 190 | let apIconX = 10; 191 | for (let i = rewardIcons.length - 1; i >= 1; i--) { 192 | let currentIcon = rewardIcons[i].gui; 193 | if ('font' in currentIcon) { 194 | this.rewards.hook.removeChildHookByIndex(i); 195 | // @ts-ignore 196 | } else if (currentIcon.offsetX) { 197 | // @ts-ignore 198 | switch (currentIcon.offsetX) { 199 | case 472: // Experience 200 | apIconX += 17; 201 | break; 202 | case 593: // Circuit Points 203 | apIconX += 13; 204 | break; 205 | case 488: // Credits 206 | apIconX += 15; 207 | break; 208 | } 209 | } 210 | } 211 | 212 | // @ts-ignore 213 | let quest = sc.quests.getStaticQuest(questId); 214 | let mwQuest = getRawQuestFromQuestId(questId); 215 | if (mwQuest == undefined) return; 216 | 217 | for (let i = 0; i < mwQuest.mwids.length; i++) { 218 | const mwid: number = mwQuest.mwids[i]; 219 | const item: ap.NetworkItem = sc.multiworld.locationInfo[mwid]; 220 | 221 | const itemInfo = plugin.getItemInfo(item); 222 | 223 | let icon = "ap-logo"; 224 | let useGenericIcon = true; 225 | const hiddenQuestRewardMode = sc.multiworld.options.hiddenQuestRewardMode; 226 | if (hiddenQuestRewardMode == "show_all") { 227 | useGenericIcon = false; 228 | } else if ( 229 | hiddenQuestRewardMode == "hide_all" || 230 | (hiddenQuestRewardMode == "vanilla" && quest.hideRewards) 231 | ) { 232 | useGenericIcon = sc.multiworld.options.hiddenQuestObfuscationLevel == "hide_all"; 233 | } else { 234 | useGenericIcon = false; 235 | } 236 | 237 | if (!useGenericIcon) { 238 | icon = itemInfo.icon; 239 | } 240 | 241 | if (icon.startsWith("ap-")) { 242 | apIconX -= 1; 243 | } 244 | 245 | const apIcon = new sc.TextGui(`\\i[${icon}]`); 246 | 247 | if (itemInfo.level > 0) { 248 | apIcon.setDrawCallback((width: number, height: number) => { 249 | sc.MenuHelper.drawLevel( 250 | itemInfo.level, 251 | width, 252 | height, 253 | this.gfx, 254 | itemInfo.isScalable 255 | ); 256 | }); 257 | } 258 | apIcon.setPos(apIconX, 10); 259 | this.rewards.addChildGui(apIcon); 260 | apIconX += 16; 261 | } 262 | } 263 | }); 264 | 265 | sc.MultiWorldQuestItemBox = ig.GuiElementBase.extend({ 266 | gfx: new ig.Image("media/gui/menu.png"), 267 | init( 268 | width: number, 269 | height: number, 270 | quest: sc.Quest, 271 | mwQuest: RawQuest, 272 | showRewardAnyway: boolean, 273 | includeAllRewards: boolean, 274 | ) { 275 | this.parent(); 276 | 277 | if (sc.multiworld.client.status != ap.CONNECTION_STATUS.CONNECTED) { 278 | return; 279 | } 280 | this.setSize(width, height); 281 | 282 | const hiddenQuestRewardMode = sc.multiworld.options.hiddenQuestRewardMode; 283 | let hideRewards = quest.hideRewards; 284 | if (hiddenQuestRewardMode == "show_all") { 285 | hideRewards = false; 286 | } else if (hiddenQuestRewardMode == "hide_all") { 287 | hideRewards = true; 288 | } 289 | 290 | this.hideRewards = hideRewards && !showRewardAnyway; 291 | this.includeAllRewards = includeAllRewards; 292 | 293 | this.quest = quest; 294 | 295 | this.setQuest(mwQuest); 296 | }, 297 | 298 | setQuest(mwQuest: RawQuest) { 299 | if (sc.multiworld.options.questDialogHints && !this.hideRewards) { 300 | const toHint = mwQuest.mwids.filter(mwid => 301 | sc.multiworld.locationInfo[mwid] != undefined && 302 | sc.multiworld.locationInfo[mwid].flags & ap.ITEM_FLAGS.PROGRESSION 303 | ); 304 | 305 | if (toHint.length > 0) { 306 | sc.multiworld.client.locations.scout(ap.CREATE_AS_HINT_MODE.HINT_ONLY_NEW, ...toHint); 307 | } 308 | } 309 | 310 | this.removeAllChildren(); 311 | 312 | const marqueeGroup = new sc.MarqueeGroup(true); 313 | 314 | let accum = 0; 315 | 316 | for (let i = 0; i < mwQuest.mwids.length; i++) { 317 | const mwid: number = mwQuest.mwids[i] 318 | const item: ap.NetworkItem = sc.multiworld.locationInfo[mwid]; 319 | 320 | const itemInfo = plugin.getItemInfo(item); 321 | 322 | if (this.hideRewards) { 323 | itemInfo.label = "?????????????"; 324 | if (sc.multiworld.questSettings.hidePlayer) { 325 | itemInfo.player = "?????????????"; 326 | } 327 | 328 | if (sc.multiworld.questSettings.hideIcon) { 329 | itemInfo.icon = "ap-logo"; 330 | itemInfo.level = 0; 331 | } 332 | } 333 | 334 | const marqueeGui = new sc.MultiWorldItemMarqueeGui(itemInfo, this.hook.size.x); 335 | 336 | if (itemInfo.level > 0) { 337 | marqueeGui.iconGui.setDrawCallback((width: number, height: number) => { 338 | sc.MenuHelper.drawLevel( 339 | itemInfo.level, 340 | width, 341 | height, 342 | this.gfx, 343 | itemInfo.isScalable 344 | ); 345 | }); 346 | } 347 | 348 | marqueeGui.addToGroup(marqueeGroup); 349 | 350 | marqueeGui.setPos(0, accum); 351 | accum += marqueeGui.hook.size.y; 352 | 353 | this.addChildGui(marqueeGui); 354 | } 355 | } 356 | }); 357 | } 358 | -------------------------------------------------------------------------------- /src/patches/shop.ts: -------------------------------------------------------------------------------- 1 | import * as ap from "archipelago.js"; 2 | import type MwRandomizer from "../plugin"; 3 | 4 | declare global { 5 | namespace sc { 6 | interface ShopMenu { 7 | shopList: sc.ShopListMenu; 8 | 9 | onQuantitySubmit(this: this, button: sc.ShopItemButton, quantity: number): void; 10 | } 11 | 12 | interface ShopListMenu { 13 | menuGfx: ig.Image; 14 | shopData: Record | undefined; 15 | } 16 | 17 | interface ShopItemButton { 18 | apItem: ap.NetworkItem | undefined; 19 | itemId: number | undefined; 20 | worldGui: sc.TextGui | undefined; 21 | slot: string | undefined; 22 | lockedGui: sc.TextGui; 23 | unlockItem?: number | null; 24 | 25 | showLockedMessage(this: this): void; 26 | } 27 | } 28 | } 29 | 30 | export function patch(plugin: MwRandomizer) { 31 | sc.ShopMenu.inject({ 32 | onQuantitySubmit(button: sc.ShopItemButton, quantity: number) { 33 | if (button.apItem == undefined) { 34 | return this.parent(button, quantity); 35 | } 36 | 37 | if (quantity == 0) { 38 | return this.parent(button, quantity); 39 | } 40 | 41 | button.setCountNumber(1); 42 | sc.menu.updateCart(button.itemId!, 1, button.price); 43 | this.onQuantityBack(button); 44 | this.cart.setCheckout(true); 45 | this.shopList.updateListEntries(); 46 | }, 47 | 48 | buyItems() { 49 | this.parent(); 50 | 51 | if (this.shopList.shopData == undefined) { 52 | return false; 53 | } 54 | 55 | for (const entry of sc.menu.shopCart) { 56 | let mwid = this.shopList.shopData[entry.id as number]; 57 | if ( 58 | mwid == undefined || 59 | sc.multiworld.locationInfo[mwid] == undefined || 60 | sc.multiworld.localCheckedLocations.includes(mwid) 61 | ) { 62 | continue; 63 | } 64 | 65 | sc.model.player.removeItem(entry.id, entry.amount, true); 66 | sc.multiworld.reallyCheckLocation(mwid); 67 | } 68 | return false; 69 | }, 70 | }); 71 | 72 | sc.ShopListMenu.inject({ 73 | menuGfx: new ig.Image("media/gui/menu.png"), 74 | 75 | init() { 76 | this.parent(); 77 | this.buttongroup?.addPressCallback((rawButton) => { 78 | let button = rawButton as unknown as sc.ShopItemButton; 79 | if (!button.active) { 80 | button.showLockedMessage(); 81 | } 82 | }); 83 | }, 84 | 85 | scrapBuyList(shopItems) { 86 | this.parent(shopItems); 87 | const shopID = sc.menu.shopID; 88 | if (shopID == undefined || sc.randoData.shops == undefined) { 89 | return; 90 | } 91 | 92 | this.shopData = sc.multiworld.options.shopSendMode == "itemType" ? 93 | sc.randoData.shops.locations.perItemType : 94 | sc.randoData.shops.locations.perShop[shopID]; 95 | 96 | if (this.shopData == undefined) { 97 | return; 98 | } 99 | 100 | const toHint = []; 101 | 102 | let accum = 0; 103 | for (const entry of this.list.getChildren()) { 104 | const gui = entry.gui as unknown as sc.ShopItemButton; 105 | gui.hook.pos.y += accum; 106 | 107 | const itemId: number = gui.data.id; 108 | const mwid: number = this.shopData[itemId]; 109 | 110 | if (sc.multiworld.localCheckedLocations.includes(mwid)) { 111 | continue; 112 | } 113 | 114 | const button: sc.ButtonGui = gui.button; 115 | 116 | const item = sc.multiworld.locationInfo[mwid]; 117 | if (item == undefined) { 118 | continue; 119 | } 120 | 121 | button.removeChildGui(button.textChild); 122 | 123 | const itemInfo = plugin.getItemInfo(item); 124 | 125 | gui.apItem = item; 126 | gui.slot = itemInfo.player; 127 | gui.itemId = itemId; 128 | 129 | const marqueeGui = new sc.ItemMarqueeGui( 130 | itemInfo.icon, 131 | itemInfo.label, 132 | button.hook.size.x - 10, 133 | { autoScroll: false, holdOnReset: false } 134 | ); 135 | 136 | if ( 137 | sc.multiworld.locationInfo[mwid] != undefined && 138 | sc.multiworld.locationInfo[mwid].flags & ap.ITEM_FLAGS.PROGRESSION 139 | ) { 140 | toHint.push(mwid); 141 | } 142 | 143 | if (itemInfo.level > 0) { 144 | marqueeGui.iconGui.setDrawCallback((width: number, height: number) => { 145 | sc.MenuHelper.drawLevel( 146 | itemInfo.level, 147 | width, 148 | height, 149 | this.menuGfx, 150 | itemInfo.isScalable 151 | ); 152 | }); 153 | } 154 | 155 | button.textChild = marqueeGui; 156 | button.text = itemInfo.label; 157 | button.textChild.hook.pos.x = 5; 158 | button.addChildGui(button.textChild); 159 | 160 | gui.lockedGui = new sc.TextGui("\\i[ap-locked]"); 161 | gui.lockedGui.hook.align.x = ig.GUI_ALIGN.X_RIGHT; 162 | gui.lockedGui.setPos(28, 1); 163 | gui.addChildGui(gui.lockedGui); 164 | 165 | const worldGui = new sc.TextGui(itemInfo.player, { "font": sc.fontsystem.tinyFont }); 166 | worldGui.hook.pos.x = 22; 167 | worldGui.hook.pos.y = button.hook.size.y; 168 | accum += worldGui.hook.size.y; 169 | 170 | gui.hook.size.y += 8; 171 | 172 | gui.worldGui = worldGui; 173 | button.addChildGui(worldGui); 174 | } 175 | 176 | if (sc.multiworld.options.questDialogHints && sc.multiworld.options.shopDialogHints && toHint.length > 0) { 177 | // @ts-ignore 178 | sc.multiworld.client.locations.scout(ap.CREATE_AS_HINT_MODE.HINT_ONLY_NEW, ...toHint); 179 | } 180 | 181 | this.list.list.contentPane.hook.size.y += accum; 182 | this.list.list.recalculateScrollBars(); 183 | 184 | this.updateListEntries(); 185 | }, 186 | 187 | updateListEntries(resetCounters: boolean | undefined | null) { 188 | this.parent(resetCounters); 189 | 190 | let coinBalance = sc.menu.shopCoinMode ? sc.arena.getTotalArenaCoins() : sc.model.player.credit; 191 | 192 | const shopID = sc.menu.shopID!; 193 | 194 | if (this.shopData == undefined) { 195 | return; 196 | } 197 | 198 | for (const entry of this.list.getChildren()) { 199 | const gui = entry.gui as unknown as sc.ShopItemButton; 200 | 201 | if (gui.apItem) { 202 | const owned = sc.multiworld.localCheckedLocations.includes(this.shopData[gui.itemId!]); 203 | gui.data = sc.multiworld.getShopLabelsFromItemData(gui.apItem!); 204 | gui.owned.setNumber( 205 | owned ? 1 : 0, 206 | true 207 | ); 208 | 209 | if (owned) { 210 | gui.setActive(false); 211 | continue; 212 | } else { 213 | gui.setActive(coinBalance - sc.menu.getTotalCost() >= gui.price); 214 | } 215 | } 216 | 217 | if (gui.itemId != undefined) { 218 | let unlockItem: number | null = null; 219 | if (sc.multiworld.options.shopReceiveMode == "itemType") { 220 | unlockItem = sc.randoData.shops.unlocks.byId[gui.itemId]; 221 | } else if (sc.multiworld.options.shopReceiveMode == "shop") { 222 | unlockItem = sc.randoData.shops.unlocks.byShop[shopID]; 223 | } else if (sc.multiworld.options.shopReceiveMode == "slot") { 224 | unlockItem = sc.randoData.shops.unlocks.byShopAndId[shopID][gui.itemId]; 225 | } 226 | 227 | gui.unlockItem = unlockItem; 228 | 229 | if (unlockItem != null) { 230 | let hasUnlockItem = sc.multiworld.receivedItemMap[unlockItem] != undefined; 231 | if (hasUnlockItem) { 232 | gui.lockedGui.setText(""); 233 | } 234 | gui.setActive(gui.active && hasUnlockItem); 235 | } 236 | } 237 | } 238 | }, 239 | 240 | changeCount(direction: 1 | -1) { 241 | const gui = this.getActiveElement(); 242 | 243 | if (!gui) { 244 | return this.parent(direction); 245 | } 246 | 247 | if (gui.itemId == undefined) { 248 | return this.parent(direction); 249 | } 250 | 251 | if (!gui.active) { 252 | gui.showLockedMessage(); 253 | return; 254 | } 255 | 256 | const quantity = sc.menu.getItemQuantity(gui.itemId, gui.price); 257 | if ((quantity == 0 && direction == 1) || (quantity == 1 && direction == -1)) { 258 | this.playSound(direction, true); 259 | sc.menu.updateCart(gui.itemId, quantity + direction, gui.price); 260 | gui.setCountNumber(quantity + direction, quantity == 0); 261 | this.updateListEntries(); 262 | } 263 | } 264 | }); 265 | 266 | sc.ShopItemButton.inject({ 267 | focusGained() { 268 | this.parent(); 269 | this.button.textChild.labelGui?.activate(); 270 | 271 | if (this.worldGui != undefined && this.slot != undefined) { 272 | this.worldGui.setText(`\\c[3]${this.slot}`); 273 | } 274 | }, 275 | 276 | focusLost() { 277 | this.parent(); 278 | this.button.textChild.labelGui?.deactivate(); 279 | this.button.textChild.labelGui?.reset(); 280 | 281 | if (this.worldGui != undefined && this.slot != undefined) { 282 | this.worldGui.setText(this.slot); 283 | } 284 | }, 285 | 286 | showLockedMessage() { 287 | if (this.unlockItem != null && sc.multiworld.receivedItemMap[this.unlockItem] == undefined) { 288 | let itemName = sc.multiworld.gamepackage.item_id_to_name[this.unlockItem]; 289 | sc.menu.setInfoText(`Collect \\c[3]${itemName}\\c[0] to unlock this slot.`); 290 | sc.menu.setBuffText(""); 291 | } 292 | } 293 | }); 294 | } 295 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import * as ap from 'archipelago.js'; 2 | import {WorldData, ItemInfo} from './item-data.model'; 3 | import {readJsonFromFile} from './utils'; 4 | import {applyPatches} from "./patches/index"; 5 | 6 | import type * as _ from 'nax-module-cache/src/headers/nax/moduleCache.d.ts' 7 | 8 | declare global { 9 | namespace sc { 10 | var randoData: WorldData; 11 | var multiWorldHud: sc.MultiWorldHudBox; 12 | } 13 | } 14 | 15 | export default class MwRandomizer { 16 | baseDirectory: string; 17 | randoData!: WorldData; 18 | itemdb: any; 19 | 20 | constructor(mod: {baseDirectory: string}) { 21 | this.baseDirectory = mod.baseDirectory; 22 | } 23 | 24 | getColoredStatus(status: string) { 25 | switch (status.toLowerCase()) { 26 | case ap.CONNECTION_STATUS.CONNECTED.toLowerCase(): 27 | return `\\c[2]${status}\\c[0]`; 28 | case ap.CONNECTION_STATUS.DISCONNECTED.toLowerCase(): 29 | return `\\c[1]${status}\\c[0]`; 30 | case ap.CONNECTION_STATUS.WAITING_FOR_AUTH.toLowerCase(): 31 | case ap.CONNECTION_STATUS.CONNECTING.toLowerCase(): 32 | return `\\c[3]${status}\\c[0]`; 33 | } 34 | } 35 | 36 | getItemInfo(item: ap.NetworkItem): ItemInfo { 37 | let gameName: string = sc.multiworld.client.data.players[item.player].game; 38 | let gameInfo: ap.GamePackage = sc.multiworld.client.data.package.get(gameName)!; 39 | if (gameInfo == undefined || gameInfo.item_id_to_name[item.item] == undefined) { 40 | gameInfo = sc.multiworld.gamepackage; 41 | gameName = "CrossCode"; 42 | } 43 | 44 | if (gameInfo.item_id_to_name[item.item] == undefined) { 45 | return {icon: "ap-item-default", label: "Unknown", player: "Archipelago", level: 0, isScalable: false}; 46 | } 47 | 48 | const playerId = sc.multiworld.client.players.get(item.player); 49 | const playerName = playerId?.alias ?? playerId?.name; 50 | 51 | let label = gameInfo.item_id_to_name[item.item]; 52 | let player = playerName ? playerName : "Archipelago"; 53 | 54 | if (gameName == "CrossCode") { 55 | const comboId: number = item.item; 56 | let level = 0; 57 | let icon = "item-default"; 58 | let isScalable = false; 59 | if (comboId >= sc.multiworld.baseNormalItemId && comboId < sc.multiworld.baseDynamicItemId) { 60 | const [itemId, _] = sc.multiworld.getItemDataFromComboId(item.item); 61 | const dbEntry = sc.inventory.getItem(itemId); 62 | if (dbEntry) { 63 | icon = dbEntry.icon + sc.inventory.getRaritySuffix(dbEntry.rarity); 64 | isScalable = dbEntry.isScalable || false; 65 | if (dbEntry.type == sc.ITEMS_TYPES.EQUIP) { 66 | level = dbEntry.level; 67 | } 68 | } 69 | } 70 | 71 | return {icon, label, player, level, isScalable}; 72 | } 73 | 74 | let cls = "unknown"; 75 | if (item.flags & ap.ITEM_FLAGS.PROGRESSION) { 76 | cls = "prog"; 77 | } else if (item.flags & ap.ITEM_FLAGS.NEVER_EXCLUDE) { 78 | cls = "useful"; 79 | } else if (item.flags & ap.ITEM_FLAGS.TRAP) { 80 | cls = "trap"; 81 | } else if (item.flags == 0) { 82 | cls = "filler"; 83 | } 84 | 85 | let icon = `ap-item-${cls}`; 86 | return {icon, label, player, level: 0, isScalable: false}; 87 | } 88 | 89 | getGuiString(item: {icon: string; label: string}): string { 90 | return `\\i[${item.icon}]${item.label}`; 91 | } 92 | 93 | async prestart() { 94 | let randoData: WorldData = await readJsonFromFile(this.baseDirectory + "data/out/data.json"); 95 | this.randoData = randoData; 96 | sc.randoData = randoData; 97 | 98 | let itemdb = await readJsonFromFile("assets/data/item-database.json"); 99 | this.itemdb = itemdb; 100 | 101 | // For those times JS decides to override `this` 102 | // Used several times in the injection code 103 | let plugin = this; 104 | 105 | applyPatches(this); 106 | 107 | sc.PartyModel.inject({ 108 | addPartyMember(name: string, ...args) { 109 | this.parent(name, ...args); 110 | sc.party.getPartyMemberModel(name).setSpLevel(sc.model.player.spLevel); 111 | }, 112 | }); 113 | 114 | let mwIcons = new ig.Font(plugin.baseDirectory.substring(7) + "assets/media/font/icons-multiworld.png", 16, ig.MultiFont.ICON_START); 115 | 116 | let index = sc.fontsystem.font.iconSets.length; 117 | sc.fontsystem.font.pushIconSet(mwIcons); 118 | sc.fontsystem.font.setMapping({ 119 | "mw-item": [index, 0], 120 | "ap-logo": [index, 2], 121 | "ap-item-unknown": [index, 2], 122 | "ap-item-trap": [index, 3], 123 | "ap-item-filler": [index, 4], 124 | "ap-item-useful": [index, 5], 125 | "ap-item-prog": [index, 6], 126 | "ap-locked": [index, 7], 127 | }); 128 | 129 | sc.CrossCode.inject({ 130 | init() { 131 | this.parent(); 132 | sc.multiWorldHud = new sc.MultiWorldHudBox(); 133 | sc.gui.rightHudPanel.addHudBox(sc.multiWorldHud); 134 | }, 135 | 136 | gotoTitle(...args) { 137 | if (sc.multiworld.client.status == ap.CONNECTION_STATUS.CONNECTED) { 138 | sc.multiworld.client.disconnect(); 139 | // sc.multiworld.updateConnectionStatus(); 140 | } 141 | this.parent(...args); 142 | }, 143 | }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/types/multiworld-hud.ts: -------------------------------------------------------------------------------- 1 | import type {ItemInfo, RawQuest} from "../item-data.model"; 2 | 3 | declare global { 4 | namespace sc { 5 | interface MultiWorldItemContent extends ig.GuiElementBase { 6 | timer: number; 7 | id: number; 8 | player: number; 9 | textGui: sc.TextGui; 10 | subGui: MultiWorldItemContent 11 | 12 | updateOption(this: this, isNormalSize: boolean): void; 13 | updateTimer(this: this): void; 14 | } 15 | 16 | interface MultiWorldItemContentConstructor extends ImpactClass { 17 | new (item: ItemInfo, receive: boolean): MultiWorldItemContent; 18 | } 19 | 20 | var MultiWorldItemContent: sc.MultiWorldItemContentConstructor; 21 | 22 | interface MultiWorldHudBox extends sc.RightHudBoxGui, sc.Model.Observer { 23 | contentEntries: MultiWorldItemContent[]; 24 | delayedStack: MultiWorldItemContent[]; 25 | size: number; 26 | 27 | addEntry(this: this, itemInfo: ItemInfo, receive: boolean): void; 28 | _popDelayed(this: this): void; 29 | _updateSizes(this: this, isNormalSize: boolean): void; 30 | } 31 | 32 | interface MultiWorldHudBoxConstructor extends ImpactClass { 33 | new (): MultiWorldHudBox; 34 | } 35 | 36 | var MultiWorldHudBox: sc.MultiWorldHudBoxConstructor; 37 | 38 | interface MultiWorldQuestItemBox extends ig.GuiElementBase { 39 | gfx: ig.Image; 40 | hideRewards: boolean; 41 | includeAllRewards: boolean; 42 | quest: sc.Quest; 43 | 44 | setQuest(this: this, mwQuest: RawQuest): void; 45 | } 46 | 47 | interface MultiWorldQuestItemBoxConstructor extends ImpactClass { 48 | new ( 49 | width: number, 50 | height: number, 51 | quest: sc.Quest, 52 | mwQuest: RawQuest, 53 | showRewardAnyway: boolean, 54 | includeAllRewards: boolean 55 | ): MultiWorldQuestItemBox; 56 | } 57 | 58 | var MultiWorldQuestItemBox: sc.MultiWorldQuestItemBoxConstructor; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/types/multiworld-model.ts: -------------------------------------------------------------------------------- 1 | import type {WorldData} from "../item-data.model"; 2 | import type * as ap from "archipelago.js"; 3 | 4 | export type MultiworldOptions = { 5 | vtShadeLock: boolean | number, 6 | meteorPassage: boolean, 7 | vtSkip: boolean, 8 | keyrings: number[], 9 | questRando: boolean, 10 | hiddenQuestRewardMode: string, 11 | hiddenQuestObfuscationLevel: string, 12 | questDialogHints: boolean, 13 | progressiveChains: Record 14 | shopSendMode?: string, 15 | shopReceiveMode?: string, 16 | shopDialogHints?: boolean, 17 | chestClearanceLevels?: Record 18 | }; 19 | 20 | declare global { 21 | namespace ig { 22 | namespace EVENT_STEP { 23 | interface SEND_ITEM extends ig.EventStepBase { 24 | mwids: number[]; 25 | oldItem: {item: string; amount: number}; 26 | } 27 | 28 | interface MW_GOAL_COMPLETED extends ig.EventStepBase { 29 | goal: string; 30 | } 31 | 32 | interface SendItemConstructor extends ImpactClass { 33 | new (settings: {mwids: number[]; item: string; amount: number}): SEND_ITEM; 34 | } 35 | 36 | interface MwGoalCompletedConstructor extends ImpactClass { 37 | new (settings: {goal: string}): MW_GOAL_COMPLETED; 38 | } 39 | 40 | var SEND_ITEM: SendItemConstructor; 41 | var MW_GOAL_COMPLETED: MwGoalCompletedConstructor; 42 | } 43 | } 44 | 45 | namespace sc { 46 | enum MULTIWORLD_MSG { 47 | CONNECTION_STATUS_CHANGED, 48 | ITEM_SENT, 49 | ITEM_RECEIVED, 50 | OPTIONS_PRESENT, 51 | } 52 | 53 | interface MultiWorldModel extends ig.GameAddon, sc.Model, ig.Storage.Listener { 54 | client: ap.Client; 55 | previousConnectionStatus: ap.ConnectionStatus; 56 | 57 | baseId: number; 58 | baseNormalItemId: number; 59 | dynamicItemAreaOffset: number; 60 | baseDynamicItemId: number; 61 | numItems: number; 62 | gamepackage: ap.GamePackage; 63 | 64 | questSettings: { 65 | hidePlayer: boolean; 66 | hideIcon: boolean; 67 | }; 68 | 69 | lastIndexSeen: number; 70 | locationInfo: {[idx: number]: ap.NetworkItem}; 71 | connectionInfo: ap.ConnectionInformation; 72 | localCheckedLocations: number[]; 73 | mode: string; 74 | options: MultiworldOptions; 75 | progressiveChainProgress: Record; 76 | 77 | receivedItemMap: Record; 78 | 79 | getShopLabelsFromItemData(item: ap.NetworkItem): sc.ListBoxButton.Data; 80 | 81 | getElementConstantFromComboId(this: this, comboId: number): number | null; 82 | getItemDataFromComboId(this: this, comboId: number): [itemId: number, quantity: number]; 83 | 84 | notifyItemsSent(this: this, items: ap.NetworkItem[]): void; 85 | onLevelLoaded(this: this): void; 86 | updateConnectionStatus(this: this): void; 87 | addMultiworldItem(this: this, itemInfo: ap.NetworkItem, index: number): void; 88 | getLocationInfo( 89 | this: this, 90 | mode: ap.CreateAsHintMode, 91 | locations: number[], 92 | callback: (info: ap.NetworkItem[]) => void 93 | ): void; 94 | storeAllLocationInfo(this: this): Promise; 95 | reallyCheckLocation(this: this, mwid: number): Promise; 96 | reallyCheckLocations(this: this, mwids: number[]): Promise; 97 | login(this: this, connectionInfo: ap.ConnectionInformation): Promise; 98 | } 99 | 100 | interface MultiWorldModelConstructor extends ImpactClass { 101 | new (): MultiWorldModel; 102 | } 103 | 104 | var MultiWorldModel: sc.MultiWorldModelConstructor; 105 | 106 | var multiworld: sc.MultiWorldModel; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | export function defineVarProperty(object: Object, name: string, igVar: string) { 4 | Object.defineProperty(object, name, { 5 | get() { 6 | return ig.vars.get(igVar); 7 | }, 8 | set(newValue: any) { 9 | ig.vars.set(igVar, newValue); 10 | }, 11 | }); 12 | } 13 | 14 | export async function readJsonFromFile(path: string) { 15 | if (path.startsWith('./assets/')) { 16 | return fetch(path.slice(1)).then(resp => resp.json()); 17 | } 18 | if (path.startsWith('/assets/')) { 19 | return fetch(path).then(resp => resp.json()); 20 | } 21 | if (path.startsWith('assets/')) { 22 | return fetch('/' + path).then(resp => resp.json()); 23 | } 24 | 25 | return JSON.parse((await fs.promises.readFile(path)) as unknown as string); 26 | } 27 | 28 | export function getElementIconString(element: string) { 29 | switch (element) { 30 | case "ALL": 31 | return "\\i[element-neutral]\\i[element-heat]\\i[element-cold]\\i[element-shock]\\i[element-wave]"; 32 | case "ALL_ELEMENTS": 33 | return "\\i[element-heat]\\i[element-cold]\\i[element-shock]\\i[element-wave]"; 34 | case "NEUTRAL": 35 | return "\\i[element-neutral]"; 36 | case "HEAT": 37 | return "\\i[element-heat]"; 38 | case "COLD": 39 | return "\\i[element-cold]"; 40 | case "SHOCK": 41 | return "\\i[element-shock]"; 42 | case "WAVE": 43 | return "\\i[element-wave]"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "node_modules/ultimate-crosscode-typedefs/crosscode-ccloader-all.d.ts", 5 | "node_modules/nax-ccuilib/src/global.d.ts" 6 | ], 7 | "compilerOptions": { 8 | /* Visit https://aka.ms/tsconfig to read more about this file */ 9 | 10 | /* Projects */ 11 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 12 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 13 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 14 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 15 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 16 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 17 | 18 | /* Language and Environment */ 19 | "target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 20 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 21 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 22 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 23 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 24 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 25 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 26 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 27 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 28 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 29 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 30 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 31 | 32 | /* Modules */ 33 | "module": "commonjs", /* Specify what module code is generated. */ 34 | "rootDir": "./src/", /* Specify the root folder within your source files. */ 35 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 36 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 37 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 38 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 39 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 40 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 41 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 42 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 43 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 44 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 45 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 46 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 47 | // "resolveJsonModule": true, /* Enable importing .json files. */ 48 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 49 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 50 | 51 | /* JavaScript Support */ 52 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 53 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 54 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 55 | 56 | /* Emit */ 57 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 58 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 59 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 60 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 63 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 64 | // "removeComments": true, /* Disable emitting comments. */ 65 | "noEmit": true, /* Disable emitting files from a compilation. */ 66 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 67 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 68 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 69 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 70 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 71 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 72 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 73 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 74 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 75 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 76 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 77 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 78 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 79 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 80 | 81 | /* Interop Constraints */ 82 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 83 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 84 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 85 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 86 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 87 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 88 | 89 | /* Type Checking */ 90 | "strict": true, /* Enable all strict type-checking options. */ 91 | // "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 92 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 93 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 94 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 95 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 96 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 97 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 98 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 99 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 100 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 101 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 102 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 103 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 104 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 105 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 106 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 107 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 108 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 109 | 110 | /* Completeness */ 111 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 112 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 113 | } 114 | } 115 | --------------------------------------------------------------------------------