diff --git a/README.md b/README.md index e05ddf915..7c24c5c5c 100644 --- a/README.md +++ b/README.md @@ -18,57 +18,28 @@ properly is _just_ as important! By using Tachi, you get access to powerful, novel rhythm game score-tracking features, like automatically breaking your scores into sessions, setting goals and rivals, and more! -There are way more features that Tachi has, and you can read about all of them [here](https://docs.tachi.ac/wiki/features). - -## Setup - -Tachi maintains a setup guide that is easy to follow even if you've never programmed before. - -Check the [Documentation](https://docs.tachi.ac/contributing/setup) for how to set Tachi up. - -You can then check the component-specific guides to see how to run those components and contribute back! - ## Quick Setup For Experienced Programmers -- Open `Tachi` in vscode +- Open `Tachi` in [VSCode](https://code.visualstudio.com) or [Cursor](https://cursor.com). +- Other editors can be used, but you will need to set up devcontainers yourself. - Install the `Dev Containers` extension - Run `Dev Container: Rebuild and Open in Container` - (It can take a couple minutes to build and bootstrap) - You should be placed inside a vscode instance called `Tachi (dev-container)` - Inside there, you'll have a perfectly set up shell with fancy utils and fancy graphics. Use `just start` to start Tachi, use `seeds` to run seeds scripts, etc. -You should not be doing Tachi dev outside of the container. The container is set up perfectly with the right versions of everything and working ESLint and all that. - -### Unsupported stuff - -Tachi is intended to be developed inside a container. This ensures that you have the correct version of MongoDB, Redis, Typescript and all that jazz. - -VSCode has excellent native support for dev containers, and as such this is the only method of local development we recommend. - -Over the years we have had a _lot_ of issues with people having subtle variations on their system (or on windows). Given the contributor-centricity of Tachi, it's untenable to expect every contributor to be an expert with local dev setup. - -The devcontainer provides us with the most simple, consistent experience, and allows us to put nice-to-haves inside the user's shell. - -That said, if you're against using vscode you can: - -- Start up the docker-compose file. -- `docker exec` your way into the dev container to get a shell -- Run `./dev/bootstrap.sh` inside the container. -- Use this shell from now on to run Tachi commands! Edit the code in whatever editor you want. - -If those instructions don't make sense to you, I really recommend just using dev containers. - -Alternatively **if you want to work outside of docker you're on your own**. Figure out the correct versions for everything (bun, postgres, redis, ts-node...) and `dev/bootstrap.sh`. +**Doing Tachi dev outside of the devcontainer is unsupported.** The container is set up perfectly with the right versions of everything and is the expected way to do things. +Please do not report bugs in the local setup if you are not using the devcontainer. ## Repository Info This monorepo contains the following codebases: -- `client/`, Which is a React frontend for Tachi. (AGPL3) +- `typescript/client/`, Which is a React frontend for Tachi. (AGPL3) The client and the server are fairly decoupled. Someone could trivially create their own frontend client for Tachi. -- `server/`, Which is an Express-Typescript backend for Tachi. (AGPL3) +- `typescript/server/`, Which is an Express-Typescript backend for Tachi. (AGPL3) This contains all of our API calls, and interfaces with our database, and powers the actual score import engine. @@ -77,12 +48,12 @@ This contains all of our API calls, and interfaces with our database, and powers **This is the source of truth for the songs, charts, and more on the site!** By submitting PRs to this, you can fix bugs on the website, add new charts, and more. -- `bot/`, Which is a discord bot frontend for Tachi. (MIT) +- `typescript/bot/`, Which is a discord bot frontend for Tachi. (MIT) -- `common/`, Which contains common types, utils and functions shared between all other packages. (MIT) +- `typescript/common/`, Which contains common types, utils and functions shared between all other packages. (MIT) This is also published to NPM when it hits production. - `docs/`, Which contains Tachi documentation. (MIT) -- `sieglinde/`, Which contains our BMS/PMS analysis functions. (MIT) +- `typescript/sieglinde/`, Which contains our BMS/PMS analysis functions. (MIT) diff --git a/db/seeds/goal-id-remap.json b/db/seeds/goal-id-remap.json new file mode 100644 index 000000000..b7072485c --- /dev/null +++ b/db/seeds/goal-id-remap.json @@ -0,0 +1,233 @@ +{ + "Gcea58fb099db74612d9654c4e8fec345154ce00c31d7525ea03efc7389a27c09": "G1a2499cae27572b8bddd861b28aaa01dbe4c885544f15fae5ed91765a7b924a9", + "Gb0d9e72831b267ce5f1feaa84f99080df6a9b371ce3a22b6754baca3c8afffaf": "Gdee336867a22a0a789ab45b863775f4351b6f70fbaa0ea1928c977179320d0ae", + "Ge3165eb0547fb894c169abc11e6f541b839ec8535bef72c895b64ca06b5767ae": "G36f76a980802c1f06060e29f0d4d590fa06cb2ab2d5059b62e72489b98e0cdfe", + "Gccdd7b7f8f476397bf624791745252d71fe5eb276f832c8dc05384ae8a5c9c6b": "G53d4c238478106b7a54b865f02d505f650c29b9bda8ff7ce229389b2bafd575a", + "G27018d6ed0720684cd03cb99104d1f1146e391f5e53ddd0d1d488b6e06722c1e": "G4a86b8c4a0e2c60bbf713d1be8f4ba7e4892ede6954a499beb8100f41ce78391", + "G3ba44cf2fa1a53bb8de5cc99d5d4c5208ebd06250c2052393d7a4a830ab72f63": "G6da6680177acd81b493da1c8163174b6ac517c6463b25f0259f377bc9f2a3966", + "G2ffadcc4aac205cc4af0c91e2c20dcb1a68564d591d7c802467e7122b70a7fda": "G749a8d3ac2bf391d65aee7491bd64bf0b20097ac4c097bc9dd0f42bccf5da047", + "Ge536bd96adc49bb76c4168fe6121e8e0c046a9c82c8e22a72f9150ae9ca317b8": "G825995e16f75db2a31953a41564f3ceabdb250a3388843daafd527098989d78f", + "G76994cf527cc8f78cc5bec3591439b980087e8e5a305e04e418d70b4b8556d61": "G5b99b92615fc9d1554e8bec4264885d642a5dd5ab9daba82cfc840da6261fcb0", + "G9758f5f089bdc9757ad5991b1282f4ed39f5a197343f1fbd3d9f6a5a5985f8e3": "G39cde64528a80e9a7b22f6a612d5567945c9e1e7a6e6bdb7275ff39029d98a71", + "G4fd0a9ec034100c7bbf40527b0bc55be71617c545f06d4c7feb4795fc5ed0601": "G2634ee3209ddc4d87edb90c34471ea8603bf22885ac53daae36979fcfe656182", + "G018c3d8a77fb97bf5534f29ff960797f82effd1c8c3852da7ddf62170bf0fbba": "G1dafbb9a067708d0d52610a52d9e3d9efdd1ee8c40198f23a7bb219e78c5513b", + "Ga13da591f9b95e132a7a1947603745649494154cec51d1dc427a9b83633edb64": "Gf460944fe4fa8d1cd8b1db041e9475b33e4d25f6dc49b383ea144d4e5180dd4e", + "G4f7d560c4b930e1c689c8aa10c87fc8bb873231da207f7375dbcdac46b6ecf35": "G1f254c17d443890622d34cee5ea0e216184c9637f76e1266eb99e0540da9fdc6", + "G226a21e1f284a93142fe1259067fceef84ba44eb1dc8d5fb02059f0c30d2b39b": "G8a7ec915d0613ce018202caa1e16dc9dbbf5ce18cb391110bc091e9136da6e00", + "Gc3a2eeb0712812682aa514e0029cb4c1b2617f5c1ed51d80319f796f388ed29b": "Gcf8d759883e71996af9623499956d3d6ee6e591b7a58e6b8698469580ea2212d", + "Gb36433d9e006d2371ce7cd337b798ec63afee190a796b5cd9c4e901c86c973df": "Gd467e4325e60010906055a976204bffd89a5fa805fbcf898c62618a8f81a57a3", + "Ge0238448ca7e640b3c2330b4e8352eda8ce728de9b49f80b25fe5e69bd34a6f0": "G01d884675908fc9e06a7e5b477afd10bf932a06ccbef8dfa25d5eb40ebedc703", + "G1381f81b6b7e50e8834c37fac064c264da3d5b082424b9427a5abbde12e1a46e": "G9cf0ca4046641e30a86ecfc206b095bc58c344907840e81a3f6f96ce7897dbc1", + "G200430b184083eaaf5e70448db9923b69aca66472f45ce981e150a53f2deb9db": "G9ec817b5b48790e7655be86536ea9f3b977d4b8f33bd2c0fef4b5675eeef5bb4", + "G6bc92b864cdc885e5b68afcd743a552a7c9f4b42d2388cf66003472867e3bd4e": "Gb8464c140da50ad417aeaf8223fe260b3f88a992c6c88bb031d72cccba48a55a", + "G8c14a7e978b55b03be418b6d57b4b828b974b1d927bcf23d3b6b2489259004ef": "Ge0973c181b48cbf54a3f6d14946f867a0ac238c3cd39bc572f9e64601652ace3", + "Gef6390558f110e73135f38ba3693a2a0bbbfebf79f5d05a18ac542037c6e82f7": "Gb3bb20d0b86bf071d17a94a8a97b089486dcea29956d1e4561c17b0fe02afb36", + "G960274ddf4dea2cacd559f121430ec00ea676f7549a98477b0bc4513784755d0": "G999a566fdad5912d5ba7bdd4ecad09438215ba1cca2a7085ae7e9404178d44ae", + "G3fa6d1586ce8508e3d479d5e78eb00fa137b014bbd75d932312b0ee70c386b15": "Gf4e2efb99bcbedcec5afe6128ad9b48c2329bc604e9f3676d8b77e4bc68708f6", + "Gd296f58f71491c79d6a957e4d1f51b0eeba55cc2c18c7cd2dd6daaa72b8c1fac": "Gd1c17cae156a8cc9993adafce8924bfee58558c51d59eec87c687c281499dc8e", + "Gb42704d7405a574587b6e56732f13326c48f56b9ecb9a8ae5ed245d56999dd46": "G661132cce7d7b205f4ccc463073f7eccdd8ad0700539f5f86fff216be4a62799", + "G0ed4c552766ea82145c8a9fbf4ffa1df3fe5645c5141db3f48ec7c5af0513c4d": "Gd064ea10a7d7a330cde62b2047047bb7116f769b56082efb3fef7a95f9c0d125", + "G31d3218b8e4a5f4c860d9b2f29247472cb9481f8b81a0e2829ec6d4e9ec1a040": "G34bcb7d0ec46bf486014ff7cccaf4b059572a58944d2c31ad628126ac8826358", + "Ga5dc820413f88b58e703f615324e0ee07aced954a62a601b42756bbefd648c60": "Ge2ff55571ba96a533f278c0536473d0f16379e306b775cc1c63c20c5c833d201", + "G71bd61d5846a87127e447c0e6be298e320b1ba8547107b4c29ff1fbc4e976e94": "G16d80e16e4657d413c93b203c6938432159f53b93dc05ead19dca8614b5f9819", + "G3be852df2c79b97a22ce7b5b3c04846e992d0a2a989aaeac89b668bd4ac4a3aa": "G2f376d12b93d701297b28c53b6bbbce4b44a61156bc739288a2ed70fd4f6c5d1", + "G39cc151b00da6e08a49d2c1ffa28b376e600cbb0d30ba369e94abb4cb909522d": "G48490053cd8c865b0b7341b8728f8dcfc6b22c7a93a06cd14b328d0811e12b36", + "G69167cb3007cbc10ff3f56435cee6133af5a3ab44d8b90f482342cb277942c76": "G53a3bad5acd46acc7ec0334bc4b4b2c317583e398e097bd219e148ccde0541f1", + "Gbff5754db52ea104e33fa633c50646da699ccfaefc63e5b4999ce389d0db7ce3": "Gfbab43227c6ad53f49a2333114f97412467b657164750f0f5288a64982f42eeb", + "G8621b92ec1aa8a15312e664940f216f500ca6a068405ff384e3c4fb846b81bfc": "G19c09c3f23a7f2216d7a0fda3a525ce614f772a4fd455ccf40aa7f8f7a26b7ab", + "G2935018dd82616079e0c56953f0de19a01afedc63b07c7e83003f9a9c76d17c8": "Gd29cd861d7aaf7d27050950e69126f8f92813e96804f440458f3009a4079cbd6", + "G298166932cfa50abfd5fdb67f5e16e5a46bd9c8b83be93db98147156bf82c872": "Gfb59b854a2d0f8dfc56cab487f4b43f4098cad073bfca42b99edb125d20e9c61", + "G37750486bcc1c26f93958af699d0e1ecf8201e8a021ea2e17bc25707a87ce5a9": "G08ed442b3f0210371e99276442281c34d4f6703bbb301a229502ef5054f1ffd9", + "Ga5db62596cf669c2593b8b03329e6608026fafc35ef5c2238416bb6a28b9f62a": "G2be92dc54829e1da6016d6145b324b1a43e1ecefca262dd4bfdb68a9f35cc43f", + "Gbed2eb0c5ce16d22ee66f0c07b921ef9d96bd3fe80f5a943aa52f0147d889e68": "G8392d2f9da662e5a7034af3e0264bb447e290d539ba4dc46ddc185f5063965d5", + "G4e6cc043b09a7cb0fa8b7ae3b0c970f5c6de6bc78ec2ab5625785586ef9fbd4e": "G786aa0be57281d82ebe04aefdc20f4722bb6ace8e9bcb8768db4db03b1375005", + "Gbeb7bbd19ed0eed65f8166d02a33091b8ad03228fc19ef560167efdf6a07f33c": "Gf0c07a8501880c2fe56826d3c8c852563851d40b47fde07c1aeff780a83cdc2b", + "G2bb7ce1fc898f028e3a525ed15bb991718136ce27200c1f8a59ef959df239589": "G8eb9a981f9671033f078269ac98b382f46f844649a30cea54b3be26332fdfc47", + "G9a06f0cc8758cc19dce70924849e633dededec6022aab385ced2b887d4cca597": "G0e1c4e8dc2bd01a3437ef728654b0edb63ad1b57f869e5cee651c1fe289b2b97", + "G9930408e8aaadb119e8bc22a707aad05dbf362a60c7bd7e445b6886ff2831d3c": "G0195d9b1603ed74eaa038f5363cdd676eeb63b30a386746cb6b349aad6f09c96", + "Gcaf0a44d2509e511fa767beb1638b22a584ff1adcbc4940f1e598135fc27b736": "Gbd03b00f85711c94f00fea90089076f48a9cccda65f23bd3ed3f4fb35e01ec8c", + "G754bf911321ae722562c301aa00487fb19c0ae4271de27a9733a177ed63df5db": "G8e816f07a69b9ef1e0004ff1b0cba95fc41149a4b01c52f98f20f3f00ae07be3", + "G654f58332e9a852e4a9b5bcf77f8b68f722afc10580beca72b5b0c422c7b2ef1": "G6c8e79d4902b4cf743a8afe439b1a1e0abdb7f8f21eb5039c6a629532e60a906", + "G89cec4ec11dd11288f01c807ce2ff0f52fdc75aac33ccdbf1f100f3f829620d2": "G57eb13343a058c69a5698d1ac367b92fd72ceb3da411e8dac0398a0768a5a4bb", + "Gad1b5c3e243dc1255cb3d862df58f116af45c5629863566118e3a2feb0869226": "G7f083af0e8e9947647a900cdda9c31d034508dbaae52c0409443a3b3ad21d59f", + "G0d4820d485f9cffeee870edbbf8edd923936905e9ebcdc50a83d23de7ec6a159": "G3d1c87e9352934da393602ab72e09d975ead0e6967d11f55fedb62c45d5859f5", + "Ga35dd5e0aa06e9c44e4bbb1112619474756fbbd301a6e7c642b817c62ef0d455": "G83e9153d064fa975a7c189ce7e881fa9f086350f4499aff3f3358033075b75ec", + "Gdcad64d899954044c12389197968aea20d15b4fdef98c52d5ec23153c0b69c20": "Gd2a24516591b18582a3eff64136f2099cc177215a0e3efcedff01890733c0788", + "G625186895fb1153ee8920313ab4a5668038fcd9712fff32e6a50887be3e3dd7e": "G3663a80447cd30ab89384962e8e6b2b8c2ccbb4c7abd1c96b149bc8181c5eba2", + "Ga6b497bfeb3e9f2255ab4805ffd921c34c0abedd52f93588035bd547ff6724e9": "G4c7474d686b61360150065ea71908db503e9052767e39c7551d7cc4ccdbd2552", + "G20789dacd98c01463a6ddee5e3cdfe39a093b2620413091e6420d63da9302473": "G324fc7b7a4f6b9c6c5600fc99efecd473ec3485a1dc8108be00b9682bf41bbf2", + "G2568289f555406026d89a0faa647c966f9ec9a5e97f3e73d099a6d3bce811a54": "G886a5a0c91eb4a0ad9402c05b12e8f6d3fc652d3effea71d3485cf50a7dbd8c6", + "G82327ca3c65a1b4750ca3605caf85c7817412fd4f179c55a85a9aad43ddd13bd": "G3af77e883209462687918d0f436c1596d6e3fd6678841f6785e08361a75af46f", + "Gda3e8b2f99d380aa0c60f8780dacf3981ad7a09324805a4d0f58867914f560e5": "G78340b6bc4ce95763f797550a2941a04f1bcfcb8df5d4091d1a9ea5c24a1f7e2", + "G6a4ff51532d93b22dfd5c8cadd408e642f6cb7a6096eb6ed079d0e98ef238d5b": "Ga96793d89c4924914e2a6f306277badffacf0990af24b2b3674e96f466951615", + "Ge18b9d2a86bb342b414e24a6549cb1f34a6c000d679a932fb4b6568ccf4063dd": "G472d131ccef571f9cb2ee06e7261214f49bbc1e36a119e3c0cc366c98f23f500", + "G6a29e0bc11a1e356f7fa1328e8b9a91fcc389be160b26ed3b408800ae6a6fd13": "G09f78b3a6b607c39b2fca68a7a4054323bb750b2fe70dc85ab13c72bf3d75860", + "Gc0bd97f34ac7a4e3e3bcc829640f6526d5bf3f4d6003cbc09b6aea147d3de1b2": "G7ac29a0cb0c338d930ffa76835b20049d1a81f81f69c794d3dcac8dfc6a0141e", + "G5f5eff96326b85ed2bc803d9424618a6da5f656e456bb4b7734cde61bc49067b": "Ge7067c5fb15f67bac212093da736ffbd04aa13e2915ddbc4a08450d0d3ec4a97", + "G723468dab17f8e083411c797b7cbe51da53b27b782413797a1931e90afc5b4e1": "Gacb2b831d861daf8f646169e0836465c9d1b75c879f9cae86ef70e37ac4526c2", + "Gc08c0654afce1b73d2c02f4593c5e29acd7c0fceb40ccc151e396fb017d314eb": "G18170eb62353e8bb450fcc44df2cd763b64c2dd853d943e310cf372599b7055f", + "G3b9b888f049bf483fc17a7eb85f344e0e5c74397e949e6e501a7b17241769c20": "Gad8194c5f9050efcb8b12e150be83682769a61897dbd16d19851f6732951f88a", + "G38feae067106ecd440229bfb4c8bcc03ab927f237ddd6c698879238c8aef3835": "G5014da556b19342d379352a3befaff4d7271a65174dc91d8278c0c8295d2a429", + "Ge43b406e4e034ec24a6deb6c447be7ec6678a7a8cdd59132f6f3cbf95165d4c1": "G2ff00b659f0ddf8610d606d53b02fdc8df69ff639db5c246f6c80a525c458db7", + "Ge21176059a83eef7fa8f5fc98c329b1d6909395ccfafa15ae9e0e0120177be93": "Gd0616dd15eec949ccd49d727b22a5fd33352fdab418e8d464313b357aca7fa13", + "G6a58f1cccd41586cd4b030644249932a622dca06c523f84184c9bbc0571941d9": "G12f2b73cd600478a0c16c0fc6b382005b3a933865f3d11de127e499de9bcfb60", + "G0f5bd097383b05e7dace9ba2cc690094d02e13756fdba1da5b3c316e84440522": "G2b883ffdbe765eaaebfac6e83bc0081b9c997ca0e3d1035d02ddff07d7fef2e1", + "G880c284bcbb6ae7e6cf6482c613f2e045c989d52754e0e9dd726a114a572b452": "G61da2b79d0434ff044ceff76352d9ba9748775d1e8baf43124c8ecb6b1c8d502", + "G6c6b2c4dd15d96f336878468a5461c178f1d9bf66340468976592433db9dff88": "G1c9806798380fd61a84e0b9c110c728cca54f643eadf620373f84cf042d95e36", + "G03e076f00f51a3bacd8d0e1a3eb349b62fa6cca1241281f12dfa2f8f42060a6f": "Gca3a6b342a136b9b6b7427ab653d76a516aadc1c9b0a7b079b98d5c04b984118", + "Gf41ec8a91f708368f7a60f7d8b4d77690c100916430148fef0fd8d16a7c6b568": "Gf151ed2cde8b6da5011d963a3f4e593c6010fd92646a7fb1c6d6738c2920a277", + "Ge2f036b10ebc5c82731cc1d99ad55852fa67e6f5e5daf8f8e1f9f2f5ea1e5725": "G2df81066938bb2ff7d0e166f026ec253a27fa4e8adbfe55e030872213dbb0af9", + "Gca3459a14f5838332035d3be80ff6705d12845b6294a29e9b60722b4fe57f79d": "G63a7f069f8942865d6ddf7b51bd8c5e1bc051671c5e3b565a8b1544d46c73b79", + "Gfc705ce59372c3d66ce7c92da9c0ebe753f8892235143cd302f1d12951224b22": "Gb6469efe0ad1fe92949ed1ccfc26115a2620cdd7be6ee041e99faaf6b571b90b", + "G394186cda313937cb8cc70f2c47064bf9acf921b9cf31cf8260107ef32ba9add": "G71cca18c17f15bbb8920c269f4c989fd064b750a8e4748b1ad767116f38a6e77", + "Gf2943e13de2a701a0d83872c8d162287262400b647f43f4cab66bc37b16e3d03": "Gb07e04f91ccfc1eb1502800e1f54f7197829b7d1443838f5b7f2835d5d702abb", + "Gb7f75c5e982075bfb653516f7e3e3a99958fb9083e48b7965b1d7a30405a7529": "Gfa235ed890bbc75bab98cd88bc07ebd83f65ce5b14ce3249af1fdf9d816be726", + "Ga26f4cafc715e6c6662abf79debc774938cc0b259b25d00397f98ec9499f94eb": "Ga9ed7c31b4262f6e5dfc58b3d00a8ccec11d0ba5e0b2e47c57b220919785b373", + "G7ca5885c32800c012cc897dc14abc2c8cac462cad7b1f4d2f88328a1f3392c35": "Ga0695b5a44b0fc387d0fd8e7289fecb6a2254231bee6c5624695e44192f77d23", + "G8bae0ae05088cc8506aff9900c34228c5427744ada392a4f7f9dd35c3037a1e8": "Gbbda7dfac9675109f47f73efe54520db3d16f51876fd8a86e29eac5ec39a534e", + "G181df38bf004505dcdde1ff8c9729f5fd39b8fa71161848c4d4d6ca6d727ec41": "G7265d66249d1d9bd54c1ae06b6c1936b984beef4a154ee137412bcf05d75b8e3", + "Gd32662938d122fcbfa18d4f8390d2772859df69192c6b924f8012093ad6c3fec": "G063ef72be4cc1cb237dfa267d9f400b66cf337136c952daf4334df7ae7e41bfc", + "G42df127decb5fac1601a864f63044d3acd56cf95c4708c48dec4da8e83154447": "Ga56e3b9b26905c5a45da42bdd7080ba5341ad94d7a3a545b365fe17992cb2b65", + "G75d3d875541961b03f620caa0fdb03681680cdafe0298ff99c442499cd5fb378": "Gb8d3310edb53dd5f6a5d08da0f6e18e75ebca930698e39abe36a52662718f1b2", + "G79c4f6884c285c67a3ba9d721622fa917201769d4ddfd0dd9bfb38472d5e10da": "Gb22d2a97955f9932a348b1596b6476ed6a7bdbc1d080b59a73cfd9fba6ebb7de", + "G7f6b492105cb229cdc5abbd6306d9537f8b0aac16c9f74db60b53a6785f9b604": "G8f28381abceaa26a9c3a76d3cfb7a630450da1dddb8cd82171f9a7b5173b8649", + "Gcd004c1887aef0e7704da2ce50153b06706a6ae86a68f747bbfbd8257bf201eb": "G254bc6d16c8ed54da6d7ad88903a46b8500d24049537901e070055347379986d", + "Ga0df6083017e36eab6bb9ebde9b26776bb85faffd728b1c54a3b205cabbac1b5": "G4b25b28d4afdfa58fee55c84a2963e1886886ae515ccab6274cd66a601b55c24", + "G55dadbcb465cc58b4c949826ca0436e808c259e909a1108143a4710e5b8b352d": "G7c5055482429522f7482e7bdb97036d9a2c7e93f294ad4b39660d8e971884c29", + "G08fd73ecdd710f045816430e5cbbe1b70d2053e466cf3153b4564993b36d8ba9": "G60b4be3a6aca95dd43fa1bf62215deabba42f7a40ab3e3eef78916705d0a38ea", + "Gc567056c12238d066060d988a8743ec2a4e9c1d09ca44d6dadd08be8ab603f13": "G332b29e41e4069bf16d5b4718408c8a8fe446aff4492c26b7ab4389a7df4286d", + "Gf53be8d6288499987c8136e0d96676662d57c929a1820303f47a18f1d9977ef6": "G89dd2e38a992365ad2f80eb39fee7e686cb13c3a9a1343b266913fa63afa5a79", + "G2455689e65bc890fe6b768786ac26bc1ecc010b7103888019a131b6aaa9a6828": "G86ef585d023d76a34105a87a29532201fa5cf0977e5b1bb7c4cdbbb2bc0d7d02", + "G2273a629e2c47d4e4a05cd86bb2dd07020945dfd1bb0c2b6e62eb3e319d67280": "G45fecf25975b96b93ce9c81fac3fccac618516935bf278c025e5707312de92f2", + "G653396c9ea159a062a7f0f7455cbf13a100fdaf48b6aef95e7ba98f1f4e01c3f": "Gb19e2023305b87213154c3a28e8c858141cf3fa113b6a15815b5d2b12009938d", + "G5852c506437c45c321c465e787a7226ce6f018f73f7299919264c357ef70fe82": "G29a70b3f17eae4f3b8256d0d832470d278a48147e839d4238e8a082193a96502", + "Gc501abbcdb1ffbfcadf48e69a49ed5c8822e5804a016223acd727f24aff037bc": "Gcf40d565e86bf83a3eef2a7cb82deccf0addefa7221e7d9d4ea81d0d26da3d8d", + "Gaab422b2ec34f6a3140193a0bdf62e7228d28a62461b1894de6a224a22e7fed7": "Gbabb92af4e18584e9641f97cc7bd63ebadffa8ca1fe410f56bc8cee0b8f308bf", + "Gd4d221f92419a6d4db84679512e9484023dc0c77dc911fb0af5332d4049895ff": "Gcd568e1f0bc9ed6c4c1e2736f7cfda2eef649931562bfbd79b4e40c07c8ecd4b", + "G7e4e5698240f19602fc4775b101507f1008d6726e7d53b77699d3d41201f90d0": "Gfaf0ebec02f07c36c9f09efe37acd925cefcebbf092ccc3b40a23bf3b072835f", + "G2267969a8bf2299a24209062578dae910a343d8aa070b815aecd9cb0fb1da8e3": "Gea588214e18bc3879ee62164eb06c7356230f35be3ae75a71d83d01645532628", + "G80d42b930287a1ef469f86d18d062661426b46e1f09c5591fd5380b93ae4f7b0": "G9973d2b6a16c70d30b0d302ace1ac6d523a4d6d7d1847134dc3c67d3fde218e7", + "G161dbccdf18052986d896dfc5323f038c1abb36f2e764ed46d92c2e90b3106a6": "Gfbbd23ea00d0c1e07e787e7f9e8a969bb1d3e50016037e7f831abcc8a76051d0", + "G096952757cb1363652f1c8d7c6a4d31f8ca0ae627f7e41568614a0bb8d283fe7": "G7a1c16f3d0dfac597511a5ef7f102b8f05f1859aa5bb347ee980e6ffe9124153", + "Ge99fd25f71e64d91771546fa9dda6b5b59f2746d2259398f34d1f3c63c9ec721": "Gb372c466966afbf3ebf70c5d3b701dab56fd376965797a6864ed163b20d2868a", + "G1a51c71951ad0fc9f1e7eaaa32ff6a6c25f3659faa0f0b63e1440590352fa850": "Gcaf664e666e9d567941bddc60734e270cc273456074b66fa81921f0718419584", + "G65f1fb8f98fb1f8da4c19cdecf4b33cae36769e9354c825231e1738e3c27eb60": "G24421c36c33c601a8ec08fbc4de2713ff7fdbdf14e088fe0a6c4cd640e544d79", + "G5b4244720155901e8b3aa8c2143c561d88f8fce9fa80cf619ae5111aacc20694": "G2788eba138e2919d69fc1c78da2af3d03d0dd5923bc816adcd13d977b9a07b51", + "Geb56e070e7fe0fbf39443b4e86c96544b6e9ffb1a055270bcfe4152b5a8b0e89": "G16b48da8eaa90d33762ebdd9157f318a6796c18233c2b7342fd60342c19f5a84", + "Ge89c922fa298f38f8a478219be94871f084206c3f6e49b9a8244c27feae5c5f3": "G89e418fe4063bd4ec408ab6f14fc2adeb8c6b083b78c035e86baa7fb08c49be5", + "G8f8c8fb87a12f4074370ca73e1f168d5d9d2c4e5ca3a2b4d7fc4a6c5e80fb82a": "Gf6b5d259f395e82c4a0253318916d1ba879518018e59db87bd074794a1b4f744", + "G1cc8b945a0c578e781694a198d70a4967367868e959b0fcee177700cf2942ab7": "G7036ef4e689adc7193a8e8eb21e35fee512ee1bf77f936a902e703da6039602e", + "G9a070f7500b05c13f92c14d8cb646012633394110e10de7cb263dbd721b66b18": "G167348b8b1dd68884dd7ec4c7889fff8f71ff8d96aece8aac2de550acadab729", + "Gd8f31713af340aa31482fbce46a07af342004e310b744c6e5b516d7385a57502": "G809602ca08ef16ac69cbb376195cf646e9a259905283e123c9dff879c6c13594", + "Gf431ae7eb6403be496d0d3bcee22bbcc503bf48e721e8e5a6eb8dcc80efbd08c": "Gf8ae63ad9fb57853c7431ea5a8ba962da3e2bd0d29df547fa9239642acd1aab8", + "G378188b4d0c49bbfa1284d1155ae558dca50abb5a245be1c37c5209ede57085f": "G03e0b772b28192e1503552d94e7ca9785d79e74f6682d922b3d8e550723c095c", + "G1c238bc8ca9b8f89e1eab6789e5a60f5624573a9b68e121044d50344cb61fe42": "G712841f80690c78026f34dcf8ed885545168bcb065afa9987038c577c703fe0f", + "Gbc23d96a89094c2c3bb6366096f6fcdca45b30ecb55192e2229e4fddd0310faa": "G46b5ce2a764563eb431d3270836e49fd911c64cd502495d10bf2da1bba4dc01c", + "G33a9358057928f0ffb8b5691315eaedc74b903fa717158d0fe077eb3eca3e517": "G04446d20628c449092f4e5a74c5416315fde66b6fa9e9977f6d074619451f6bd", + "G6a1d19018159d80457f7045b7908ba2a32ab4889bac33153be140967f7cedcc7": "G911d105e08b4c74f2ddb698efbb952b51753cb7dba4e35ad512e6104fd1064b6", + "G2db28d9a6929d00d781cb8134d17f3c7b683ecd6bc389984240fce4dc0475403": "Gab1c4a139570cfbee48db6c96f66cd39486d99911771340f11e05533aa4b3287", + "Gf3b0dfb58f116c1c7134db24401590403234c478a10c076758543f29b2c235bb": "G3b2d3447cae04f856cb6c51a4d924aeabefc2034c1dd8a823bd134c04cc3b9db", + "G1e2eda29c30949f974cb30a042fe5dc78551d53a76ded24fc46cc4b73845259c": "G51eae5902c3a14284b515cf85001a6e94cc195f8b408865bd808074a11b0f947", + "G7f67f44a0fd3eea6c22c83398ebcafd71136248f0911fb8f790a6d4c837b01aa": "Gc275e614ee2303f13e217d7883a35ff4d3507a96f58db9e701023596c91d9a69", + "G0ae78134b5a4c3884353d6d5900dcfd12f8d53695a7ce9691f9327edc86cc289": "G84b57d80510556b7aa49b3c01d76d4e4c29521d70c34fa97647a37676ea9362a", + "Ge0417bddb65b1021a308a19495d824130c6d0b1ca23e606249f316bb21f2a43c": "Gf5cc6c22777f61ca37c672458970ffae60a6ffb238343fb0b22a838bba83b9c2", + "G47c8f8f25c445a4558206e8dfbd429068349dff7abdd0ed482c7d3900d8e6f59": "G15ae6ddaa4b63de46dd2371010df7864752996f8e904906faae0feb72c511d1a", + "Ga1bc389b21e3012240f33f493de6db7efd1389a372b67f6fbe9856c400d29f0f": "G0d44be3bae50f7554f0e696e39b726926f6bb750fd49426c848c84ed9bcbbaad", + "G00d676101f4850abd718e1fe908539d4f91ad4ea58f73af7b9cbc52544e74684": "G2a931695fbcb409c4e88590d1af4887a4b41c5787c625a45b2ca7b3a672fd034", + "G22531dc83f001d7bc2f73a28b7a3d71f2835b174a2eba151393319b710b375ec": "G00122bef622fadcfd27c3ae3472bc8f5a929a64ad5dd62f7dd7e6c67e595766f", + "G873e92ff8bb8352eef55b97e11329e216bd2a037145e0385147b094bd592da82": "G39e38988b5fff299596322caf2d7e522d5af8c373560b3bd592fa58992a67a58", + "G5bc76bd2a019f34bef39f4a4cf731c1d726a82abb47b75834f5e7ce200af93a2": "Ge1e31f0d0cbb42a4ade9ef4955f2e24527fdd26f43eac8d5be6f8df7b897d8f0", + "Gac4eda4ba8dfc2158542ae76bd49fbbbdea750f4332338cd330c1f4c8bc1a84b": "G1d5ca7dbac64333501d2addcdc5204b8f67ff97d8816b31148de032b3f88143b", + "G25943e5bfe8824d0f67c282728f9cd3650666519e71aacf8a4273cfd5d08b8de": "Ga5f928c598496bb359148b3a369b2c39803a9e65829150f0719b5de89856350e", + "G3c37a44164e6ae1115dd1ccbd67a54d4131a15b228e2afe0d9f271894d8629ef": "Gf9361811b028db747f42efac21279b2309cc60b6c4928c2995f7d5dd44d003b1", + "Geb61eb5bc6b35cad82582243c0cf6879dee5a1743c78645ca625b6de31ca4f6a": "G37a14b574a6fa69869b81853cd18acc23ca6cc8e5a0e66ba52bc50d2adb37bb4", + "G28366badbe5c87fa61a5025a58cdf8f52f14b3330b19083567de401115f9c72c": "G7fe33015d02f9b19626121a96480e12cbe06905f3a33252b64ec9927857050e3", + "G8f9f1a73b569a99206b8a99461d713dec97720bd049df1f8e433632993633001": "G26c265e7188e5a1536a818eb38fda52eaee3bfd7e71b3e1f029e0262ea6a782c", + "Gd837975332349aff6f091232ebe8e4275096bfd5e8866eb96f8f7988163d5ff6": "G94c2a3d6e936cf946d7b870caaed917eb9a00d644f68ac514387a13a1356b2c1", + "G1fa2d8289b79aaf5a3067df209f175eedc834d18b34ed43d3e62978fa6302e32": "G909ef14ed7b214a25332693fe623ae65e2e0468f91def016d7a77f8d5779af46", + "G1f62ddb6de93461446da5898c67392c8030f0c6bad3a11ba0433565230f9fac6": "G604f3063c34494062b11e181543cf881d408b67311695f750927b3f907e75cf5", + "Gb4c1a4e8e178dbf99d0ed3f5c1a59fc83d88f93a6ab0b4567dc4e70db837b2c6": "Gd95a09ddc1ce7d3d139c7c622883d58c5541127c94d3fea1c3828f912ca412f0", + "G87456157681bafef37d52d99370088e547d5e9ad55fd45369e12dd2bd3895a3c": "G6f51d83a87ea6468d36357d2fbebad8248a1db01503d409f7bced22a17d21199", + "G488d565d68431ce46422f085f04c04e80366b57a64a025e8914ff8d0297ee382": "G16448646d8f0ee92742e50607460ed9ab36ce77078f83336592c060360326d59", + "G26df4f768894142b4161e7b4b362a6db09bccae563dc106e0de662c10924c88f": "G39a50e57d5a90612ce18d55ad410299f854a5d88c439582fd51facdbf9d338f1", + "G66acf9104f38a848ab38caeb409c7ccecccea02a6b7c1453d5d4ef4caaa95b35": "Gcfebd2e376a3480b9e6c033f117469b765582902c36339422ad9a9898402ee1f", + "Gd20bb734f7924f085951fae9e2c019d9e63aa010e617ae92f81b9249e56bc172": "Gcf2e16e52eeb5b2c5af03c96e37381eb1a2ca7ed1e5941fafbc297a5553381b0", + "Gfe5cb6e3f57cf27279b3f79c99791fd5c30acb5bea982f34a99ae9c9dd7f5105": "G60ea64a9d5c8db898668210740f06c79924b37229dca4bda9c6908b2f958bfc6", + "Ge0dbddf1ca9773ebaf2ee8d1bfe42b372eb6b3094c2de1b0275766d326daa706": "Gfe519c3456dff488fd95160e05e82566c09f994ef97daea3342ac39b7e5d73ff", + "G4670b732730d59913fab058592f9c12e1d1c1885f5342faae6e64aba0c6e855f": "G60e9c01536ddd674ae7da090f6fed232d8de0358a0d253949d780133d12e5bf8", + "Gd5be9b5c9a426041a9ca79acd1fa0f371b0e524898e6ffe5f0321b5f55fbc67e": "Ga6d0023afac9a643d7bfa255e678433ff5d320d4b3de803df57e1385e97a781b", + "Gd6850c9703150082ab42078509f94a098ceafb612b6ce4ca0db8e94b357ff278": "G93ee06f15bec2deed7fb4c59221da49da778147972ebc070d3602f8beb78c13c", + "G4c2683550000efb9ceea5b1b6f4d67444f2487db1d3d38bcea144f98ebb22f2b": "G2753016668b125fa4c054f8c5044e59ebee545a1cb4aa50f9a95af5e2afd600d", + "G9113ca9a758f555f24c8ba3037da9a052562b3d686e6303027206572389811e1": "Ga70c91a1d0c0240cfb3c0b3628b4b00e4250208335e09d2efd04cd137325d789", + "G28c623459d09d203b4220d0419835925a486ff24b553c70910b34ce87d1e34c6": "G19bb3009f3b3af570179c740657bcd8ce468e0ae1a0cfddf138c31331657bdbc", + "Gd65e62814382a3e165c32cb2be3c66e4d6a9428fa9a4970e933ac0a580afc585": "G9f3ef95935d9c7f41602019787938467d1fce5294f7c91850fe3d5840ee13fce", + "Ge8ea38a06a88f3bf81f8f59bf39290b53f8d8a62e315dc957eaec03b7b8df3a4": "Gf5463e029bcc05da520e5cd0a7964c2cbdc36618c46506212fd5488c22679391", + "G8145fdbea66519513f3aa8f47c191d8029d522f08e4c1b14a1694686373fb781": "G9c1ef5e70be5684ec6d0267e0fc0c526ed9d095338d7d9832ba4fa225daf12d1", + "G68fecfe183d3d874fd24f1c1f516854d79d643df0e6fbefb64b50d1ddffb305b": "G7a12b47023c873d34f89671f2dd3d967af1fbc190ca6565d3a9d4e47a764372f", + "G7998550b0215cb2dd7a7fd39de62577972c95b69e127a044362ae2d09157590e": "G4f2f1e9c58fe75fc1d4adb980abbd4ccef0c8e2a458eebd2dfe5c61e402a1ba4", + "G39ecef606cbc725421f980588673387079ee68fcbad3f0284a65abef8cd1f867": "G7107e4419481cd400f06fe820457cfcc57868665d07f987ef19b129432a7ebf2", + "Gd0acd0dfd8f9f91ae9b6aa7734fb726c0a8e83e67d7771d8c72d64311708e750": "G6888d8c1566ce138427dfdd51ff1e7ebe5a7272b1ebf5ef88c119e0565f2d69c", + "Gc3663f8188539baf2fc8dc21b5be903a9086ca25367b8311db9f7bf0f834f761": "G943e04101cb28a01426725ec45387a3a9de9bdbb999acaa0478f2e482c4da76b", + "G5a250fcecae6a6b81897f1995aa49a885def0f49e612d0002d401d5f522fd38d": "Ge8bef7d934fe71987f467f45f1c7d335062cc66470775c67f85fde23e8eea88a", + "G7a2666b1e2568598202eccdcf1de4fdcfe40812f4a33e9d8ca4e2073e5a7e864": "G202700527e32bf7de07b254e7b5c4a3629e8d15c428a5a1f22d2b20b21232a63", + "G23c50bcf3db50731d51d3e0c33d2572c83fdbcc12350d9e11190d3566fae9032": "G247303e9e1b7a1c4863c6024ec6e9bba3e8da377cf8ea44b925b296c98594d7e", + "G3c62ea2e1321ac2629743faa53c42016a006c714881cd99ec0437919285ed5d5": "G09ee2358ba001fc10bf09472c2f4b1edcf4a512c9a77661baf3cd37464ec0584", + "G6407753fe57a62da297fde4b8c638ccc14211e2288c1af3fb575784e4e88d154": "G56caf3712a9a9805e1ee8eb7fe86673b64314baad10bbfc0e8c86b679051b444", + "Gcdc3cf2ac89b63f88e687f718102c99ecb24769ea33e97ed4642b0e8380af63d": "G698f0bd2b5a3859478a49898f499bad6c84ec7989cec5822b5390ae33fcca96f", + "Ge7541ba6c58aaf29c0e66f1b2709ffb88f558d932caeeccf2e4cd1af5f94d087": "G65278d2891251faf37d35823634a41b75f05843239b9ec80961e1ffc99f023f2", + "G722a7884a3552c4e449b11c83ee0883e8e0d3e246db5c53ea1814cc295784c91": "Ge7914262f1dabad65f3c12a74f2fe9dc72e622e35bac5a5ab7da601907687c8f", + "G36a064626e4138a85f72f40a610fa4b320a8627d7b143043f226bbff50e8d457": "G9096ca58f8aa719866a945404c0a03fdfb2ab9c6c0cd5628e8e82cfa22ca6162", + "G82d70fe20b48a11d416a61ea462a0b8cc1bd6eb4a9b3eb545a30263cafab28a7": "G780126a57153930857d9bb1e3a8aa071f60fe58bf3dbf11a49033b0f249fb483", + "G8abfe67c7680a61f6c1d1e98906475f7beacabe66e31f5845456bb7fbd13f0bc": "G2f368ae70e177729fbbe2b809198761ee4e374e8d64ecda15495df7a40e02cf2", + "G8cd2cfb82e8a476a66c7a8c3f6b60f7958a8acf2a4120181fbda9f59db564e39": "G130f6090577b551ba3a65bb2da44e5807d407c41f9dac35aa4ed259e0eb21d8f", + "G67afe3fcc2835cf40b85cf1253a352e9555d1ac66336d6482581069dc032232d": "Gc0447d29f102fc5bec6236e7cc2652568c0c32e01fcfb5f1fbc78d204b91b0d0", + "Gcd4e04fc14af725f3d1ea3671e23adc4a5c536d6fe9a7e9f6cbd87483f028fc7": "Gf1dbcfa03d5e4496dea64d2093673b70762d6d66a3c5cc19cb329bd34d89841e", + "G0b172f023b29fa35dc5c18dd4a764435b4877636c1d99e2960e91795ca83dd80": "G8980504c85d097d4b41a7a803ae476a050d2f82d00cb8c648feee4abc228d64c", + "G3ce2fa38830ea34a4c106e80c57c082fb71e6a0f13570e3877d8d116c325de20": "G1ba1112c67f42f72661953a758f9d0fa41798e73337a2e42851bef711e489f92", + "G37eedc8030f77487c846f6c0b47e51e420501991ea5b2a6bcf1b2c7204f203c0": "G0f51e650348ed098980ff3ef3a09392e01cc66279959e3e6dde3cc502ead527c", + "Gbb19cb56b35c8e0c75234944c7b3499a68cd0db804b9812079d2f4b8e4a2ebab": "G1a79132331135eca1bb77173e9ce844490b4624a7ecc5d7ffae02e40f4378404", + "G3d3c2ca6e7e7ca8c13b6abf0fba5f35492a2e6e0ea3918b4370fef8575e014dd": "Ge06603c39c07860b82c2d49cfe4303c386e8bd88cfafaf991932963112fbdf39", + "Ge9b0dffe433395244dde749564ada73c29933739e8207d257bbc94c35642a83e": "G3bf0295c50cf8431f0d2c6aa13595d7dc4dcdf0738ed4288453a9047c3f1fe86", + "G34ce6ded60134f35b48b4b9fb7f32e0757f9b2c254ba14b7898f93a49c03433d": "G475ca03a09c5035de96d162dbae307e016cb16f14d7215817b5fbf613c88ae35", + "Gd11be9670bd13f96a70dab4ca5f60d0d929b9cb26dd35f9c2775e539f68ab491": "G044dccb6b25fc04451e9100bbfaa3b634dd3556c161f7b4a6c1da40d502913d6", + "G685db835351b0918d0e71aefc4b10cbfac7a9e7e23294b99423eac41ce63df08": "G0a1437544cfd3bc60d98e11db00ec4aef55df24454e63fa986d6de3cbd9c02f7", + "Gd04e7f4fdee9c4173dee2b1a40111f88617c0d6cbffa4880c26130e1c7969b80": "Gfbf6843a90e7657e22782f9cee31c2e6448047a4afae1432d7dffc6b7c1f8305", + "G3c76b02aba28d133f4df7d4ae457d990f73a5e9d85cb1390d7b86ef4e6134151": "G1bd1da343629dd0fa15bcfe24cc5cc0e67f0d67b255c9528b0428e47be9813ad", + "G31b1b568376afa7187b1ee3aa89eeed0b8d0daa42cff051679511bf6856e6101": "G0515f7a6c498cc4ff2712a4178010910de0454c135b1fd0ea9710a96b019966a", + "Gb722c2d8c46d04ad1d0d4147b92f42b2dfba8cb5cd0160fbe1424b2f93af81b2": "Ge89577e221b6a55ff840aebd8f9ba403f6e219f9d034ea835e978292ec1cb479", + "Ge120b5fd900dbf3d2bd80e08dc8abd58709ea5d9ca97c7a0bf3e916a02a1cfe2": "G8924c90d4678ac3d0495f6ad67ae4ea07c752a5a6d28b2629f26b8d98dbd9e5c", + "G6783d25ac9d92c0bc1927fe553c50cda7bb878748df6275f4a983e67ee985248": "G607d0a0684e41637139fef8ff230f41bb909942c9a94f57f7d0485a0628264ce", + "G984819589a24c33cf390855b5ce944ac5505632b912fc30f6abbeebdb08ecb1d": "G72bbfc671c0f211355085393012b9835ac65d8491ff2431f878626f89df7eb07", + "G446cf9f06120cf2bbbdcb991c949d15943b1b4324d3d3d20e478a07efa9d0f88": "G90bcbf690a855cc9be365f22c9fcfe47e1e73b158a2cc2ea228e14452825f354", + "Ga18826414797270c316fd9b3838e73214ab45478c317590fb3cd9d9a0953aff0": "G6358c8004675bb53ae6bde30a8cd93735aa8fd123438cd93256d70fbd6f29cc6", + "G4a4220694788149fc08096febe84ddc584b26b6c9ab40c4b0a46bd2acb897b20": "Gfb2a810bd50e3d3813a6fbd939119c340bf250cfa9843b65f61bdc8301b74ef9", + "G77a7f616a426fd5d305ab73ed4f4cbad6c22357a9bf20d0fea763e9d2c0ca2fd": "G4a159b7973fabd82ea7e51bc96d58fc94df7fe15077769d711a155445b9eff63", + "G380901c2d644407924611992a93ba2d23709366d261e73d671561589c4657df7": "G259ab31e08b751f67b3fc496de2c4ae262274962ec5589e2e4a2880b70d96857", + "Gbcae3cba420afc06b123e43ba6c2305d14972e0f75ce87bf97dd09fc9c7381bd": "G899d13fe5225f7fca4cc5a3ad77afe02ef7f03a874d3ee2ac5a1a177b9240acf", + "G53b10706ce6041409c4921177d36d0d700e8c9366efe7ad8e34660b07df1087b": "G9540d88dcc938ebb0ef124b5f031eadc0966c826023e9719c59e349487cee00d", + "G1e61ca0b8b1941e8fa72261d8f08285fcdd919f8438017a46bcbbb059961910c": "G1f9940b18b32059b73851cfd2d813ed952f6ae55a917fba2f2318091d5323df9", + "Gd39322849e696f84aeb5f0a80ec4420ee56c6ef7c05fd430c436d2fb7ea2bbdd": "G00b9df1cc7183235c56613655d77b35df8a9716a4ff456b86629269fa579a1a4", + "G2667d24e2fb0015183e6d9d9e0bf277e1026ed2b1020d1787673ffa6692b9506": "G465cbe195779daf0dd8449ead358f77dd3047c01538d8a8177fcca7f6c7732b9", + "G05c2ab5e047aa67b80ff338e4c4e108f784b129105970a8ea7dad02bb488b3ef": "G9957c837ce04f4494df641b057cfc3161fdc8a4ec9898dc136548f46f75bc41e", + "Gb287f5494eee0a93e4a6f890546af25d5ae3a4f2e78b58f2fd31040c40fa354e": "G0f88e481ccc93429f62c7124103bb0d29c80eaf46265cb42ebf873fc1cded1ef", + "G56a9563f7da4e29b97be8dcdc95a384e7bac136566e1170c9b60d38fc79314c5": "Gba598c9c26a325bb63a54c9fb6be633ef19f4c23e13f569204d3d586a4b6670e", + "Ga08f0d8f1b4616acfac36826684318e4335f31106235eca6fbda80bb7dd36018": "G13a74d151e58d9e4edea8c247804cacc51d435c831e355408ecfe4aef8131942", + "G42f1df315e5648de270061cdc00adbdd07c4fcb5ba34dee344063e94b076ac4c": "G6b92ca680c48f3df1ccf2772438f75abbcc1de394ec6c2ddd6e5fd7727cfef63", + "G32afd19762c11d926ae794f659b99c31a0c21097866a792e95b931a49dddbdfd": "G011d1f17d3a353d69ad87465ea6962d8e83843c4f6cf0caf0e8a45792800ea09", + "G0e1f2bf6e45848b28311505260a5c04951ce9f5fd199363e5fc8fee420c39a17": "Ge11fd188ac70c54f6b37465777598f77980ab49565ab206118a5b85a73d47194", + "Gb29e1c2989fc5cf181ad34af23025d56a39ba5481e10f67ee468b229328b404d": "G8bbc3e020dba18d3d294f2a51f722e1460b047324a80b15c44db4eb81de65729", + "Gb00614020a091ce7df081bbab49e16c50715f4d9bae0fb4a643c55ee3d4ce88a": "G5882d96f0ef38eafb99037fdfc4e61a1fcc0038d5c19bef289f5ab055163f83d", + "Gf827d9b6ab5f37ced9ffc825bf9bff85d16d597a6f49c4008c771d38becf1e95": "G3a85c5204e9708532cf7f0c038bc6e65cfc929a40d090faaf361a07fb6b8bfa6", + "G0d74c382453d4fea5706533e1d748519b2b566351eb09a1f13704630c20eda31": "G10db40bef5ef910ec3b247621889e66bc431220746a5e536c45d4f8bf0386f36", + "Geee0d6a1d6729fa68e90d32453453f0ea89d18d98092ea65db8944d3d9923b74": "G5234d7e017a9d520b2714f84bd66ff9ac28e04573e4afff04249bcef6ed7c0ed", + "G8a9df565dad7b7eeb7e4f55328f0a0a218e807792875881c3011a5220f387f0b": "G10b47f5c3084d79bc92d45e616dcfb7dc3a7040aa3155fd49397352be6f5b6fa", + "G06c67b34e5d84643f0a6b863f7323a7497483d96c92ea3e1018d1b209801534e": "G582b57b4ce8cf63c44690d329d9e80f9701a841ca3f8c8dc291dbdcfb545a38d", + "Gcc178c0d77ede6a01125d09e2c9df9cf6257620178b00a7d5a791b1d532bd737": "G7d14143c6d71c5a9412d4ee87816eba1b926e1f9e282d482cf4c8f8c1bdaebb0", + "Gc987927c7c4b07a08d4fd8c8bd21999f7c59e8dd7201510b2dd18aeb590e30ff": "G04fe718f0483fb71361bb3bd82d42674b25ca069823831c8907f52f7a68b88c0", + "Gb454a482cd31c757cafd931aa41542fa60c219c13d46214c8514c10b5c792d59": "G9c178aa1967a795a17a9eee9c84bbabd63b4cedc3223c5c6fd245839729718df", + "G0c761389ef0fbbad848457ddd0e98fc55c282180271ad16ca4c9815846fec994": "G54b92c7e85b819464c7bb21b9c1665fc745c13f289ebe2946ea36bc6f0e074ac", + "G9cc798bd379dad47ba50176b236b09209a448b6eae397e932169b0552485e2bc": "G647d4f2a60cf456a392b6f062725978732d1723ef93b0cc2dc1bb75302e9479b", + "G15163c2d1ddf5856d12492deacb2317cd48c47bc325a61f515dad052864b2158": "G1fb1cc0ee9612ead59f8792d4fe28ba527af5fecdea0b2198f9c7ac2c4d2abd4", + "G33d09280105bfe16aaff54dad304739e2e091c9a1e647164caf2394ee513a2e2": "G5e9b6f5ab4339cbfcc62ab4f13f0ddb4c2576573e0f25f032602d11816acd846", + "G7d0dc89e17f74a9548c10538e3e2c6e6b99e2c5e77f98699d104f4c06682a432": "Gf67e8bf457336de147cd7e3aa5fd27d3aaa33f057a66452c4a3eae73d73e0ef0" +} diff --git a/db/seeds/goals.json b/db/seeds/goals.json index 7182048e9..cef7596c4 100644 --- a/db/seeds/goals.json +++ b/db/seeds/goals.json @@ -1,7 +1,7 @@ [ { "charts": { - "data": "66b41c0e8f23758bd7191f6e03db3ad318b698b9", + "data": "C19d35e10e7481c15f10", "type": "single" }, "criteria": { @@ -10,13 +10,13 @@ "value": 5 }, "game": "iidx", - "goalID": "Gcea58fb099db74612d9654c4e8fec345154ce00c31d7525ea03efc7389a27c09", + "goalID": "G1a2499cae27572b8bddd861b28aaa01dbe4c885544f15fae5ed91765a7b924a9", "name": "HARD CLEAR Feel The Beat (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "3532382e64eaa40afb7921aa61c95bbe30fc06f1", + "data": "C19d35e10dda97aab78f", "type": "single" }, "criteria": { @@ -25,13 +25,13 @@ "value": 6 }, "game": "iidx", - "goalID": "Gb0d9e72831b267ce5f1feaa84f99080df6a9b371ce3a22b6754baca3c8afffaf", + "goalID": "Gdee336867a22a0a789ab45b863775f4351b6f70fbaa0ea1928c977179320d0ae", "name": "EX HARD CLEAR 子供の落書き帳 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "ead53d880162aff2a9005f9cab6baba8a7fcc5d6", + "data": "C19d35e10de3bc6b2d08", "type": "single" }, "criteria": { @@ -40,16 +40,13 @@ "value": 5 }, "game": "iidx", - "goalID": "Ge3165eb0547fb894c169abc11e6f541b839ec8535bef72c895b64ca06b5767ae", + "goalID": "G36f76a980802c1f06060e29f0d4d590fa06cb2ab2d5059b62e72489b98e0cdfe", "name": "HARD CLEAR TITANS RETURN (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": [ - "b76eea63de2a01fd7a1a9b6d08251a006387ea8f", - "54c09f84b1910128ca1adc2b20f44617c54d13f4" - ], + "data": ["C19d35e10d6cf3457ce4", "C19d35e10f64eaa8c280"], "type": "multi" }, "criteria": { @@ -58,13 +55,13 @@ "value": 4 }, "game": "iidx", - "goalID": "Gccdd7b7f8f476397bf624791745252d71fe5eb276f832c8dc05384ae8a5c9c6b", + "goalID": "G53d4c238478106b7a54b865f02d505f650c29b9bda8ff7ce229389b2bafd575a", "name": "CLEAR either 灼熱Beach Side Bunny (SP ANOTHER 12) or 灼熱 Pt.2 Long Train Running (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "b08a82f6f2c3f1dc086507060da5c6aa52fbdbb4", + "data": "C19d35e10e23c92de486", "type": "single" }, "criteria": { @@ -73,13 +70,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G27018d6ed0720684cd03cb99104d1f1146e391f5e53ddd0d1d488b6e06722c1e", + "goalID": "G4a86b8c4a0e2c60bbf713d1be8f4ba7e4892ede6954a499beb8100f41ce78391", "name": "HARD CLEAR Timepiece phase II (CN Ver.) (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "0e1fbd5bfe34351cb8c4869eddebc34241ec4546", + "data": "C19d35e10b9930d1f331", "type": "single" }, "criteria": { @@ -88,13 +85,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G3ba44cf2fa1a53bb8de5cc99d5d4c5208ebd06250c2052393d7a4a830ab72f63", + "goalID": "G6da6680177acd81b493da1c8163174b6ac517c6463b25f0259f377bc9f2a3966", "name": "AAA AA (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "ee20096270986182b3741f3c4167d106ed6b74c5", + "data": "C19d35e10ccc65fb2af7", "type": "single" }, "criteria": { @@ -103,13 +100,13 @@ "value": 98 }, "game": "iidx", - "goalID": "G2ffadcc4aac205cc4af0c91e2c20dcb1a68564d591d7c802467e7122b70a7fda", + "goalID": "G749a8d3ac2bf391d65aee7491bd64bf0b20097ac4c097bc9dd0f42bccf5da047", "name": "Get 98% on Fly Above (SP ANOTHER 8)", "playtype": "SP" }, { "charts": { - "data": "293679f8eb416ddaefff758e4b9b72cb580afd47", + "data": "C19d35e10c9cf8814c74", "type": "single" }, "criteria": { @@ -118,13 +115,13 @@ "value": 98 }, "game": "iidx", - "goalID": "Ge536bd96adc49bb76c4168fe6121e8e0c046a9c82c8e22a72f9150ae9ca317b8", + "goalID": "G825995e16f75db2a31953a41564f3ceabdb250a3388843daafd527098989d78f", "name": "Get 98% on the trigger of innocence (SP ANOTHER 9)", "playtype": "SP" }, { "charts": { - "data": "751e03fdb173415dea50f56694e999ee86b9a28b", + "data": "C19d35e10c48d4251b33", "type": "single" }, "criteria": { @@ -133,13 +130,13 @@ "value": 98 }, "game": "iidx", - "goalID": "G76994cf527cc8f78cc5bec3591439b980087e8e5a305e04e418d70b4b8556d61", + "goalID": "G5b99b92615fc9d1554e8bec4264885d642a5dd5ab9daba82cfc840da6261fcb0", "name": "Get 98% on CROSSROAD ~Left Story~ (SP ANOTHER 10)", "playtype": "SP" }, { "charts": { - "data": "aa3999bf49c4e3f24f66b1e0d79f9120fc4cb61c", + "data": "C19d35e10d349b35e88b", "type": "single" }, "criteria": { @@ -148,13 +145,13 @@ "value": 8 }, "game": "iidx", - "goalID": "G9758f5f089bdc9757ad5991b1282f4ed39f5a197343f1fbd3d9f6a5a5985f8e3", + "goalID": "G39cde64528a80e9a7b22f6a612d5567945c9e1e7a6e6bdb7275ff39029d98a71", "name": "MAX- mosaic (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "e9e7b62210fae1aff51a485fd971968e846c5ccd", + "data": "C19d35e10aa354e019b2", "type": "single" }, "criteria": { @@ -163,13 +160,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G4fd0a9ec034100c7bbf40527b0bc55be71617c545f06d4c7feb4795fc5ed0601", + "goalID": "G2634ee3209ddc4d87edb90c34471ea8603bf22885ac53daae36979fcfe656182", "name": "AAA Happy Wedding (SP ANOTHER 8)", "playtype": "SP" }, { "charts": { - "data": "1dce504149aed886a1e3de31a27fe7df277dae9b", + "data": "C19d35e10db9c840b431", "type": "single" }, "criteria": { @@ -178,13 +175,13 @@ "value": 8 }, "game": "iidx", - "goalID": "G018c3d8a77fb97bf5534f29ff960797f82effd1c8c3852da7ddf62170bf0fbba", + "goalID": "G1dafbb9a067708d0d52610a52d9e3d9efdd1ee8c40198f23a7bb219e78c5513b", "name": "MAX- SHADE (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -194,13 +191,13 @@ "value": 5 }, "game": "iidx", - "goalID": "Ga13da591f9b95e132a7a1947603745649494154cec51d1dc427a9b83633edb64", + "goalID": "Gf460944fe4fa8d1cd8b1db041e9475b33e4d25f6dc49b383ea144d4e5180dd4e", "name": "HARD CLEAR 80% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -210,13 +207,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G4f7d560c4b930e1c689c8aa10c87fc8bb873231da207f7375dbcdac46b6ecf35", + "goalID": "G1f254c17d443890622d34cee5ea0e216184c9637f76e1266eb99e0540da9fdc6", "name": "AAA 15% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "5f94fe9c3bdc18c440e7ba2dff824adaa2c9d3a9", + "data": "C19d35e10ee2d55ce89f", "type": "single" }, "criteria": { @@ -225,13 +222,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G226a21e1f284a93142fe1259067fceef84ba44eb1dc8d5fb02059f0c30d2b39b", + "goalID": "G8a7ec915d0613ce018202caa1e16dc9dbbf5ce18cb391110bc091e9136da6e00", "name": "CLEAR Chrono Diver -PENDULUMs- (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "5a8a6b9304857b2c4316ae5903fc60bfb8369270", + "data": "C19d35e10e72067256c4", "type": "single" }, "criteria": { @@ -240,13 +237,13 @@ "value": 5 }, "game": "iidx", - "goalID": "Gc3a2eeb0712812682aa514e0029cb4c1b2617f5c1ed51d80319f796f388ed29b", + "goalID": "Gcf8d759883e71996af9623499956d3d6ee6e591b7a58e6b8698469580ea2212d", "name": "HARD CLEAR Verflucht (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "fac63d4c66de9207490de8477161c0f7020ab04d", + "data": "C19d35e10fadceccb5a3", "type": "single" }, "criteria": { @@ -255,13 +252,13 @@ "value": 4 }, "game": "iidx", - "goalID": "Gb36433d9e006d2371ce7cd337b798ec63afee190a796b5cd9c4e901c86c973df", + "goalID": "Gd467e4325e60010906055a976204bffd89a5fa805fbcf898c62618a8f81a57a3", "name": "CLEAR ECHIDNA (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "0d10ce53e74bb3123bec65ad80bc55ebf15522c2", + "data": "C19d35e10c06ae55d103", "type": "single" }, "criteria": { @@ -270,13 +267,13 @@ "value": 5 }, "game": "iidx", - "goalID": "Ge0238448ca7e640b3c2330b4e8352eda8ce728de9b49f80b25fe5e69bd34a6f0", + "goalID": "G01d884675908fc9e06a7e5b477afd10bf932a06ccbef8dfa25d5eb40ebedc703", "name": "HARD CLEAR Ganymede (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "cff493858721bb0bfc31ffcd27110f52fc67027a", + "data": "C19d35e10c901a0bc56b", "type": "single" }, "criteria": { @@ -285,13 +282,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G1381f81b6b7e50e8834c37fac064c264da3d5b082424b9427a5abbde12e1a46e", + "goalID": "G9cf0ca4046641e30a86ecfc206b095bc58c344907840e81a3f6f96ce7897dbc1", "name": "CLEAR Time to Air (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "11c2e365f1c8b4b260a8b2042b656287ddff9ce3", + "data": "C19d35e110fc503ba1c9", "type": "single" }, "criteria": { @@ -300,13 +297,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G200430b184083eaaf5e70448db9923b69aca66472f45ce981e150a53f2deb9db", + "goalID": "G9ec817b5b48790e7655be86536ea9f3b977d4b8f33bd2c0fef4b5675eeef5bb4", "name": "CLEAR Lords Of The Roundtable (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "4771e0a786f920a026227248e7baa5a4664e37b9", + "data": "C19d35e10c66db4e6b52", "type": "single" }, "criteria": { @@ -315,13 +312,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G6bc92b864cdc885e5b68afcd743a552a7c9f4b42d2388cf66003472867e3bd4e", + "goalID": "Gb8464c140da50ad417aeaf8223fe260b3f88a992c6c88bb031d72cccba48a55a", "name": "AAA snow storm (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "5e3dda64f5ab8c00855428695d6c4980818d9c8a", + "data": "C19d35e10c23bba5d50d", "type": "single" }, "criteria": { @@ -330,13 +327,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G8c14a7e978b55b03be418b6d57b4b828b974b1d927bcf23d3b6b2489259004ef", + "goalID": "Ge0973c181b48cbf54a3f6d14946f867a0ac238c3cd39bc572f9e64601652ace3", "name": "AAA カゴノトリ~弐式~ (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "72f09e8bf2501d9e0e79e4ad033ae05684d7e6a1", + "data": "C19d35e10fb2a84e0979", "type": "single" }, "criteria": { @@ -345,13 +342,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gef6390558f110e73135f38ba3693a2a0bbbfebf79f5d05a18ac542037c6e82f7", + "goalID": "Gb3bb20d0b86bf071d17a94a8a97b089486dcea29956d1e4561c17b0fe02afb36", "name": "AAA (This Is Not) The Angels (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "7b1dea115384791aebc0217eb54e3a50a0f16c50", + "data": "C19d35e10b0ea35c665e", "type": "single" }, "criteria": { @@ -360,13 +357,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G960274ddf4dea2cacd559f121430ec00ea676f7549a98477b0bc4513784755d0", + "goalID": "G999a566fdad5912d5ba7bdd4ecad09438215ba1cca2a7085ae7e9404178d44ae", "name": "AAA quasar (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "906c67a8d37dea58b9163a020a505dc1ae877d88", + "data": "C19d35e10d8f3ccafa63", "type": "single" }, "criteria": { @@ -375,13 +372,13 @@ "value": 8 }, "game": "iidx", - "goalID": "G3fa6d1586ce8508e3d479d5e78eb00fa137b014bbd75d932312b0ee70c386b15", + "goalID": "Gf4e2efb99bcbedcec5afe6128ad9b48c2329bc604e9f3676d8b77e4bc68708f6", "name": "MAX- Sakura Reflection (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "cd5b930ea606a039e65ef9ad91bc014d3d2004d9", + "data": "C19d35e10ca3d803cf57", "type": "single" }, "criteria": { @@ -390,13 +387,13 @@ "value": 96 }, "game": "iidx", - "goalID": "Gd296f58f71491c79d6a957e4d1f51b0eeba55cc2c18c7cd2dd6daaa72b8c1fac", + "goalID": "Gd1c17cae156a8cc9993adafce8924bfee58558c51d59eec87c687c281499dc8e", "name": "Get 96% on Wanna Party? (SP ANOTHER 8)", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -406,13 +403,13 @@ "value": 5 }, "game": "iidx", - "goalID": "Gb42704d7405a574587b6e56732f13326c48f56b9ecb9a8ae5ed245d56999dd46", + "goalID": "G661132cce7d7b205f4ccc463073f7eccdd8ad0700539f5f86fff216be4a62799", "name": "HARD CLEAR 70% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -422,13 +419,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G0ed4c552766ea82145c8a9fbf4ffa1df3fe5645c5141db3f48ec7c5af0513c4d", + "goalID": "Gd064ea10a7d7a330cde62b2047047bb7116f769b56082efb3fef7a95f9c0d125", "name": "AAA 5% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F7aaf1fe3b7bc5a346106a596e67ae2c86462c671d11c7f1db1422d8e7a421735", + "data": "F19d35f0cf28bc354ac6", "type": "folder" }, "criteria": { @@ -438,13 +435,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G31d3218b8e4a5f4c860d9b2f29247472cb9481f8b81a0e2829ec6d4e9ec1a040", + "goalID": "G34bcb7d0ec46bf486014ff7cccaf4b059572a58944d2c31ad628126ac8826358", "name": "AAA 10% of the charts in the Level 11 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "924bf011fdd8334b609b02e382123f9f5440d16d", + "data": "C19d35e10f99dfa6293c", "type": "single" }, "criteria": { @@ -453,13 +450,13 @@ "value": 3 }, "game": "iidx", - "goalID": "Ga5dc820413f88b58e703f615324e0ee07aced954a62a601b42756bbefd648c60", + "goalID": "Ge2ff55571ba96a533f278c0536473d0f16379e306b775cc1c63c20c5c833d201", "name": "EASY CLEAR Mare Nectaris (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "8afd1828f839cb76d6ca05f39cdfd8ff81ae4291", + "data": "C19d35e10fd32cd83a72", "type": "single" }, "criteria": { @@ -468,13 +465,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G71bd61d5846a87127e447c0e6be298e320b1ba8547107b4c29ff1fbc4e976e94", + "goalID": "G16d80e16e4657d413c93b203c6938432159f53b93dc05ead19dca8614b5f9819", "name": "CLEAR SAMURAI-Scramble (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "3207f18df11e80fba6d8dffd974d244870818add", + "data": "C19d35e10e702796c211", "type": "single" }, "criteria": { @@ -483,13 +480,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G3be852df2c79b97a22ce7b5b3c04846e992d0a2a989aaeac89b668bd4ac4a3aa", + "goalID": "G2f376d12b93d701297b28c53b6bbbce4b44a61156bc739288a2ed70fd4f6c5d1", "name": "HARD CLEAR 疾風迅雷 (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "941ea3ea41ca712be3298546f7d1ed2dfff6daee", + "data": "C19d35e10e725f7d4aa1", "type": "single" }, "criteria": { @@ -498,13 +495,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G39cc151b00da6e08a49d2c1ffa28b376e600cbb0d30ba369e94abb4cb909522d", + "goalID": "G48490053cd8c865b0b7341b8728f8dcfc6b22c7a93a06cd14b328d0811e12b36", "name": "HARD CLEAR Verflucht (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "e826dec4285f65c5fcdb2c1af11bc721cc8664eb", + "data": "C19d35e110388eaae351", "type": "single" }, "criteria": { @@ -513,13 +510,13 @@ "value": 6 }, "game": "iidx", - "goalID": "G69167cb3007cbc10ff3f56435cee6133af5a3ab44d8b90f482342cb277942c76", + "goalID": "G53a3bad5acd46acc7ec0334bc4b4b2c317583e398e097bd219e148ccde0541f1", "name": "EX HARD CLEAR お菓子の王国 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "30132aa340a50383128522eece24b86eb3c64c47", + "data": "C19d35e10ccb9d731ae8", "type": "single" }, "criteria": { @@ -528,13 +525,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gbff5754db52ea104e33fa633c50646da699ccfaefc63e5b4999ce389d0db7ce3", + "goalID": "Gfbab43227c6ad53f49a2333114f97412467b657164750f0f5288a64982f42eeb", "name": "AAA BITTER CHOCOLATE STRIKER (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "cea0eadee4ae414d91360c88f2c69c1b0e6f6681", + "data": "C19d35e10f007d7641ea", "type": "single" }, "criteria": { @@ -543,13 +540,13 @@ "value": 8 }, "game": "iidx", - "goalID": "G8621b92ec1aa8a15312e664940f216f500ca6a068405ff384e3c4fb846b81bfc", + "goalID": "G19c09c3f23a7f2216d7a0fda3a525ce614f772a4fd455ccf40aa7f8f7a26b7ab", "name": "MAX- 千年ノ理 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "38bd9ce59428447155842131b331a73bef16947a", + "data": "C19d35e10c7a843f209f", "type": "single" }, "criteria": { @@ -558,13 +555,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G2935018dd82616079e0c56953f0de19a01afedc63b07c7e83003f9a9c76d17c8", + "goalID": "Gd29cd861d7aaf7d27050950e69126f8f92813e96804f440458f3009a4079cbd6", "name": "AAA Blue Rain (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "f98b2075a9ecb1b6b1962718cfd508dc99b180c4", + "data": "C19d35e10f95f5d8fdfd", "type": "single" }, "criteria": { @@ -573,13 +570,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G298166932cfa50abfd5fdb67f5e16e5a46bd9c8b83be93db98147156bf82c872", + "goalID": "Gfb59b854a2d0f8dfc56cab487f4b43f4098cad073bfca42b99edb125d20e9c61", "name": "AAA Rave Cannon (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "50e49f508e3001c8d384a8b9c6a6f4e197964117", + "data": "C19d35e10ce1a6cd40a5", "type": "single" }, "criteria": { @@ -588,13 +585,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G37750486bcc1c26f93958af699d0e1ecf8201e8a021ea2e17bc25707a87ce5a9", + "goalID": "G08ed442b3f0210371e99276442281c34d4f6703bbb301a229502ef5054f1ffd9", "name": "AAA naughty girl@Queen's Palace (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "9683f55a11e42a5aa5475376814f5d3f2d2c4a28", + "data": "C19d35e1100a6051cfaf", "type": "single" }, "criteria": { @@ -603,13 +600,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Ga5db62596cf669c2593b8b03329e6608026fafc35ef5c2238416bb6a28b9f62a", + "goalID": "G2be92dc54829e1da6016d6145b324b1a43e1ecefca262dd4bfdb68a9f35cc43f", "name": "AAA L.F.O (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "7b1dea115384791aebc0217eb54e3a50a0f16c50", + "data": "C19d35e10b0ea35c665e", "type": "single" }, "criteria": { @@ -618,13 +615,13 @@ "value": 8 }, "game": "iidx", - "goalID": "Gbed2eb0c5ce16d22ee66f0c07b921ef9d96bd3fe80f5a943aa52f0147d889e68", + "goalID": "G8392d2f9da662e5a7034af3e0264bb447e290d539ba4dc46ddc185f5063965d5", "name": "MAX- quasar (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "77da55f73956c4ddb3029260648062d57edea6f1", + "data": "C19d35e110c03b300e12", "type": "single" }, "criteria": { @@ -633,13 +630,13 @@ "value": 99 }, "game": "iidx", - "goalID": "G4e6cc043b09a7cb0fa8b7ae3b0c970f5c6de6bc78ec2ab5625785586ef9fbd4e", + "goalID": "G786aa0be57281d82ebe04aefdc20f4722bb6ace8e9bcb8768db4db03b1375005", "name": "Get 99% on Sleepless days (SP ANOTHER 9)", "playtype": "SP" }, { "charts": { - "data": "0781ab04ceaaa5bcd436322832af46684ddb305f", + "data": "C19d35e10c06ece39c47", "type": "single" }, "criteria": { @@ -648,13 +645,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gbeb7bbd19ed0eed65f8166d02a33091b8ad03228fc19ef560167efdf6a07f33c", + "goalID": "Gf0c07a8501880c2fe56826d3c8c852563851d40b47fde07c1aeff780a83cdc2b", "name": "AAA Ganymede (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -664,13 +661,13 @@ "value": 3 }, "game": "iidx", - "goalID": "G2bb7ce1fc898f028e3a525ed15bb991718136ce27200c1f8a59ef959df239589", + "goalID": "G8eb9a981f9671033f078269ac98b382f46f844649a30cea54b3be26332fdfc47", "name": "EASY CLEAR 100% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -680,13 +677,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G9a06f0cc8758cc19dce70924849e633dededec6022aab385ced2b887d4cca597", + "goalID": "G0e1c4e8dc2bd01a3437ef728654b0edb63ad1b57f869e5cee651c1fe289b2b97", "name": "AAA 30% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "0b60c515b4b222d0684d4b956c1aa409db0baada", + "data": "C19d35e10c3a32758e55", "type": "single" }, "criteria": { @@ -695,13 +692,13 @@ "value": 6 }, "game": "iidx", - "goalID": "G9930408e8aaadb119e8bc22a707aad05dbf362a60c7bd7e445b6886ff2831d3c", + "goalID": "G0195d9b1603ed74eaa038f5363cdd676eeb63b30a386746cb6b349aad6f09c96", "name": "EX HARD CLEAR VANESSA (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "8b72cb6971dc27ed793de77e53f48234f1d1bfe7", + "data": "C19d35e10ebbaefebf9e", "type": "single" }, "criteria": { @@ -710,13 +707,13 @@ "value": 6 }, "game": "iidx", - "goalID": "Gcaf0a44d2509e511fa767beb1638b22a584ff1adcbc4940f1e598135fc27b736", + "goalID": "Gbd03b00f85711c94f00fea90089076f48a9cccda65f23bd3ed3f4fb35e01ec8c", "name": "EX HARD CLEAR 共鳴遊戯の華 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "eedbf725a86ae88dcdb95db92739783bb88facb7", + "data": "C19d35e10e7be9bbea22", "type": "single" }, "criteria": { @@ -725,13 +722,13 @@ "value": 6 }, "game": "iidx", - "goalID": "G754bf911321ae722562c301aa00487fb19c0ae4271de27a9733a177ed63df5db", + "goalID": "G8e816f07a69b9ef1e0004ff1b0cba95fc41149a4b01c52f98f20f3f00ae07be3", "name": "EX HARD CLEAR Sigmund (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "924bf011fdd8334b609b02e382123f9f5440d16d", + "data": "C19d35e10f99dfa6293c", "type": "single" }, "criteria": { @@ -740,13 +737,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G654f58332e9a852e4a9b5bcf77f8b68f722afc10580beca72b5b0c422c7b2ef1", + "goalID": "G6c8e79d4902b4cf743a8afe439b1a1e0abdb7f8f21eb5039c6a629532e60a906", "name": "HARD CLEAR Mare Nectaris (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "b40b86ed92909dacbe3f7ecb21e92ea4d1e76c60", + "data": "C19d35e10bb035d085c7", "type": "single" }, "criteria": { @@ -755,13 +752,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G89cec4ec11dd11288f01c807ce2ff0f52fdc75aac33ccdbf1f100f3f829620d2", + "goalID": "G57eb13343a058c69a5698d1ac367b92fd72ceb3da411e8dac0398a0768a5a4bb", "name": "HARD CLEAR 冥 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "626bab7cbee43b78846fa7742df3a4599e0926ce", + "data": "C19d35e10c8df7878eed", "type": "single" }, "criteria": { @@ -770,13 +767,13 @@ "value": 5 }, "game": "iidx", - "goalID": "Gad1b5c3e243dc1255cb3d862df58f116af45c5629863566118e3a2feb0869226", + "goalID": "G7f083af0e8e9947647a900cdda9c31d034508dbaae52c0409443a3b3ad21d59f", "name": "HARD CLEAR ICARUS (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "e064e485f02479fe47bb16ff5a347ef21ae1ff3a", + "data": "C19d35e10d28511d0a70", "type": "single" }, "criteria": { @@ -785,13 +782,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G0d4820d485f9cffeee870edbbf8edd923936905e9ebcdc50a83d23de7ec6a159", + "goalID": "G3d1c87e9352934da393602ab72e09d975ead0e6967d11f55fedb62c45d5859f5", "name": "AAA Red. by Full Metal Jacket (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "a350b9499abd6ba8e377419bad41c79e7a351950", + "data": "C19d35e10ff0d16fa61b", "type": "single" }, "criteria": { @@ -800,13 +797,13 @@ "value": 8 }, "game": "iidx", - "goalID": "Ga35dd5e0aa06e9c44e4bbb1112619474756fbbd301a6e7c642b817c62ef0d455", + "goalID": "G83e9153d064fa975a7c189ce7e881fa9f086350f4499aff3f3358033075b75ec", "name": "MAX- Summerlights(IIDX Edition) (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "5e3b344a6e268cd02949ea2485e96c26020e3018", + "data": "C19d35e10e85d8095be6", "type": "single" }, "criteria": { @@ -815,13 +812,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gdcad64d899954044c12389197968aea20d15b4fdef98c52d5ec23153c0b69c20", + "goalID": "Gd2a24516591b18582a3eff64136f2099cc177215a0e3efcedff01890733c0788", "name": "AAA Ancient Scapes (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "6930092efae33d9f38c81c16382d1a9e99df7ac7", + "data": "C19d35e10e0fdf62fb36", "type": "single" }, "criteria": { @@ -830,13 +827,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G625186895fb1153ee8920313ab4a5668038fcd9712fff32e6a50887be3e3dd7e", + "goalID": "G3663a80447cd30ab89384962e8e6b2b8c2ccbb4c7abd1c96b149bc8181c5eba2", "name": "AAA Elemental Creation (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "e93419a7db180a68db29dd3614c7aa3fd1d274ab", + "data": "C19d35e10b7a96dc977b", "type": "single" }, "criteria": { @@ -845,13 +842,13 @@ "value": 8 }, "game": "iidx", - "goalID": "Ga6b497bfeb3e9f2255ab4805ffd921c34c0abedd52f93588035bd547ff6724e9", + "goalID": "G4c7474d686b61360150065ea71908db503e9052767e39c7551d7cc4ccdbd2552", "name": "MAX- spiral galaxy (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "93a083bf32cc05675a5b2a7fd5bdda5c8ccd7c92", + "data": "C19d35e10d5d19dc96f3", "type": "single" }, "criteria": { @@ -860,13 +857,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G20789dacd98c01463a6ddee5e3cdfe39a093b2620413091e6420d63da9302473", + "goalID": "G324fc7b7a4f6b9c6c5600fc99efecd473ec3485a1dc8108be00b9682bf41bbf2", "name": "AAA Dances with Snow Fairies (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "0e1fbd5bfe34351cb8c4869eddebc34241ec4546", + "data": "C19d35e10b9930d1f331", "type": "single" }, "criteria": { @@ -875,13 +872,13 @@ "value": 8 }, "game": "iidx", - "goalID": "G2568289f555406026d89a0faa647c966f9ec9a5e97f3e73d099a6d3bce811a54", + "goalID": "G886a5a0c91eb4a0ad9402c05b12e8f6d3fc652d3effea71d3485cf50a7dbd8c6", "name": "MAX- AA (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "751e03fdb173415dea50f56694e999ee86b9a28b", + "data": "C19d35e10c48d4251b33", "type": "single" }, "criteria": { @@ -890,13 +887,13 @@ "value": 99 }, "game": "iidx", - "goalID": "G82327ca3c65a1b4750ca3605caf85c7817412fd4f179c55a85a9aad43ddd13bd", + "goalID": "G3af77e883209462687918d0f436c1596d6e3fd6678841f6785e08361a75af46f", "name": "Get 99% on CROSSROAD ~Left Story~ (SP ANOTHER 10)", "playtype": "SP" }, { "charts": { - "data": "aba1ba5d09a6e00670cb6c13862838b95fc314bb", + "data": "C19d35e10ca35fcd331f", "type": "single" }, "criteria": { @@ -905,13 +902,13 @@ "value": 97 }, "game": "iidx", - "goalID": "Gda3e8b2f99d380aa0c60f8780dacf3981ad7a09324805a4d0f58867914f560e5", + "goalID": "G78340b6bc4ce95763f797550a2941a04f1bcfcb8df5d4091d1a9ea5c24a1f7e2", "name": "Get 97% on Wanna Party? (SP LEGGENDARIA 11)", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -921,13 +918,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G6a4ff51532d93b22dfd5c8cadd408e642f6cb7a6096eb6ed079d0e98ef238d5b", + "goalID": "Ga96793d89c4924914e2a6f306277badffacf0990af24b2b3674e96f466951615", "name": "HARD CLEAR 100% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -937,13 +934,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Ge18b9d2a86bb342b414e24a6549cb1f34a6c000d679a932fb4b6568ccf4063dd", + "goalID": "G472d131ccef571f9cb2ee06e7261214f49bbc1e36a119e3c0cc366c98f23f500", "name": "AAA 70% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -953,13 +950,13 @@ "value": 8 }, "game": "iidx", - "goalID": "G6a29e0bc11a1e356f7fa1328e8b9a91fcc389be160b26ed3b408800ae6a6fd13", + "goalID": "G09f78b3a6b607c39b2fca68a7a4054323bb750b2fe70dc85ab13c72bf3d75860", "name": "MAX- 15% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "8a6c7c48659f0e929a3087b63e43bcee98a84305", + "data": "C19d35e110cb4745b16a", "type": "single" }, "criteria": { @@ -968,13 +965,13 @@ "value": 6 }, "game": "iidx", - "goalID": "Gc0bd97f34ac7a4e3e3bcc829640f6526d5bf3f4d6003cbc09b6aea147d3de1b2", + "goalID": "G7ac29a0cb0c338d930ffa76835b20049d1a81f81f69c794d3dcac8dfc6a0141e", "name": "EX HARD CLEAR X-DEN (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "924bf011fdd8334b609b02e382123f9f5440d16d", + "data": "C19d35e10f99dfa6293c", "type": "single" }, "criteria": { @@ -983,13 +980,13 @@ "value": 6 }, "game": "iidx", - "goalID": "G5f5eff96326b85ed2bc803d9424618a6da5f656e456bb4b7734cde61bc49067b", + "goalID": "Ge7067c5fb15f67bac212093da736ffbd04aa13e2915ddbc4a08450d0d3ec4a97", "name": "EX HARD CLEAR Mare Nectaris (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "b40b86ed92909dacbe3f7ecb21e92ea4d1e76c60", + "data": "C19d35e10bb035d085c7", "type": "single" }, "criteria": { @@ -998,13 +995,13 @@ "value": 6 }, "game": "iidx", - "goalID": "G723468dab17f8e083411c797b7cbe51da53b27b782413797a1931e90afc5b4e1", + "goalID": "Gacb2b831d861daf8f646169e0836465c9d1b75c879f9cae86ef70e37ac4526c2", "name": "EX HARD CLEAR 冥 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "626bab7cbee43b78846fa7742df3a4599e0926ce", + "data": "C19d35e10c8df7878eed", "type": "single" }, "criteria": { @@ -1013,13 +1010,13 @@ "value": 6 }, "game": "iidx", - "goalID": "Gc08c0654afce1b73d2c02f4593c5e29acd7c0fceb40ccc151e396fb017d314eb", + "goalID": "G18170eb62353e8bb450fcc44df2cd763b64c2dd853d943e310cf372599b7055f", "name": "EX HARD CLEAR ICARUS (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "a19082dc0aa2089b79926566019abc4c76028d89", + "data": "C19d35e10c5fd84f94e2", "type": "single" }, "criteria": { @@ -1028,13 +1025,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G3b9b888f049bf483fc17a7eb85f344e0e5c74397e949e6e501a7b17241769c20", + "goalID": "Gad8194c5f9050efcb8b12e150be83682769a61897dbd16d19851f6732951f88a", "name": "FULL COMBO KAMAITACHI (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "b40b86ed92909dacbe3f7ecb21e92ea4d1e76c60", + "data": "C19d35e10bb035d085c7", "type": "single" }, "criteria": { @@ -1043,13 +1040,13 @@ "value": 3700 }, "game": "iidx", - "goalID": "G38feae067106ecd440229bfb4c8bcc03ab927f237ddd6c698879238c8aef3835", + "goalID": "G5014da556b19342d379352a3befaff4d7271a65174dc91d8278c0c8295d2a429", "name": "Get a score of 3,700 on 冥 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "0e1fbd5bfe34351cb8c4869eddebc34241ec4546", + "data": "C19d35e10b9930d1f331", "type": "single" }, "criteria": { @@ -1058,13 +1055,13 @@ "value": 3600 }, "game": "iidx", - "goalID": "Ge43b406e4e034ec24a6deb6c447be7ec6678a7a8cdd59132f6f3cbf95165d4c1", + "goalID": "G2ff00b659f0ddf8610d606d53b02fdc8df69ff639db5c246f6c80a525c458db7", "name": "Get a score of 3,600 on AA (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "b76eea63de2a01fd7a1a9b6d08251a006387ea8f", + "data": "C19d35e10d6cf3457ce4", "type": "single" }, "criteria": { @@ -1073,16 +1070,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Ge21176059a83eef7fa8f5fc98c329b1d6909395ccfafa15ae9e0e0120177be93", + "goalID": "Gd0616dd15eec949ccd49d727b22a5fd33352fdab418e8d464313b357aca7fa13", "name": "AAA 灼熱Beach Side Bunny (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": [ - "b36b423792e515bd7f8d10d4bdbcf65f2a3d9451", - "273f633c0ace2c34bd29e19738cd076be031da8e" - ], + "data": ["C19d35e10e17abee5636", "C19d35e10da1d3c16cda"], "type": "multi" }, "criteria": { @@ -1091,13 +1085,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G6a58f1cccd41586cd4b030644249932a622dca06c523f84184c9bbc0571941d9", + "goalID": "G12f2b73cd600478a0c16c0fc6b382005b3a933865f3d11de127e499de9bcfb60", "name": "AAA either Thor's Hammer (SP ANOTHER 12) or DIAVOLO (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -1107,13 +1101,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G0f5bd097383b05e7dace9ba2cc690094d02e13756fdba1da5b3c316e84440522", + "goalID": "G2b883ffdbe765eaaebfac6e83bc0081b9c997ca0e3d1035d02ddff07d7fef2e1", "name": "AAA 100% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -1123,13 +1117,13 @@ "value": 8 }, "game": "iidx", - "goalID": "G880c284bcbb6ae7e6cf6482c613f2e045c989d52754e0e9dd726a114a572b452", + "goalID": "G61da2b79d0434ff044ceff76352d9ba9748775d1e8baf43124c8ecb6b1c8d502", "name": "MAX- 70% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "3b8ddcfffd0fc5abb76f0372a006abdc4a99e02e", + "data": "C19d35e10cd64efcc396", "type": "single" }, "criteria": { @@ -1138,13 +1132,13 @@ "value": 6 }, "game": "iidx", - "goalID": "G6c6b2c4dd15d96f336878468a5461c178f1d9bf66340468976592433db9dff88", + "goalID": "G1c9806798380fd61a84e0b9c110c728cca54f643eadf620373f84cf042d95e36", "name": "EX HARD CLEAR 卑弥呼 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "941ea3ea41ca712be3298546f7d1ed2dfff6daee", + "data": "C19d35e10e725f7d4aa1", "type": "single" }, "criteria": { @@ -1153,13 +1147,13 @@ "value": 6 }, "game": "iidx", - "goalID": "G03e076f00f51a3bacd8d0e1a3eb349b62fa6cca1241281f12dfa2f8f42060a6f", + "goalID": "Gca3a6b342a136b9b6b7427ab653d76a516aadc1c9b0a7b079b98d5c04b984118", "name": "EX HARD CLEAR Verflucht (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "b76eea63de2a01fd7a1a9b6d08251a006387ea8f", + "data": "C19d35e10d6cf3457ce4", "type": "single" }, "criteria": { @@ -1168,13 +1162,13 @@ "value": 6 }, "game": "iidx", - "goalID": "Gf41ec8a91f708368f7a60f7d8b4d77690c100916430148fef0fd8d16a7c6b568", + "goalID": "Gf151ed2cde8b6da5011d963a3f4e593c6010fd92646a7fb1c6d6738c2920a277", "name": "EX HARD CLEAR 灼熱Beach Side Bunny (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "3207f18df11e80fba6d8dffd974d244870818add", + "data": "C19d35e10e702796c211", "type": "single" }, "criteria": { @@ -1183,13 +1177,13 @@ "value": 6 }, "game": "iidx", - "goalID": "Ge2f036b10ebc5c82731cc1d99ad55852fa67e6f5e5daf8f8e1f9f2f5ea1e5725", + "goalID": "G2df81066938bb2ff7d0e166f026ec253a27fa4e8adbfe55e030872213dbb0af9", "name": "EX HARD CLEAR 疾風迅雷 (SP LEGGENDARIA 12)", "playtype": "SP" }, { "charts": { - "data": "b40b86ed92909dacbe3f7ecb21e92ea4d1e76c60", + "data": "C19d35e10bb035d085c7", "type": "single" }, "criteria": { @@ -1198,13 +1192,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gca3459a14f5838332035d3be80ff6705d12845b6294a29e9b60722b4fe57f79d", + "goalID": "G63a7f069f8942865d6ddf7b51bd8c5e1bc051671c5e3b565a8b1544d46c73b79", "name": "AAA 冥 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "22d9639b9b72406779bfa9912f43630e59a469de", + "data": "C19d35e10dbfc1d2f37f", "type": "single" }, "criteria": { @@ -1213,13 +1207,13 @@ "value": 8 }, "game": "iidx", - "goalID": "Gfc705ce59372c3d66ce7c92da9c0ebe753f8892235143cd302f1d12951224b22", + "goalID": "Gb6469efe0ad1fe92949ed1ccfc26115a2620cdd7be6ee041e99faaf6b571b90b", "name": "MAX- BLACK.by X-Cross Fade (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "018c7e303fe582d5cfebad605c7461d73d46a2b0", + "data": "C19d35e110deb33206fd", "type": "single" }, "criteria": { @@ -1228,13 +1222,13 @@ "value": 98 }, "game": "iidx", - "goalID": "G394186cda313937cb8cc70f2c47064bf9acf921b9cf31cf8260107ef32ba9add", + "goalID": "G71cca18c17f15bbb8920c269f4c989fd064b750a8e4748b1ad767116f38a6e77", "name": "Get 98% on LIGHTNING STRIKES (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "fec79942d6aed43d525ee9c4027394f8762ac441", + "data": "C19d35e1106680455fca", "type": "single" }, "criteria": { @@ -1243,13 +1237,13 @@ "value": 8 }, "game": "iidx", - "goalID": "Gf2943e13de2a701a0d83872c8d162287262400b647f43f4cab66bc37b16e3d03", + "goalID": "Gb07e04f91ccfc1eb1502800e1f54f7197829b7d1443838f5b7f2835d5d702abb", "name": "MAX- Initiation (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -1259,13 +1253,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gb7f75c5e982075bfb653516f7e3e3a99958fb9083e48b7965b1d7a30405a7529", + "goalID": "Gfa235ed890bbc75bab98cd88bc07ebd83f65ce5b14ce3249af1fdf9d816be726", "name": "AAA 95% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -1275,13 +1269,13 @@ "value": 8 }, "game": "iidx", - "goalID": "Ga26f4cafc715e6c6662abf79debc774938cc0b259b25d00397f98ec9499f94eb", + "goalID": "Ga9ed7c31b4262f6e5dfc58b3d00a8ccec11d0ba5e0b2e47c57b220919785b373", "name": "MAX- 40% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "6d0d36165f67eac018700ac922cdc3fa0ab0fc1c", + "data": "C19d35e10d1ee836ec5d", "type": "single" }, "criteria": { @@ -1290,13 +1284,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G7ca5885c32800c012cc897dc14abc2c8cac462cad7b1f4d2f88328a1f3392c35", + "goalID": "Ga0695b5a44b0fc387d0fd8e7289fecb6a2254231bee6c5624695e44192f77d23", "name": "HARD CLEAR Bad Maniacs (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "9d273085a9b4612817998c4cd280f57c7f9bae10", + "data": "C19d35e10cc5d0502e02", "type": "single" }, "criteria": { @@ -1305,13 +1299,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G8bae0ae05088cc8506aff9900c34228c5427744ada392a4f7f9dd35c3037a1e8", + "goalID": "Gbbda7dfac9675109f47f73efe54520db3d16f51876fd8a86e29eac5ec39a534e", "name": "HARD CLEAR MENDES (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "9ed42a76e1423d731ecbbb0ada7fdaa7a6d94b4e", + "data": "C19d35e10d43eb22bded", "type": "single" }, "criteria": { @@ -1320,13 +1314,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G181df38bf004505dcdde1ff8c9729f5fd39b8fa71161848c4d4d6ca6d727ec41", + "goalID": "G7265d66249d1d9bd54c1ae06b6c1936b984beef4a154ee137412bcf05d75b8e3", "name": "HARD CLEAR quell~the seventh slave~ (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "176776e4ada0d3b289abe50be203b6ba865fa63d", + "data": "C19d35e10d9b7dbc5818", "type": "single" }, "criteria": { @@ -1335,13 +1329,13 @@ "value": 5 }, "game": "iidx", - "goalID": "Gd32662938d122fcbfa18d4f8390d2772859df69192c6b924f8012093ad6c3fec", + "goalID": "G063ef72be4cc1cb237dfa267d9f400b66cf337136c952daf4334df7ae7e41bfc", "name": "HARD CLEAR 天空の夜明け (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "b0a587771ee70a2c9d78c359ceee869557554fad", + "data": "C19d35e10de437fdeb67", "type": "single" }, "criteria": { @@ -1350,13 +1344,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G42df127decb5fac1601a864f63044d3acd56cf95c4708c48dec4da8e83154447", + "goalID": "Ga56e3b9b26905c5a45da42bdd7080ba5341ad94d7a3a545b365fe17992cb2b65", "name": "HARD CLEAR HAERETICUS (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "22d9639b9b72406779bfa9912f43630e59a469de", + "data": "C19d35e10dbfc1d2f37f", "type": "single" }, "criteria": { @@ -1365,13 +1359,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G75d3d875541961b03f620caa0fdb03681680cdafe0298ff99c442499cd5fb378", + "goalID": "Gb8d3310edb53dd5f6a5d08da0f6e18e75ebca930698e39abe36a52662718f1b2", "name": "CLEAR BLACK.by X-Cross Fade (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "53264cfe35cb08e68cce8a563068876ba3b70b2a", + "data": "C19d35e10d4dd4f478d8", "type": "single" }, "criteria": { @@ -1380,13 +1374,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G79c4f6884c285c67a3ba9d721622fa917201769d4ddfd0dd9bfb38472d5e10da", + "goalID": "Gb22d2a97955f9932a348b1596b6476ed6a7bdbc1d080b59a73cfd9fba6ebb7de", "name": "HARD CLEAR Watch Out Pt.2 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "30171e98a562966fa6b44aa4929f8385742f9365", + "data": "C19d35e109f52b25193d", "type": "single" }, "criteria": { @@ -1395,13 +1389,13 @@ "value": 6 }, "game": "iidx", - "goalID": "G7f6b492105cb229cdc5abbd6306d9537f8b0aac16c9f74db60b53a6785f9b604", + "goalID": "G8f28381abceaa26a9c3a76d3cfb7a630450da1dddb8cd82171f9a7b5173b8649", "name": "EX HARD CLEAR era (nostalmix) (SP ANOTHER 9)", "playtype": "SP" }, { "charts": { - "data": "aa3999bf49c4e3f24f66b1e0d79f9120fc4cb61c", + "data": "C19d35e10d349b35e88b", "type": "single" }, "criteria": { @@ -1410,13 +1404,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gcd004c1887aef0e7704da2ce50153b06706a6ae86a68f747bbfbd8257bf201eb", + "goalID": "G254bc6d16c8ed54da6d7ad88903a46b8500d24049537901e070055347379986d", "name": "AAA mosaic (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "bd1d893e21dcdcd2d8aaebf90a383deb1e2c4fba", + "data": "C19d35e10e6b91722bfd", "type": "single" }, "criteria": { @@ -1425,13 +1419,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Ga0df6083017e36eab6bb9ebde9b26776bb85faffd728b1c54a3b205cabbac1b5", + "goalID": "G4b25b28d4afdfa58fee55c84a2963e1886886ae515ccab6274cd66a601b55c24", "name": "AAA Hypersonik (SP ANOTHER 10)", "playtype": "SP" }, { "charts": { - "data": "6b32423ab1efcff9bed3c50a77dc54180ee82fb9", + "data": "C19d35e10f2dd0aff237", "type": "single" }, "criteria": { @@ -1440,13 +1434,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G55dadbcb465cc58b4c949826ca0436e808c259e909a1108143a4710e5b8b352d", + "goalID": "G7c5055482429522f7482e7bdb97036d9a2c7e93f294ad4b39660d8e971884c29", "name": "AAA PENDUAL TALISMAN (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "a48168c01535bb480a7bcc57f1a1e27755df143a", + "data": "C19d35e10c8e0dd1a8b9", "type": "single" }, "criteria": { @@ -1455,13 +1449,13 @@ "value": 8 }, "game": "iidx", - "goalID": "G08fd73ecdd710f045816430e5cbbe1b70d2053e466cf3153b4564993b36d8ba9", + "goalID": "G60b4be3a6aca95dd43fa1bf62215deabba42f7a40ab3e3eef78916705d0a38ea", "name": "MAX- Dazzlin' Darlin (SP ANOTHER 10)", "playtype": "SP" }, { "charts": { - "data": "62da00fbfb75c282634f3a1f25d86ba7c5efd28c", + "data": "C19d35e10c567004da0c", "type": "single" }, "criteria": { @@ -1470,13 +1464,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gc567056c12238d066060d988a8743ec2a4e9c1d09ca44d6dadd08be8ab603f13", + "goalID": "G332b29e41e4069bf16d5b4718408c8a8fe446aff4492c26b7ab4389a7df4286d", "name": "AAA Make Me Your Own (SP ANOTHER 10)", "playtype": "SP" }, { "charts": { - "data": "e15e8f535c3d93a480e4f828dd244a40ad80bb14", + "data": "C19d35e10eb1a356a5e4", "type": "single" }, "criteria": { @@ -1485,13 +1479,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gf53be8d6288499987c8136e0d96676662d57c929a1820303f47a18f1d9977ef6", + "goalID": "G89dd2e38a992365ad2f80eb39fee7e686cb13c3a9a1343b266913fa63afa5a79", "name": "AAA Line 4 Ruin (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -1501,13 +1495,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G2455689e65bc890fe6b768786ac26bc1ecc010b7103888019a131b6aaa9a6828", + "goalID": "G86ef585d023d76a34105a87a29532201fa5cf0977e5b1bb7c4cdbbb2bc0d7d02", "name": "CLEAR 50% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F7aaf1fe3b7bc5a346106a596e67ae2c86462c671d11c7f1db1422d8e7a421735", + "data": "F19d35f0cf28bc354ac6", "type": "folder" }, "criteria": { @@ -1517,13 +1511,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G2273a629e2c47d4e4a05cd86bb2dd07020945dfd1bb0c2b6e62eb3e319d67280", + "goalID": "G45fecf25975b96b93ce9c81fac3fccac618516935bf278c025e5707312de92f2", "name": "AAA 5% of the charts in the Level 11 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F844651ec49a21e5c065b1138bbb98b1bfd9f3abd6a0dca444892993b15e8b41d", + "data": "F19d35f0cf283cdfb15a", "type": "folder" }, "criteria": { @@ -1533,13 +1527,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G653396c9ea159a062a7f0f7455cbf13a100fdaf48b6aef95e7ba98f1f4e01c3f", + "goalID": "Gb19e2023305b87213154c3a28e8c858141cf3fa113b6a15815b5d2b12009938d", "name": "AAA 10% of the charts in the Level 10 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "0e1fbd5bfe34351cb8c4869eddebc34241ec4546", + "data": "C19d35e10b9930d1f331", "type": "single" }, "criteria": { @@ -1548,13 +1542,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G5852c506437c45c321c465e787a7226ce6f018f73f7299919264c357ef70fe82", + "goalID": "G29a70b3f17eae4f3b8256d0d832470d278a48147e839d4238e8a082193a96502", "name": "HARD CLEAR AA (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "54cc0d53d5ebbff81fd39d6bffa00e33a9f6208e", + "data": "C19d35e10be5a4cce48e", "type": "single" }, "criteria": { @@ -1563,13 +1557,13 @@ "value": 5 }, "game": "iidx", - "goalID": "Gc501abbcdb1ffbfcadf48e69a49ed5c8822e5804a016223acd727f24aff037bc", + "goalID": "Gcf40d565e86bf83a3eef2a7cb82deccf0addefa7221e7d9d4ea81d0d26da3d8d", "name": "HARD CLEAR Scripted Connection⇒ A mix (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "c4e2ec5aa666d32507efa443e8a573b9508806b1", + "data": "C19d35e1100f41bfb80d", "type": "single" }, "criteria": { @@ -1578,17 +1572,17 @@ "value": 4 }, "game": "iidx", - "goalID": "Gaab422b2ec34f6a3140193a0bdf62e7228d28a62461b1894de6a224a22e7fed7", + "goalID": "Gbabb92af4e18584e9641f97cc7bd63ebadffa8ca1fe410f56bc8cee0b8f308bf", "name": "CLEAR Dr. Chemical & Killing Machine (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { "data": [ - "e064e485f02479fe47bb16ff5a347ef21ae1ff3a", - "53264cfe35cb08e68cce8a563068876ba3b70b2a", - "e7b29f13bb17d14b16564a3427da8877e602e0dd", - "35386cc0ac41449b7cf86c96668a3be680c9efda" + "C19d35e10d28511d0a70", + "C19d35e10d4dd4f478d8", + "C19d35e10f6815e2d9c3", + "C19d35e10d9a810c7092" ], "type": "multi" }, @@ -1598,13 +1592,13 @@ "value": 4 }, "game": "iidx", - "goalID": "Gd4d221f92419a6d4db84679512e9484023dc0c77dc911fb0af5332d4049895ff", + "goalID": "Gcd568e1f0bc9ed6c4c1e2736f7cfda2eef649931562bfbd79b4e40c07c8ecd4b", "name": "CLEAR any one of Red. by Full Metal Jacket (SP ANOTHER 12), Watch Out Pt.2 (SP ANOTHER 12), 199024club -Re:BounceKiller- (SP ANOTHER 12) or Snake Stick (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "e51624299cdcd9d6cecee915dba09a6cf72b816f", + "data": "C19d35e10b4c8388c544", "type": "single" }, "criteria": { @@ -1613,13 +1607,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G7e4e5698240f19602fc4775b101507f1008d6726e7d53b77699d3d41201f90d0", + "goalID": "Gfaf0ebec02f07c36c9f09efe37acd925cefcebbf092ccc3b40a23bf3b072835f", "name": "HARD CLEAR 雪月花 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "906c67a8d37dea58b9163a020a505dc1ae877d88", + "data": "C19d35e10d8f3ccafa63", "type": "single" }, "criteria": { @@ -1628,13 +1622,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G2267969a8bf2299a24209062578dae910a343d8aa070b815aecd9cb0fb1da8e3", + "goalID": "Gea588214e18bc3879ee62164eb06c7356230f35be3ae75a71d83d01645532628", "name": "AAA Sakura Reflection (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "3fe9c758a2a6885c5670f3c27fde3ed16d0475fc", + "data": "C19d35e10b56c98d83ec", "type": "single" }, "criteria": { @@ -1643,13 +1637,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G80d42b930287a1ef469f86d18d062661426b46e1f09c5591fd5380b93ae4f7b0", + "goalID": "G9973d2b6a16c70d30b0d302ace1ac6d523a4d6d7d1847134dc3c67d3fde218e7", "name": "AAA Daisuke (SP ANOTHER 10)", "playtype": "SP" }, { "charts": { - "data": "bb9298962b63b31f7dd95f774a6459dd24256915", + "data": "C19d35e10f626004bcc2", "type": "single" }, "criteria": { @@ -1658,13 +1652,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G161dbccdf18052986d896dfc5323f038c1abb36f2e764ed46d92c2e90b3106a6", + "goalID": "Gfbbd23ea00d0c1e07e787e7f9e8a969bb1d3e50016037e7f831abcc8a76051d0", "name": "AAA NO LIMIT -オレ達に限界は無い- (SP ANOTHER 10)", "playtype": "SP" }, { "charts": { - "data": "77da55f73956c4ddb3029260648062d57edea6f1", + "data": "C19d35e110c03b300e12", "type": "single" }, "criteria": { @@ -1673,13 +1667,13 @@ "value": 8 }, "game": "iidx", - "goalID": "G096952757cb1363652f1c8d7c6a4d31f8ca0ae627f7e41568614a0bb8d283fe7", + "goalID": "G7a1c16f3d0dfac597511a5ef7f102b8f05f1859aa5bb347ee980e6ffe9124153", "name": "MAX- Sleepless days (SP ANOTHER 9)", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -1689,13 +1683,13 @@ "value": 4 }, "game": "iidx", - "goalID": "Ge99fd25f71e64d91771546fa9dda6b5b59f2746d2259398f34d1f3c63c9ec721", + "goalID": "Gb372c466966afbf3ebf70c5d3b701dab56fd376965797a6864ed163b20d2868a", "name": "CLEAR 30% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F7aaf1fe3b7bc5a346106a596e67ae2c86462c671d11c7f1db1422d8e7a421735", + "data": "F19d35f0cf28bc354ac6", "type": "folder" }, "criteria": { @@ -1705,13 +1699,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G1a51c71951ad0fc9f1e7eaaa32ff6a6c25f3659faa0f0b63e1440590352fa850", + "goalID": "Gcaf664e666e9d567941bddc60734e270cc273456074b66fa81921f0718419584", "name": "AAA 1% of the charts in the Level 11 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F844651ec49a21e5c065b1138bbb98b1bfd9f3abd6a0dca444892993b15e8b41d", + "data": "F19d35f0cf283cdfb15a", "type": "folder" }, "criteria": { @@ -1721,13 +1715,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G65f1fb8f98fb1f8da4c19cdecf4b33cae36769e9354c825231e1738e3c27eb60", + "goalID": "G24421c36c33c601a8ec08fbc4de2713ff7fdbdf14e088fe0a6c4cd640e544d79", "name": "AAA 5% of the charts in the Level 10 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "0153200938271d4d9c3c90e2d7b826070ad32087", + "data": "C19d35e10db35e3bea51", "type": "single" }, "criteria": { @@ -1736,13 +1730,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G5b4244720155901e8b3aa8c2143c561d88f8fce9fa80cf619ae5111aacc20694", + "goalID": "G2788eba138e2919d69fc1c78da2af3d03d0dd5923bc816adcd13d977b9a07b51", "name": "CLEAR The Sampling Paradise (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "9dd405f9c80df79c698adc67812716a555895c3a", + "data": "C19d35e10d44a0368416", "type": "single" }, "criteria": { @@ -1751,13 +1745,13 @@ "value": 4 }, "game": "iidx", - "goalID": "Geb56e070e7fe0fbf39443b4e86c96544b6e9ffb1a055270bcfe4152b5a8b0e89", + "goalID": "G16b48da8eaa90d33762ebdd9157f318a6796c18233c2b7342fd60342c19f5a84", "name": "CLEAR SOLID STATE SQUAD (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "f98b2075a9ecb1b6b1962718cfd508dc99b180c4", + "data": "C19d35e10f95f5d8fdfd", "type": "single" }, "criteria": { @@ -1766,13 +1760,13 @@ "value": 4 }, "game": "iidx", - "goalID": "Ge89c922fa298f38f8a478219be94871f084206c3f6e49b9a8244c27feae5c5f3", + "goalID": "G89e418fe4063bd4ec408ab6f14fc2adeb8c6b083b78c035e86baa7fb08c49be5", "name": "CLEAR Rave Cannon (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "84fb6b1231bc87038c19b43c381f6ed9a116972a", + "data": "C19d35e10f38234d75b3", "type": "single" }, "criteria": { @@ -1781,13 +1775,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G8f8c8fb87a12f4074370ca73e1f168d5d9d2c4e5ca3a2b4d7fc4a6c5e80fb82a", + "goalID": "Gf6b5d259f395e82c4a0253318916d1ba879518018e59db87bd074794a1b4f744", "name": "CLEAR Highcharge Divolt (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "7397b54511895b6b465b3846135cfe53f7e58701", + "data": "C19d35e1105f10fa13ea", "type": "single" }, "criteria": { @@ -1796,13 +1790,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G1cc8b945a0c578e781694a198d70a4967367868e959b0fcee177700cf2942ab7", + "goalID": "G7036ef4e689adc7193a8e8eb21e35fee512ee1bf77f936a902e703da6039602e", "name": "HARD CLEAR Venom (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "a15e518e1a033e7c0f3e69ff6bfd66475cc01a8a", + "data": "C19d35e10d91c250a8b6", "type": "single" }, "criteria": { @@ -1811,13 +1805,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G9a070f7500b05c13f92c14d8cb646012633394110e10de7cb263dbd721b66b18", + "goalID": "G167348b8b1dd68884dd7ec4c7889fff8f71ff8d96aece8aac2de550acadab729", "name": "CLEAR Broken (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "c2311194e3897ddb5745b1760d2c0141f933e683", + "data": "C19d35e109af227afb4c", "type": "single" }, "criteria": { @@ -1826,13 +1820,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gd8f31713af340aa31482fbce46a07af342004e310b744c6e5b516d7385a57502", + "goalID": "G809602ca08ef16ac69cbb376195cf646e9a259905283e123c9dff879c6c13594", "name": "AAA 5.1.1. (SP ANOTHER 10)", "playtype": "SP" }, { "charts": { - "data": "0b2197dd1979d54e7afd890b7f768d4b3d677eac", + "data": "C19d35e10ebcfc2e29ea", "type": "single" }, "criteria": { @@ -1841,13 +1835,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Gf431ae7eb6403be496d0d3bcee22bbcc503bf48e721e8e5a6eb8dcc80efbd08c", + "goalID": "Gf8ae63ad9fb57853c7431ea5a8ba962da3e2bd0d29df547fa9239642acd1aab8", "name": "AAA chrono diver -fragment- (SP LEGGENDARIA 10)", "playtype": "SP" }, { "charts": { - "data": "8d1335bc1a9d0e57c3a73eee3be996e9ca619067", + "data": "C19d35e10f97155336a0", "type": "single" }, "criteria": { @@ -1856,13 +1850,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G378188b4d0c49bbfa1284d1155ae558dca50abb5a245be1c37c5209ede57085f", + "goalID": "G03e0b772b28192e1503552d94e7ca9785d79e74f6682d922b3d8e550723c095c", "name": "AAA ¡Viva! (SP ANOTHER 10)", "playtype": "SP" }, { "charts": { - "data": "a87edb68c1710d6aec654c4dfacc761daf618b5c", + "data": "C19d35e10ce2c3937b31", "type": "single" }, "criteria": { @@ -1871,13 +1865,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G1c238bc8ca9b8f89e1eab6789e5a60f5624573a9b68e121044d50344cb61fe42", + "goalID": "G712841f80690c78026f34dcf8ed885545168bcb065afa9987038c577c703fe0f", "name": "AAA BRIDAL FESTIVAL !!! (SP ANOTHER 9)", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -1887,13 +1881,13 @@ "value": 4 }, "game": "iidx", - "goalID": "Gbc23d96a89094c2c3bb6366096f6fcdca45b30ecb55192e2229e4fddd0310faa", + "goalID": "G46b5ce2a764563eb431d3270836e49fd911c64cd502495d10bf2da1bba4dc01c", "name": "CLEAR 15% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -1903,13 +1897,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G33a9358057928f0ffb8b5691315eaedc74b903fa717158d0fe077eb3eca3e517", + "goalID": "G04446d20628c449092f4e5a74c5416315fde66b6fa9e9977f6d074619451f6bd", "name": "HARD CLEAR 5% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F844651ec49a21e5c065b1138bbb98b1bfd9f3abd6a0dca444892993b15e8b41d", + "data": "F19d35f0cf283cdfb15a", "type": "folder" }, "criteria": { @@ -1919,13 +1913,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G6a1d19018159d80457f7045b7908ba2a32ab4889bac33153be140967f7cedcc7", + "goalID": "G911d105e08b4c74f2ddb698efbb952b51753cb7dba4e35ad512e6104fd1064b6", "name": "AAA 1% of the charts in the Level 10 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "7b1dea115384791aebc0217eb54e3a50a0f16c50", + "data": "C19d35e10b0ea35c665e", "type": "single" }, "criteria": { @@ -1934,13 +1928,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G2db28d9a6929d00d781cb8134d17f3c7b683ecd6bc389984240fce4dc0475403", + "goalID": "Gab1c4a139570cfbee48db6c96f66cd39486d99911771340f11e05533aa4b3287", "name": "HARD CLEAR quasar (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "950879ed0084cb67446716c944de61742a3b1bc9", + "data": "C19d35e10c92cd3d3ff7", "type": "single" }, "criteria": { @@ -1949,13 +1943,13 @@ "value": 5 }, "game": "iidx", - "goalID": "Gf3b0dfb58f116c1c7134db24401590403234c478a10c076758543f29b2c235bb", + "goalID": "G3b2d3447cae04f856cb6c51a4d924aeabefc2034c1dd8a823bd134c04cc3b9db", "name": "HARD CLEAR Digitank System (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "bfc53502d89b12fef16f929d11d7430cac055ade", + "data": "C19d35e10d940cf12555", "type": "single" }, "criteria": { @@ -1964,13 +1958,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G1e2eda29c30949f974cb30a042fe5dc78551d53a76ded24fc46cc4b73845259c", + "goalID": "G51eae5902c3a14284b515cf85001a6e94cc195f8b408865bd808074a11b0f947", "name": "CLEAR SPECIAL SUMMER CAMPAIGN! (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "7244887d53a4e383f42b05dda3ac24a2d6556821", + "data": "C19d35e10d8a6b5ed1a1", "type": "single" }, "criteria": { @@ -1979,13 +1973,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G7f67f44a0fd3eea6c22c83398ebcafd71136248f0911fb8f790a6d4c837b01aa", + "goalID": "Gc275e614ee2303f13e217d7883a35ff4d3507a96f58db9e701023596c91d9a69", "name": "CLEAR sakura storm (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": "cea0eadee4ae414d91360c88f2c69c1b0e6f6681", + "data": "C19d35e10f007d7641ea", "type": "single" }, "criteria": { @@ -1994,17 +1988,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G0ae78134b5a4c3884353d6d5900dcfd12f8d53695a7ce9691f9327edc86cc289", + "goalID": "G84b57d80510556b7aa49b3c01d76d4e4c29521d70c34fa97647a37676ea9362a", "name": "CLEAR 千年ノ理 (SP ANOTHER 12)", "playtype": "SP" }, { "charts": { - "data": [ - "a345321491ad99adc6fe960bb8b8f27a1915b291", - "b10719db4cd4082b2ff37e88e76cd2695177ff55", - "88107d20d1df5a6d45fbed8c21079e2a1cc573b1" - ], + "data": ["C19d35e10e37c954e085", "C19d35e10fef1ca4f7d5", "C19d35e10dde016f1746"], "type": "multi" }, "criteria": { @@ -2013,13 +2003,13 @@ "value": 4 }, "game": "iidx", - "goalID": "Ge0417bddb65b1021a308a19495d824130c6d0b1ca23e606249f316bb21f2a43c", + "goalID": "Gf5cc6c22777f61ca37c672458970ffae60a6ffb238343fb0b22a838bba83b9c2", "name": "CLEAR any one of examination leave (SP ANOTHER 11), Macuilxochitl (SP ANOTHER 11) or 532nm (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "1dce504149aed886a1e3de31a27fe7df277dae9b", + "data": "C19d35e10db9c840b431", "type": "single" }, "criteria": { @@ -2028,13 +2018,13 @@ "value": 5 }, "game": "iidx", - "goalID": "G47c8f8f25c445a4558206e8dfbd429068349dff7abdd0ed482c7d3900d8e6f59", + "goalID": "G15ae6ddaa4b63de46dd2371010df7864752996f8e904906faae0feb72c511d1a", "name": "HARD CLEAR SHADE (SP ANOTHER 11)", "playtype": "SP" }, { "charts": { - "data": "0738cadf2af4cc9a012eaca8348b94e4f4e4db23", + "data": "C19d35e10e4539ba6e7e", "type": "single" }, "criteria": { @@ -2043,13 +2033,13 @@ "value": 7 }, "game": "iidx", - "goalID": "Ga1bc389b21e3012240f33f493de6db7efd1389a372b67f6fbe9856c400d29f0f", + "goalID": "G0d44be3bae50f7554f0e696e39b726926f6bb750fd49426c848c84ed9bcbbaad", "name": "AAA Element of SPADA (SP ANOTHER 9)", "playtype": "SP" }, { "charts": { - "data": "293679f8eb416ddaefff758e4b9b72cb580afd47", + "data": "C19d35e10c9cf8814c74", "type": "single" }, "criteria": { @@ -2058,13 +2048,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G00d676101f4850abd718e1fe908539d4f91ad4ea58f73af7b9cbc52544e74684", + "goalID": "G2a931695fbcb409c4e88590d1af4887a4b41c5787c625a45b2ca7b3a672fd034", "name": "AAA the trigger of innocence (SP ANOTHER 9)", "playtype": "SP" }, { "charts": { - "data": "1b8e0a918c49fe3a5b3422274f25dc125e0e1b39", + "data": "C19d35e10fd794519b32", "type": "single" }, "criteria": { @@ -2073,13 +2063,13 @@ "value": 7 }, "game": "iidx", - "goalID": "G22531dc83f001d7bc2f73a28b7a3d71f2835b174a2eba151393319b710b375ec", + "goalID": "G00122bef622fadcfd27c3ae3472bc8f5a929a64ad5dd62f7dd7e6c67e595766f", "name": "AAA Sweet Clapper (SP ANOTHER 10)", "playtype": "SP" }, { "charts": { - "data": "F4641845dcaacfc772a35291197128d6958a0b2551d0831ea1ab6bf2901bad44a", + "data": "F19d35f0cf28a4492ffe", "type": "folder" }, "criteria": { @@ -2089,13 +2079,13 @@ "value": 4 }, "game": "iidx", - "goalID": "G873e92ff8bb8352eef55b97e11329e216bd2a037145e0385147b094bd592da82", + "goalID": "G39e38988b5fff299596322caf2d7e522d5af8c373560b3bd592fa58992a67a58", "name": "CLEAR 10% of the charts in the Level 12 (CastHour) folder", "playtype": "SP" }, { "charts": { - "data": "F5d3b507b2e00c793b7f7d1196195d33890556abd564beef68d08fbd4be56ae8c", + "data": "F19d35f0cf29a9db88d4", "type": "folder" }, "criteria": { @@ -2105,17 +2095,17 @@ "value": 7 }, "game": "iidx", - "goalID": "G5bc76bd2a019f34bef39f4a4cf731c1d726a82abb47b75834f5e7ce200af93a2", + "goalID": "Ge1e31f0d0cbb42a4ade9ef4955f2e24527fdd26f43eac8d5be6f8df7b897d8f0", "name": "AAA 1% of the charts in the Level 9 (CastHour) folder", "playtype": "SP" }, { "charts": { "data": [ - "b1203df0a538d7e9e0bd14c37a98acfe91ff70dc", - "2d0727e9a2bea503a4b235f5658d8f18ada6f413", - "3ec880dc108a0dad9f23fc4ddb5817478d0924c1", - "3678b67f108e769241767d2e07053ba1b25fc3cf" + "C19d35e11e15bb108e3d", + "C19d35e11efe2ce4b6e9", + "C19d35e11d9d3c8350b1", + "C19d35e11e185a6d1faa" ], "type": "multi" }, @@ -2125,13 +2115,13 @@ "value": 9 }, "game": "sdvx", - "goalID": "Gac4eda4ba8dfc2158542ae76bd49fbbbdea750f4332338cd330c1f4c8bc1a84b", + "goalID": "G1d5ca7dbac64333501d2addcdc5204b8f67ff97d8816b31148de032b3f88143b", "name": "S any one of Paradission (ADV 12), Decoy (ADV 12), NEO GRAVITY (ADV 12) or Celestial stinger (ADV 12)", "playtype": "Single" }, { "charts": { - "data": "F366713bbe56a09b513e571c86c97dcb71455c770f700611099e5d69974b04a01", + "data": "F19d35f0cf2acd6a27e5", "type": "folder" }, "criteria": { @@ -2140,13 +2130,13 @@ "value": 9 }, "game": "sdvx", - "goalID": "G25943e5bfe8824d0f67c282728f9cd3650666519e71aacf8a4273cfd5d08b8de", + "goalID": "Ga5f928c598496bb359148b3a369b2c39803a9e65829150f0719b5de89856350e", "name": "S any chart in the Level 13 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F21e35fbe0718c2db51ab633ae43d53945012fb86f740ef81a2d4ba75b425d384", + "data": "F19d35f0cf2a597982ce", "type": "folder" }, "criteria": { @@ -2156,17 +2146,13 @@ "value": 8 }, "game": "sdvx", - "goalID": "G3c37a44164e6ae1115dd1ccbd67a54d4131a15b228e2afe0d9f271894d8629ef", + "goalID": "Gf9361811b028db747f42efac21279b2309cc60b6c4928c2995f7d5dd44d003b1", "name": "AAA+ 3 charts in the Level 14 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "e6cdd57b0fe40ce2e5c3355f632c40a39793a0db", - "1dd2ac700d91c78e6faaa09d9b9726bb1c239348", - "19e8a6b9e8746c625f7e8109e17480edea36dacd" - ], + "data": ["C19d35e120e2bd10f710", "C19d35e1207bcf55ca00", "C19d35e120dd312d5101"], "type": "multi" }, "criteria": { @@ -2175,13 +2161,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "Geb61eb5bc6b35cad82582243c0cf6879dee5a1743c78645ca625b6de31ca4f6a", + "goalID": "G37a14b574a6fa69869b81853cd18acc23ca6cc8e5a0e66ba52bc50d2adb37bb4", "name": "AAA any one of The First Step (EXH 14), EGOISM -Rebuild- (EXH 14) or Still Lonesome (EXH 14)", "playtype": "Single" }, { "charts": { - "data": "Fb6d41e93c52dd1cf72b05a8fbfa979ecb88fe9c6675180e2d8cbd2d5a6e9476c", + "data": "F19d35f0cf2ab3530fb4", "type": "folder" }, "criteria": { @@ -2191,13 +2177,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "G28366badbe5c87fa61a5025a58cdf8f52f14b3330b19083567de401115f9c72c", + "goalID": "G7fe33015d02f9b19626121a96480e12cbe06905f3a33252b64ec9927857050e3", "name": "AAA 5 charts in the Level 15 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Fb6d41e93c52dd1cf72b05a8fbfa979ecb88fe9c6675180e2d8cbd2d5a6e9476c", + "data": "F19d35f0cf2ab3530fb4", "type": "folder" }, "criteria": { @@ -2207,13 +2193,13 @@ "value": 6 }, "game": "sdvx", - "goalID": "G8f9f1a73b569a99206b8a99461d713dec97720bd049df1f8e433632993633001", + "goalID": "G26c265e7188e5a1536a818eb38fda52eaee3bfd7e71b3e1f029e0262ea6a782c", "name": "AA+ 20 charts in the Level 15 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Fb6d41e93c52dd1cf72b05a8fbfa979ecb88fe9c6675180e2d8cbd2d5a6e9476c", + "data": "F19d35f0cf2ab3530fb4", "type": "folder" }, "criteria": { @@ -2223,13 +2209,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "Gd837975332349aff6f091232ebe8e4275096bfd5e8866eb96f8f7988163d5ff6", + "goalID": "G94c2a3d6e936cf946d7b870caaed917eb9a00d644f68ac514387a13a1356b2c1", "name": "EXCESSIVE CLEAR 10 charts in the Level 15 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Fb6d41e93c52dd1cf72b05a8fbfa979ecb88fe9c6675180e2d8cbd2d5a6e9476c", + "data": "F19d35f0cf2ab3530fb4", "type": "folder" }, "criteria": { @@ -2239,13 +2225,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "G1fa2d8289b79aaf5a3067df209f175eedc834d18b34ed43d3e62978fa6302e32", + "goalID": "G909ef14ed7b214a25332693fe623ae65e2e0468f91def016d7a77f8d5779af46", "name": "CLEAR 30 charts in the Level 15 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Fbfa65f45a9f5436a5dbc55a7f90d116b50ac748d5e881b56451ed4b40531a53a", + "data": "F19d35f0cf2a193b41f8", "type": "folder" }, "criteria": { @@ -2255,17 +2241,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "G1f62ddb6de93461446da5898c67392c8030f0c6bad3a11ba0433565230f9fac6", + "goalID": "G604f3063c34494062b11e181543cf881d408b67311695f750927b3f907e75cf5", "name": "CLEAR 10 charts in the Level 16 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "5db205288187fac8a2b41e9c51b6f6294a66b4d1", - "a7cbe0a9e50ec9c026a7b3e77f6db943993a281e", - "c72d53152552b453cd6566399f75dfee8b5c4456" - ], + "data": ["C19d35e11b34740b1895", "C19d35e11dd0d9664428", "C19d35e11ccba56d68b0"], "type": "multi" }, "criteria": { @@ -2274,13 +2256,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "Gb4c1a4e8e178dbf99d0ed3f5c1a59fc83d88f93a6ab0b4567dc4e70db837b2c6", + "goalID": "Gd95a09ddc1ce7d3d139c7c622883d58c5541127c94d3fea1c3828f912ca412f0", "name": "CLEAR any one of 孤独の番人 (EXH 16), 爆なな☆てすとロイヤー (EXH 16) or ポメグラネイト (EXH 16)", "playtype": "Single" }, { "charts": { - "data": "F366713bbe56a09b513e571c86c97dcb71455c770f700611099e5d69974b04a01", + "data": "F19d35f0cf2acd6a27e5", "type": "folder" }, "criteria": { @@ -2290,13 +2272,13 @@ "value": 3 }, "game": "sdvx", - "goalID": "G87456157681bafef37d52d99370088e547d5e9ad55fd45369e12dd2bd3895a3c", + "goalID": "G6f51d83a87ea6468d36357d2fbebad8248a1db01503d409f7bced22a17d21199", "name": "ULTIMATE CHAIN 3 charts in the Level 13 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F21e35fbe0718c2db51ab633ae43d53945012fb86f740ef81a2d4ba75b425d384", + "data": "F19d35f0cf2a597982ce", "type": "folder" }, "criteria": { @@ -2306,13 +2288,13 @@ "value": 9 }, "game": "sdvx", - "goalID": "G488d565d68431ce46422f085f04c04e80366b57a64a025e8914ff8d0297ee382", + "goalID": "G16448646d8f0ee92742e50607460ed9ab36ce77078f83336592c060360326d59", "name": "S 3 charts in the Level 14 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Fb6d41e93c52dd1cf72b05a8fbfa979ecb88fe9c6675180e2d8cbd2d5a6e9476c", + "data": "F19d35f0cf2ab3530fb4", "type": "folder" }, "criteria": { @@ -2322,17 +2304,13 @@ "value": 8 }, "game": "sdvx", - "goalID": "G26df4f768894142b4161e7b4b362a6db09bccae563dc106e0de662c10924c88f", + "goalID": "G39a50e57d5a90612ce18d55ad410299f854a5d88c439582fd51facdbf9d338f1", "name": "AAA+ 4 charts in the Level 15 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "e8f68fff897924a8e24cb2d9b1f808a40b7550fe", - "1dbf9967abd36259de44a22dbbf2436008ff0813", - "87315643cf50d4d4fa6d9db91737b0cb4a7b142d" - ], + "data": ["C19d35e11f958b637f1e", "C19d35e11aceb41230d4", "C19d35e11eddc2799df2"], "type": "multi" }, "criteria": { @@ -2341,13 +2319,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "G66acf9104f38a848ab38caeb409c7ccecccea02a6b7c1453d5d4ef4caaa95b35", + "goalID": "Gcfebd2e376a3480b9e6c033f117469b765582902c36339422ad9a9898402ee1f", "name": "AAA any one of ARROW RAIN (EXH 16), GAMBOL (dfk SLC rmx) (INF 16) or Juggler's Maddness (EXH 16)", "playtype": "Single" }, { "charts": { - "data": "Fbfa65f45a9f5436a5dbc55a7f90d116b50ac748d5e881b56451ed4b40531a53a", + "data": "F19d35f0cf2a193b41f8", "type": "folder" }, "criteria": { @@ -2357,16 +2335,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "Gd20bb734f7924f085951fae9e2c019d9e63aa010e617ae92f81b9249e56bc172", + "goalID": "Gcf2e16e52eeb5b2c5af03c96e37381eb1a2ca7ed1e5941fafbc297a5553381b0", "name": "AAA 5 charts in the Level 16 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "55c7e6b036bd64460374db77c35c2fb21dca1bd6", - "55864ff29e0cb630e156fe27a743990db3ae50ad" - ], + "data": ["C19d35e12116d1a9fcfc", "C19d35e11bef7119bb6c"], "type": "multi" }, "criteria": { @@ -2375,13 +2350,13 @@ "value": 6 }, "game": "sdvx", - "goalID": "Gfe5cb6e3f57cf27279b3f79c99791fd5c30acb5bea982f34a99ae9c9dd7f5105", + "goalID": "G60ea64a9d5c8db898668210740f06c79924b37229dca4bda9c6908b2f958bfc6", "name": "AA+ either VALKYRIE ASSAULT (EXH 16) or choux à la crème (EXH 16)", "playtype": "Single" }, { "charts": { - "data": "Fbfa65f45a9f5436a5dbc55a7f90d116b50ac748d5e881b56451ed4b40531a53a", + "data": "F19d35f0cf2a193b41f8", "type": "folder" }, "criteria": { @@ -2391,13 +2366,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "Ge0dbddf1ca9773ebaf2ee8d1bfe42b372eb6b3094c2de1b0275766d326daa706", + "goalID": "Gfe519c3456dff488fd95160e05e82566c09f994ef97daea3342ac39b7e5d73ff", "name": "CLEAR 40 charts in the Level 16 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Fbfa65f45a9f5436a5dbc55a7f90d116b50ac748d5e881b56451ed4b40531a53a", + "data": "F19d35f0cf2a193b41f8", "type": "folder" }, "criteria": { @@ -2407,13 +2382,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "G4670b732730d59913fab058592f9c12e1d1c1885f5342faae6e64aba0c6e855f", + "goalID": "G60e9c01536ddd674ae7da090f6fed232d8de0358a0d253949d780133d12e5bf8", "name": "EXCESSIVE CLEAR 15 charts in the Level 16 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2423,18 +2398,18 @@ "value": 1 }, "game": "sdvx", - "goalID": "Gd5be9b5c9a426041a9ca79acd1fa0f371b0e524898e6ffe5f0321b5f55fbc67e", + "goalID": "Ga6d0023afac9a643d7bfa255e678433ff5d320d4b3de803df57e1385e97a781b", "name": "CLEAR 5 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { "data": [ - "33a9f54ec49c172f6f4fd0168bd37c8710ca5beb", - "f226a410dbba28fd587c131e45728640a9bfb4f4", - "88d24c5a0d2a813517672d9c7e92465147f51f1f", - "4274fa3f33790fab5d3e21634f608d0484b9782a", - "c4d76339c2e651d79be5689576768e42d8747690" + "C19d35e120117c0027cb", + "C19d35e120ea3dfad3cc", + "C19d35e12051437d3f4e", + "C19d35e11c8638b06a83", + "C19d35e11e6b5f42b4d7" ], "type": "multi" }, @@ -2444,13 +2419,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "Gd6850c9703150082ab42078509f94a098ceafb612b6ce4ca0db8e94b357ff278", + "goalID": "G93ee06f15bec2deed7fb4c59221da49da778147972ebc070d3602f8beb78c13c", "name": "CLEAR any one of D1g1t1ze b0dy (MXM 17), 逆さま♥シンデレラパレード (MXM 17), AXION (MXM 17), The Sampling Paradise (P*Light Remix) (EXH 17) or 黎明スケッチブック (MXM 17)", "playtype": "Single" }, { "charts": { - "data": "F21e35fbe0718c2db51ab633ae43d53945012fb86f740ef81a2d4ba75b425d384", + "data": "F19d35f0cf2a597982ce", "type": "folder" }, "criteria": { @@ -2460,13 +2435,13 @@ "value": 3 }, "game": "sdvx", - "goalID": "G4c2683550000efb9ceea5b1b6f4d67444f2487db1d3d38bcea144f98ebb22f2b", + "goalID": "G2753016668b125fa4c054f8c5044e59ebee545a1cb4aa50f9a95af5e2afd600d", "name": "ULTIMATE CHAIN 4 charts in the Level 14 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Fb6d41e93c52dd1cf72b05a8fbfa979ecb88fe9c6675180e2d8cbd2d5a6e9476c", + "data": "F19d35f0cf2ab3530fb4", "type": "folder" }, "criteria": { @@ -2476,13 +2451,13 @@ "value": 9 }, "game": "sdvx", - "goalID": "G9113ca9a758f555f24c8ba3037da9a052562b3d686e6303027206572389811e1", + "goalID": "Ga70c91a1d0c0240cfb3c0b3628b4b00e4250208335e09d2efd04cd137325d789", "name": "S 4 charts in the Level 15 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Fbfa65f45a9f5436a5dbc55a7f90d116b50ac748d5e881b56451ed4b40531a53a", + "data": "F19d35f0cf2a193b41f8", "type": "folder" }, "criteria": { @@ -2492,17 +2467,17 @@ "value": 8 }, "game": "sdvx", - "goalID": "G28c623459d09d203b4220d0419835925a486ff24b553c70910b34ce87d1e34c6", + "goalID": "G19bb3009f3b3af570179c740657bcd8ce468e0ae1a0cfddf138c31331657bdbc", "name": "AAA+ 4 charts in the Level 16 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { "data": [ - "69e71c20bb03725e2f988712cb7fd184d92bb50c", - "6fbac4d38c0dcf19ffabeeb7ef7c1d9bb1e34019", - "0e0c5d7bd5deff9419e6ec8c3bc65e684a895065", - "022b18c73e4dd394c1b58e3280193f752876115e" + "C19d35e11ff6254c9de0", + "C19d35e11fcb3586053f", + "C19d35e11b3bcfad9fe5", + "C19d35e11c76723ab1c7" ], "type": "multi" }, @@ -2512,13 +2487,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "Gd65e62814382a3e165c32cb2be3c66e4d6a9428fa9a4970e933ac0a580afc585", + "goalID": "G9f3ef95935d9c7f41602019787938467d1fce5294f7c91850fe3d5840ee13fce", "name": "AAA any one of Glorious Journey (EXH 16), Jack-the-Ripper◆ (EXH 16), 最終鬼畜妹フランドール・S (EXH 16) or KHAMEN BREAK (EXH 16)", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2528,17 +2503,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "Ge8ea38a06a88f3bf81f8f59bf39290b53f8d8a62e315dc957eaec03b7b8df3a4", + "goalID": "Gf5463e029bcc05da520e5cd0a7964c2cbdc36618c46506212fd5488c22679391", "name": "AAA 5 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "5af67282a0b8aced73ca895caff14cc6b2ad7f16", - "2f3c1811d226b774d717b69eac77aea0b2369a70", - "215966819017f8105db82f6cb28d0648dd7e4246" - ], + "data": ["C19d35e11f23dafc55df", "C19d35e11e354caa941e", "C19d35e11e0ea3dc1118"], "type": "multi" }, "criteria": { @@ -2547,13 +2518,13 @@ "value": 6 }, "game": "sdvx", - "goalID": "G8145fdbea66519513f3aa8f47c191d8029d522f08e4c1b14a1694686373fb781", + "goalID": "G9c1ef5e70be5684ec6d0267e0fc0c526ed9d095338d7d9832ba4fa225daf12d1", "name": "AA+ any one of GERBERA-For Finalists- (EXH 17), JOMANDA (EXH 17) or Destroy (GRV 17)", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2563,13 +2534,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "G68fecfe183d3d874fd24f1c1f516854d79d643df0e6fbefb64b50d1ddffb305b", + "goalID": "G7a12b47023c873d34f89671f2dd3d967af1fbc190ca6565d3a9d4e47a764372f", "name": "CLEAR 50 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2579,13 +2550,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "G7998550b0215cb2dd7a7fd39de62577972c95b69e127a044362ae2d09157590e", + "goalID": "G4f2f1e9c58fe75fc1d4adb980abbd4ccef0c8e2a458eebd2dfe5c61e402a1ba4", "name": "EXCESSIVE CLEAR 20 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -2594,18 +2565,18 @@ "value": 1 }, "game": "sdvx", - "goalID": "G39ecef606cbc725421f980588673387079ee68fcbad3f0284a65abef8cd1f867", + "goalID": "G7107e4419481cd400f06fe820457cfcc57868665d07f987ef19b129432a7ebf2", "name": "CLEAR any chart in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { "data": [ - "048577a83a2c39b9d4a5982614031fa46cbf1cbf", - "d2242273430d85a453e399c18f528de50d304c80", - "ecbd5e8c5216154865d2312a0db2c9b83a0d3012", - "9db5ed1dad4bbf44a2af4ad2522c071f6acd7369", - "cb6d52909b50ae3737fc0cb3d6b2fe434b626f08" + "C19d35e11b6c51b0996a", + "C19d35e11d1b90a142cd", + "C19d35e120923275152e", + "C19d35e1213e377197a0", + "C19d35e120e6c92ce324" ], "type": "multi" }, @@ -2615,13 +2586,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "Gd0acd0dfd8f9f91ae9b6aa7734fb726c0a8e83e67d7771d8c72d64311708e750", + "goalID": "G6888d8c1566ce138427dfdd51ff1e7ebe5a7272b1ebf5ef88c119e0565f2d69c", "name": "CLEAR any one of Booths of Fighters (EXH 17), Nofram (EXH 17), ULTRA B+K (MXM 17), Innocent Azure (MXM 18) or 狂水一華 (MXM 18)", "playtype": "Single" }, { "charts": { - "data": "Fb6d41e93c52dd1cf72b05a8fbfa979ecb88fe9c6675180e2d8cbd2d5a6e9476c", + "data": "F19d35f0cf2ab3530fb4", "type": "folder" }, "criteria": { @@ -2631,13 +2602,13 @@ "value": 3 }, "game": "sdvx", - "goalID": "Gc3663f8188539baf2fc8dc21b5be903a9086ca25367b8311db9f7bf0f834f761", + "goalID": "G943e04101cb28a01426725ec45387a3a9de9bdbb999acaa0478f2e482c4da76b", "name": "ULTIMATE CHAIN 5 charts in the Level 15 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Fbfa65f45a9f5436a5dbc55a7f90d116b50ac748d5e881b56451ed4b40531a53a", + "data": "F19d35f0cf2a193b41f8", "type": "folder" }, "criteria": { @@ -2647,13 +2618,13 @@ "value": 9 }, "game": "sdvx", - "goalID": "G5a250fcecae6a6b81897f1995aa49a885def0f49e612d0002d401d5f522fd38d", + "goalID": "Ge8bef7d934fe71987f467f45f1c7d335062cc66470775c67f85fde23e8eea88a", "name": "S 5 charts in the Level 16 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2663,17 +2634,13 @@ "value": 8 }, "game": "sdvx", - "goalID": "G7a2666b1e2568598202eccdcf1de4fdcfe40812f4a33e9d8ca4e2073e5a7e864", + "goalID": "G202700527e32bf7de07b254e7b5c4a3629e8d15c428a5a1f22d2b20b21232a63", "name": "AAA+ 5 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "cf3900c96082ce1559917a0940d575c1bbc4fcaf", - "fd20f61ab4bd3cf590b9a3a29f9394e3395ecaf1", - "d2954f11da65ac7c7bf77edfdfd3dc37ae187f1a" - ], + "data": ["C19d35e11de939fe3f5e", "C19d35e11da7a2b49a00", "C19d35e120185035c8e6"], "type": "multi" }, "criteria": { @@ -2682,13 +2649,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "G23c50bcf3db50731d51d3e0c33d2572c83fdbcc12350d9e11190d3566fae9032", + "goalID": "G247303e9e1b7a1c4863c6024ec6e9bba3e8da377cf8ea44b925b296c98594d7e", "name": "AAA any one of CENSORED!! (EXH 17), UROBØROS (EXH 17) or Into The Madness (MXM 17)", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2698,17 +2665,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "G3c62ea2e1321ac2629743faa53c42016a006c714881cd99ec0437919285ed5d5", + "goalID": "G09ee2358ba001fc10bf09472c2f4b1edcf4a512c9a77661baf3cd37464ec0584", "name": "AAA 20 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "e31c3490b55cd980c893c433d551a2781b280bf2", - "0b68b53d241af59b286bad84e0c1fea35450460c", - "349b13af7948b6394b5379d6404f54888ae70fea" - ], + "data": ["C19d35e11f8e16a9bb3a", "C19d35e1200edabf797a", "C19d35e11f6e613ef1d7"], "type": "multi" }, "criteria": { @@ -2717,13 +2680,13 @@ "value": 6 }, "game": "sdvx", - "goalID": "G6407753fe57a62da297fde4b8c638ccc14211e2288c1af3fb575784e4e88d154", + "goalID": "G56caf3712a9a9805e1ee8eb7fe86673b64314baad10bbfc0e8c86b679051b444", "name": "AA+ any one of She Turns Me On (GRV 18), Enchanté (MXM 18) or KIMIDORI Streak!! (MXM 18)", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2733,13 +2696,13 @@ "value": 6 }, "game": "sdvx", - "goalID": "Gcdc3cf2ac89b63f88e687f718102c99ecb24769ea33e97ed4642b0e8380af63d", + "goalID": "G698f0bd2b5a3859478a49898f499bad6c84ec7989cec5822b5390ae33fcca96f", "name": "AA+ 50 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2749,13 +2712,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "Ge7541ba6c58aaf29c0e66f1b2709ffb88f558d932caeeccf2e4cd1af5f94d087", + "goalID": "G65278d2891251faf37d35823634a41b75f05843239b9ec80961e1ffc99f023f2", "name": "EXCESSIVE CLEAR 40 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2765,13 +2728,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "G722a7884a3552c4e449b11c83ee0883e8e0d3e246db5c53ea1814cc295784c91", + "goalID": "Ge7914262f1dabad65f3c12a74f2fe9dc72e622e35bac5a5ab7da601907687c8f", "name": "CLEAR 80 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -2781,13 +2744,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "G36a064626e4138a85f72f40a610fa4b320a8627d7b143043f226bbff50e8d457", + "goalID": "G9096ca58f8aa719866a945404c0a03fdfb2ab9c6c0cd5628e8e82cfa22ca6162", "name": "CLEAR 10 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Fbfa65f45a9f5436a5dbc55a7f90d116b50ac748d5e881b56451ed4b40531a53a", + "data": "F19d35f0cf2a193b41f8", "type": "folder" }, "criteria": { @@ -2797,13 +2760,13 @@ "value": 3 }, "game": "sdvx", - "goalID": "G82d70fe20b48a11d416a61ea462a0b8cc1bd6eb4a9b3eb545a30263cafab28a7", + "goalID": "G780126a57153930857d9bb1e3a8aa071f60fe58bf3dbf11a49033b0f249fb483", "name": "ULTIMATE CHAIN 5 charts in the Level 16 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2813,17 +2776,13 @@ "value": 8 }, "game": "sdvx", - "goalID": "G8abfe67c7680a61f6c1d1e98906475f7beacabe66e31f5845456bb7fbd13f0bc", + "goalID": "G2f368ae70e177729fbbe2b809198761ee4e374e8d64ecda15495df7a40e02cf2", "name": "AAA+ 40 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "906d6444ab6302d282c56bf16e709a73efd9483a", - "c3c452c08dc98349d19f421444dbe865aa9c884d", - "b41288563f0533827c58046c5dc5c677524e7d71" - ], + "data": ["C19d35e11e8e7534f29c", "C19d35e11d3a7a55cb74", "C19d35e11cf85dbaf63d"], "type": "multi" }, "criteria": { @@ -2832,13 +2791,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "G8cd2cfb82e8a476a66c7a8c3f6b60f7958a8acf2a4120181fbda9f59db564e39", + "goalID": "G130f6090577b551ba3a65bb2da44e5807d407c41f9dac35aa4ed259e0eb21d8f", "name": "AAA any one of HE4VEN ~天国へようこそ~ (EXH 17), TOXIC VIBRATION (EXH 17) or False Cross (EXH 17)", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -2848,17 +2807,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "G67afe3fcc2835cf40b85cf1253a352e9555d1ac66336d6482581069dc032232d", + "goalID": "Gc0447d29f102fc5bec6236e7cc2652568c0c32e01fcfb5f1fbc78d204b91b0d0", "name": "AAA 15 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "32b5f9069c910e14fa4742fbfbbf8b6c044b8381", - "56c22baf5fdd4f77bd0547ae7d30d836a40bf1bb", - "892be9aed05113dc72040416192aa607de392f12" - ], + "data": ["C19d35e11dc9b7fe43ae", "C19d35e11f5114594be5", "C19d35e11ea4d05b7556"], "type": "multi" }, "criteria": { @@ -2867,17 +2822,13 @@ "value": 6 }, "game": "sdvx", - "goalID": "Gcd4e04fc14af725f3d1ea3671e23adc4a5c536d6fe9a7e9f6cbd87483f028fc7", + "goalID": "Gf1dbcfa03d5e4496dea64d2093673b70762d6d66a3c5cc19cb329bd34d89841e", "name": "AA+ any one of XROSS THE XOUL (GRV 18), Re:End of a Dream (MXM 18) or ΩVERSOUL (MXM 18)", "playtype": "Single" }, { "charts": { - "data": [ - "3e59ddf0d47a21c952e3060a8a55c652e5b4e849", - "77e581a8ca57f6677c4655640241b3e828b7e2b3", - "1875c09d07fdb06c7d0e9f209e7f041b7cef0b25" - ], + "data": ["C19d35e11e92fa2b5166", "C19d35e11d4a7886bcae", "C19d35e11e2960cc06d3"], "type": "multi" }, "criteria": { @@ -2886,13 +2837,13 @@ "value": 6 }, "game": "sdvx", - "goalID": "G0b172f023b29fa35dc5c18dd4a764435b4877636c1d99e2960e91795ca83dd80", + "goalID": "G8980504c85d097d4b41a7a803ae476a050d2f82d00cb8c648feee4abc228d64c", "name": "AA+ any one of Fin.ArcDeaR (MXM 19), Got more raves? (HVN 19) or 極圏 (HVN 19)", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -2902,13 +2853,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "G3ce2fa38830ea34a4c106e80c57c082fb71e6a0f13570e3877d8d116c325de20", + "goalID": "G1ba1112c67f42f72661953a758f9d0fa41798e73337a2e42851bef711e489f92", "name": "CLEAR 90 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -2918,13 +2869,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "G37eedc8030f77487c846f6c0b47e51e420501991ea5b2a6bcf1b2c7204f203c0", + "goalID": "G0f51e650348ed098980ff3ef3a09392e01cc66279959e3e6dde3cc502ead527c", "name": "EXCESSIVE CLEAR 30 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -2934,17 +2885,17 @@ "value": 1 }, "game": "sdvx", - "goalID": "Gbb19cb56b35c8e0c75234944c7b3499a68cd0db804b9812079d2f4b8e4a2ebab", + "goalID": "G1a79132331135eca1bb77173e9ce844490b4624a7ecc5d7ffae02e40f4378404", "name": "CLEAR 7 charts in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { "data": [ - "23bee4f62dd8c4c260e8479df4068457c9d78622", - "6cd2d65dee0fc683b63f2ce8cfea0d4072e87c4d", - "5d2b4c981b3d58d521db951390273f221c9c4ea8", - "54a980cf285ffc90f409fa2bb88f8a5adaaf48fa" + "C19d35e11f0bad30e281", + "C19d35e11bd43bb420b4", + "C19d35e120e0df2b6a87", + "C19d35e1214834a337bc" ], "type": "multi" }, @@ -2954,13 +2905,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "G3d3c2ca6e7e7ca8c13b6abf0fba5f35492a2e6e0ea3918b4370fef8575e014dd", + "goalID": "Ge06603c39c07860b82c2d49cfe4303c386e8bd88cfafaf991932963112fbdf39", "name": "CLEAR any one of НУМЛ (MXM 18), ムーニャポヨポヨスッポコニャーゴ (VVD 18), MeteorA (MXM 18) or SuperMiracleEnsemble (MXM 18)", "playtype": "Single" }, { "charts": { - "data": "Fbfa65f45a9f5436a5dbc55a7f90d116b50ac748d5e881b56451ed4b40531a53a", + "data": "F19d35f0cf2a193b41f8", "type": "folder" }, "criteria": { @@ -2970,13 +2921,13 @@ "value": 3 }, "game": "sdvx", - "goalID": "Ge9b0dffe433395244dde749564ada73c29933739e8207d257bbc94c35642a83e", + "goalID": "G3bf0295c50cf8431f0d2c6aa13595d7dc4dcdf0738ed4288453a9047c3f1fe86", "name": "ULTIMATE CHAIN 15 charts in the Level 16 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -2986,17 +2937,13 @@ "value": 9 }, "game": "sdvx", - "goalID": "G34ce6ded60134f35b48b4b9fb7f32e0757f9b2c254ba14b7898f93a49c03433d", + "goalID": "G475ca03a09c5035de96d162dbae307e016cb16f14d7215817b5fbf613c88ae35", "name": "S 15 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "8e81441df561f80b94cbd35fd31a00ef6bcf56a4", - "8694be55c683329116c3d4a268c49a2d6f710039", - "2a723e1c6f89210d2f9d492338a44725fc3d1d55" - ], + "data": ["C19d35e11eb0be169eed", "C19d35e11e1c4ce9fa55", "C19d35e120916329931b"], "type": "multi" }, "criteria": { @@ -3005,13 +2952,13 @@ "value": 8 }, "game": "sdvx", - "goalID": "Gd11be9670bd13f96a70dab4ca5f60d0d929b9cb26dd35f9c2775e539f68ab491", + "goalID": "G044dccb6b25fc04451e9100bbfaa3b634dd3556c161f7b4a6c1da40d502913d6", "name": "AAA+ any one of Poison AND÷OR Affection (MXM 17), DIABLOSIS::Nāga (EXH 17) or MilK (MXM 17)", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -3021,13 +2968,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "G685db835351b0918d0e71aefc4b10cbfac7a9e7e23294b99423eac41ce63df08", + "goalID": "G0a1437544cfd3bc60d98e11db00ec4aef55df24454e63fa986d6de3cbd9c02f7", "name": "AAA 30 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -3037,17 +2984,13 @@ "value": 8 }, "game": "sdvx", - "goalID": "Gd04e7f4fdee9c4173dee2b1a40111f88617c0d6cbffa4880c26130e1c7969b80", + "goalID": "Gfbf6843a90e7657e22782f9cee31c2e6448047a4afae1432d7dffc6b7c1f8305", "name": "AAA+ 15 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "29b4bab6647e12580dbda8c5f0f0368926f8f055", - "a48487c183029ecf4e471194e09545371a8989cf", - "3236410d1422e3e6cea8f834c6acb6719dc5b8a9" - ], + "data": ["C19d35e11db98d00c586", "C19d35e11db31802a858", "C19d35e11ca41c6a9a26"], "type": "multi" }, "criteria": { @@ -3056,17 +2999,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "G3c76b02aba28d133f4df7d4ae457d990f73a5e9d85cb1390d7b86ef4e6134151", + "goalID": "G1bd1da343629dd0fa15bcfe24cc5cc0e67f0d67b255c9528b0428e47be9813ad", "name": "AAA any one of REVOLVER (EXH 18), Demise Quartet (EXH 18) or Wish upon Twin Stars (GRV 18)", "playtype": "Single" }, { "charts": { - "data": [ - "404645d474cd207942ffa0a9395370448ece6379", - "89056ceb94e7217495922b0e58b2e57aba82b5db", - "789d45a0a13552fc0ffd46bdb3f61e570253ee7a" - ], + "data": ["C19d35e11c4418d22289", "C19d35e11fbaf4bc2192", "C19d35e11f64f3ed2258"], "type": "multi" }, "criteria": { @@ -3075,13 +3014,13 @@ "value": 6 }, "game": "sdvx", - "goalID": "G31b1b568376afa7187b1ee3aa89eeed0b8d0daa42cff051679511bf6856e6101", + "goalID": "G0515f7a6c498cc4ff2712a4178010910de0454c135b1fd0ea9710a96b019966a", "name": "AA+ any one of Idola (HVN 18), Xevel (MXM 18) or Venomous Firefly (MXM 18)", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -3091,13 +3030,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "Gb722c2d8c46d04ad1d0d4147b92f42b2dfba8cb5cd0160fbe1424b2f93af81b2", + "goalID": "Ge89577e221b6a55ff840aebd8f9ba403f6e219f9d034ea835e978292ec1cb479", "name": "EXCESSIVE CLEAR 75 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -3107,13 +3046,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "Ge120b5fd900dbf3d2bd80e08dc8abd58709ea5d9ca97c7a0bf3e916a02a1cfe2", + "goalID": "G8924c90d4678ac3d0495f6ad67ae4ea07c752a5a6d28b2629f26b8d98dbd9e5c", "name": "CLEAR 35 charts in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -3122,13 +3061,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "G6783d25ac9d92c0bc1927fe553c50cda7bb878748df6275f4a983e67ee985248", + "goalID": "G607d0a0684e41637139fef8ff230f41bb909942c9a94f57f7d0485a0628264ce", "name": "EXCESSIVE CLEAR any chart in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F587912aa1af1c26daf085428f3f3ae420cbe905775782f9e2506108d3ff03a5a", + "data": "F19d35f0cf2a89b2f040", "type": "folder" }, "criteria": { @@ -3137,13 +3076,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "G984819589a24c33cf390855b5ce944ac5505632b912fc30f6abbeebdb08ecb1d", + "goalID": "G72bbfc671c0f211355085393012b9835ac65d8491ff2431f878626f89df7eb07", "name": "CLEAR any chart in the Level 20 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F1d4d6156ad27065d590bb0efdce48df95e9cf088157a97e391cf89032653c799", + "data": "F19d35f0cf2a69b98cad", "type": "folder" }, "criteria": { @@ -3153,13 +3092,13 @@ "value": 3 }, "game": "sdvx", - "goalID": "G446cf9f06120cf2bbbdcb991c949d15943b1b4324d3d3d20e478a07efa9d0f88", + "goalID": "G90bcbf690a855cc9be365f22c9fcfe47e1e73b158a2cc2ea228e14452825f354", "name": "ULTIMATE CHAIN 10 charts in the Level 17 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -3169,13 +3108,13 @@ "value": 9 }, "game": "sdvx", - "goalID": "Ga18826414797270c316fd9b3838e73214ab45478c317590fb3cd9d9a0953aff0", + "goalID": "G6358c8004675bb53ae6bde30a8cd93735aa8fd123438cd93256d70fbd6f29cc6", "name": "S 15 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -3185,13 +3124,13 @@ "value": 8 }, "game": "sdvx", - "goalID": "G4a4220694788149fc08096febe84ddc584b26b6c9ab40c4b0a46bd2acb897b20", + "goalID": "Gfb2a810bd50e3d3813a6fbd939119c340bf250cfa9843b65f61bdc8301b74ef9", "name": "AAA+ 50 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -3201,17 +3140,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "G77a7f616a426fd5d305ab73ed4f4cbad6c22357a9bf20d0fea763e9d2c0ca2fd", + "goalID": "G4a159b7973fabd82ea7e51bc96d58fc94df7fe15077769d711a155445b9eff63", "name": "AAA 100 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "17804f7fd79f77a439eefe9f78553bba4fc70c7e", - "9b195eec44424a241c40d72aca1a5b72d4a25760", - "9608e9b97734cdc18ef970f3c36378efa3209389" - ], + "data": ["C19d35e12093ed1d3609", "C19d35e11f256efc4a68", "C19d35e11fb0cc4f031b"], "type": "multi" }, "criteria": { @@ -3220,17 +3155,17 @@ "value": 7 }, "game": "sdvx", - "goalID": "G380901c2d644407924611992a93ba2d23709366d261e73d671561589c4657df7", + "goalID": "G259ab31e08b751f67b3fc496de2c4ae262274962ec5589e2e4a2880b70d96857", "name": "AAA any one of ANGER of the GOD (MXM 19), A Lasting Promise (MXM 19) or Elemental Creation (MXM 19)", "playtype": "Single" }, { "charts": { "data": [ - "fb14854eae90e56739e5eb8188f42dc40983203a", - "abd8c85a798a42499db64b0951d5baddda6229c8", - "bb89bffccc6a07439441d58a13f2262933c2d4f3", - "6d230651c8baa198781a59d162b7b3ca293308a1" + "C19d35e12065c03ed06d", + "C19d35e11bf7c6543c42", + "C19d35e11bf6ba32797b", + "C19d35e11edacce45bc9" ], "type": "multi" }, @@ -3240,13 +3175,13 @@ "value": 6 }, "game": "sdvx", - "goalID": "Gbcae3cba420afc06b123e43ba6c2305d14972e0f75ce87bf97dd09fc9c7381bd", + "goalID": "G899d13fe5225f7fca4cc5a3ad77afe02ef7f03a874d3ee2ac5a1a177b9240acf", "name": "AA+ any one of OUTERHEΛVEN (MXM 19), For UltraPlayers (HVN 19), Bangin' Burst (GRV 19) or Dyscontrolled Galaxy (MXM 19)", "playtype": "Single" }, { "charts": { - "data": "F587912aa1af1c26daf085428f3f3ae420cbe905775782f9e2506108d3ff03a5a", + "data": "F19d35f0cf2a89b2f040", "type": "folder" }, "criteria": { @@ -3256,18 +3191,18 @@ "value": 6 }, "game": "sdvx", - "goalID": "G53b10706ce6041409c4921177d36d0d700e8c9366efe7ad8e34660b07df1087b", + "goalID": "G9540d88dcc938ebb0ef124b5f031eadc0966c826023e9719c59e349487cee00d", "name": "AA+ 2 charts in the Level 20 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { "data": [ - "d3a0355c54bfbe7cedb61807def2552578922502", - "f35b6c7a1c66450ff2b4d28d34863d6372471e85", - "dc66d62fd45ab6d4f49bb80e7f80214a32830714", - "6cd2d65dee0fc683b63f2ce8cfea0d4072e87c4d", - "23a140b3a02df862cd037f1ccbce0993a5775a50" + "C19d35e11d1e805a5f51", + "C19d35e121317dc1d3b1", + "C19d35e1215841b89966", + "C19d35e11bd43bb420b4", + "C19d35e11b7ece8eb9f2" ], "type": "multi" }, @@ -3278,13 +3213,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "G1e61ca0b8b1941e8fa72261d8f08285fcdd919f8438017a46bcbbb059961910c", + "goalID": "G1f9940b18b32059b73851cfd2d813ed952f6ae55a917fba2f2318091d5323df9", "name": "CLEAR KAC 2012 ULTIMATE MEDLEY -HISTORIA SOUND VOLTEX- (GRV 18), Unleashed Redness (MXM 18), Verflucht (MXM 18), ムーニャポヨポヨスッポコニャーゴ (VVD 18) and Joyeuse (VVD 18)", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -3294,13 +3229,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "Gd39322849e696f84aeb5f0a80ec4420ee56c6ef7c05fd430c436d2fb7ea2bbdd", + "goalID": "G00b9df1cc7183235c56613655d77b35df8a9716a4ff456b86629269fa579a1a4", "name": "EXCESSIVE CLEAR 40% of the charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -3310,16 +3245,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "G2667d24e2fb0015183e6d9d9e0bf277e1026ed2b1020d1787673ffa6692b9506", + "goalID": "G465cbe195779daf0dd8449ead358f77dd3047c01538d8a8177fcca7f6c7732b9", "name": "EXCESSIVE CLEAR 7 charts in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "398c3d11f704b00e0b58adefcb94dd4913efd2cb", - "c5ab692c15d506f75f862c72279fe2e635e6bcb8" - ], + "data": ["C19d35e11f82f2e7ec28", "C19d35e11f2c731fd2f4"], "type": "multi" }, "criteria": { @@ -3328,13 +3260,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "G05c2ab5e047aa67b80ff338e4c4e108f784b129105970a8ea7dad02bb488b3ef", + "goalID": "G9957c837ce04f4494df641b057cfc3161fdc8a4ec9898dc136548f46f75bc41e", "name": "CLEAR either Xronièr (MXM 20) or I (MXM 20)", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -3344,13 +3276,13 @@ "value": 3 }, "game": "sdvx", - "goalID": "Gb287f5494eee0a93e4a6f890546af25d5ae3a4f2e78b58f2fd31040c40fa354e", + "goalID": "G0f88e481ccc93429f62c7124103bb0d29c80eaf46265cb42ebf873fc1cded1ef", "name": "ULTIMATE CHAIN 5 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -3360,17 +3292,13 @@ "value": 9 }, "game": "sdvx", - "goalID": "G56a9563f7da4e29b97be8dcdc95a384e7bac136566e1170c9b60d38fc79314c5", + "goalID": "Gba598c9c26a325bb63a54c9fb6be633ef19f4c23e13f569204d3d586a4b6670e", "name": "S 50 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "582580eaee4239b53b6e0fb9afae841105af0d22", - "fdc62614b45e260f9060bf8a650e799558ad5984", - "3eab57430f2f3e524fc19b23c6a1ade73afa928e" - ], + "data": ["C19d35e11edd1a77b160", "C19d35e11f95c08dff6e", "C19d35e11eff5f8f499e"], "type": "multi" }, "criteria": { @@ -3379,17 +3307,17 @@ "value": 8 }, "game": "sdvx", - "goalID": "Ga08f0d8f1b4616acfac36826684318e4335f31106235eca6fbda80bb7dd36018", + "goalID": "G13a74d151e58d9e4edea8c247804cacc51d435c831e355408ecfe4aef8131942", "name": "AAA+ any one of Juggler's Maddness (MXM 18), ARROW RAIN (MXM 18) or BEAST BASS BOMB (MXM 18)", "playtype": "Single" }, { "charts": { "data": [ - "6c509b82109b09f7468710576992f211fbfda149", - "590c969321f2df3302fa089822b6f30ff1996e00", - "5b1d8a964b378fa2dac29aba5d0e2aeddf159cdb", - "27d0c0a80b69e90cdd55fc4b251cb35785d69a99" + "C19d35e11fd1e9472d3b", + "C19d35e120824c51c1af", + "C19d35e120692a824c55", + "C19d35e11fc11f0de15f" ], "type": "multi" }, @@ -3400,13 +3328,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "G42f1df315e5648de270061cdc00adbdd07c4fcb5ba34dee344063e94b076ac4c", + "goalID": "G6b92ca680c48f3df1ccf2772438f75abbcc1de394ec6c2ddd6e5fd7727cfef63", "name": "AAA any 2 of V (MXM 18), Macuilxochitl (Latin Jazz Mix) (MXM 18), pique (MXM 18) or †渚の小悪魔ラヴリィ~レイディオ† (MXM 18)", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -3416,13 +3344,13 @@ "value": 8 }, "game": "sdvx", - "goalID": "G32afd19762c11d926ae794f659b99c31a0c21097866a792e95b931a49dddbdfd", + "goalID": "G011d1f17d3a353d69ad87465ea6962d8e83843c4f6cf0caf0e8a45792800ea09", "name": "AAA+ 5 charts in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -3432,18 +3360,18 @@ "value": 7 }, "game": "sdvx", - "goalID": "G0e1f2bf6e45848b28311505260a5c04951ce9f5fd199363e5fc8fee420c39a17", + "goalID": "Ge11fd188ac70c54f6b37465777598f77980ab49565ab206118a5b85a73d47194", "name": "AAA 25 charts in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { "data": [ - "2600985c774e0863099b3fd2639202f658abf713", - "6cd2d65dee0fc683b63f2ce8cfea0d4072e87c4d", - "77020504ed3869737d82e71bc027e3e553599077", - "14fee79c579be39f6a542d368a119e3a419642bf", - "c98a63cf50af10a88a7d964f3449af8aac4950ba" + "C19d35e1206420cfd974", + "C19d35e11bd43bb420b4", + "C19d35e11b033cd8120f", + "C19d35e11f2d49948acb", + "C19d35e11e848b7dc8b4" ], "type": "multi" }, @@ -3454,17 +3382,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "Gb29e1c2989fc5cf181ad34af23025d56a39ba5481e10f67ee468b229328b404d", + "goalID": "G8bbc3e020dba18d3d294f2a51f722e1460b047324a80b15c44db4eb81de65729", "name": "EXCESSIVE CLEAR any 4 of * Erm, could it be a Spatiotemporal ShockWAVE Syndrome...? (EXH 18), ムーニャポヨポヨスッポコニャーゴ (VVD 18), croiX (INF 18), 神話に芽吹く (MXM 18) or 準備運動 (MXM 18)", "playtype": "Single" }, { "charts": { - "data": [ - "5fbb00173534ae249a38b9b39628687f8d7a9a3c", - "bb89bffccc6a07439441d58a13f2262933c2d4f3", - "8b795ebee16fc9fa7ad3122b069ef1594817bf96" - ], + "data": ["C19d35e120ba2a3794e5", "C19d35e11bf6ba32797b", "C19d35e11f2f083cda5a"], "type": "multi" }, "criteria": { @@ -3473,13 +3397,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "Gb00614020a091ce7df081bbab49e16c50715f4d9bae0fb4a643c55ee3d4ce88a", + "goalID": "G5882d96f0ef38eafb99037fdfc4e61a1fcc0038d5c19bef289f5ab055163f83d", "name": "EXCESSIVE CLEAR any one of 色を喪った街 (MXM 19), Bangin' Burst (GRV 19) or Awakening (MXM 19)", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -3489,13 +3413,13 @@ "value": 1 }, "game": "sdvx", - "goalID": "Gf827d9b6ab5f37ced9ffc825bf9bff85d16d597a6f49c4008c771d38becf1e95", + "goalID": "G3a85c5204e9708532cf7f0c038bc6e65cfc929a40d090faaf361a07fb6b8bfa6", "name": "CLEAR 100 charts in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -3505,13 +3429,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "G0d74c382453d4fea5706533e1d748519b2b566351eb09a1f13704630c20eda31", + "goalID": "G10db40bef5ef910ec3b247621889e66bc431220746a5e536c45d4f8bf0386f36", "name": "EXCESSIVE CLEAR 35 charts in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F32e3ab1ab5258d702efa6bb7690194d6abc9cad66e21daeceaa5c58a7fa4c41c", + "data": "F19d35f0cf2a4b184f28", "type": "folder" }, "criteria": { @@ -3521,13 +3445,13 @@ "value": 3 }, "game": "sdvx", - "goalID": "Geee0d6a1d6729fa68e90d32453453f0ea89d18d98092ea65db8944d3d9923b74", + "goalID": "G5234d7e017a9d520b2714f84bd66ff9ac28e04573e4afff04249bcef6ed7c0ed", "name": "ULTIMATE CHAIN 25 charts in the Level 18 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -3537,13 +3461,13 @@ "value": 9 }, "game": "sdvx", - "goalID": "G8a9df565dad7b7eeb7e4f55328f0a0a218e807792875881c3011a5220f387f0b", + "goalID": "G10b47f5c3084d79bc92d45e616dcfb7dc3a7040aa3155fd49397352be6f5b6fa", "name": "S 10 charts in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -3553,17 +3477,13 @@ "value": 8 }, "game": "sdvx", - "goalID": "G06c67b34e5d84643f0a6b863f7323a7497483d96c92ea3e1018d1b209801534e", + "goalID": "G582b57b4ce8cf63c44690d329d9e80f9701a841ca3f8c8dc291dbdcfb545a38d", "name": "AAA+ 50% of the charts in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "af2a885efa9fc6c0caf213262f096d3cb3c487e1", - "45b3bb92fce693be1dc0ce5835006ad7bc244321", - "6e46c458f855c11f758b20b896d24d7b45f41868" - ], + "data": ["C19d35e121160373cd93", "C19d35e11edb93dff80c", "C19d35e11fdc89943ed9"], "type": "multi" }, "criteria": { @@ -3572,17 +3492,13 @@ "value": 8 }, "game": "sdvx", - "goalID": "Gcc178c0d77ede6a01125d09e2c9df9cf6257620178b00a7d5a791b1d532bd737", + "goalID": "G7d14143c6d71c5a9412d4ee87816eba1b926e1f9e282d482cf4c8f8c1bdaebb0", "name": "AAA+ any one of VALKYRIE ASSAULT (MXM 19), セイレーン ~悲壮の竪琴~ (MXM 19) or TENKAICHI ULTIMATE BOSSRUSH MEDLEY (MXM 19)", "playtype": "Single" }, { "charts": { - "data": [ - "420447bc8ce601af5ab2eaf1f3c0d64b7353f3c9", - "c84e8adac03850de691b856d075520db36fee105", - "1fcd897e6833349b05d27e2086fc1e256641b445" - ], + "data": ["C19d35e11e1d061b96ca", "C19d35e11f838e8b8dc1", "C19d35e1210bfd434371"], "type": "multi" }, "criteria": { @@ -3591,13 +3507,13 @@ "value": 7 }, "game": "sdvx", - "goalID": "Gc987927c7c4b07a08d4fd8c8bd21999f7c59e8dd7201510b2dd18aeb590e30ff", + "goalID": "G04fe718f0483fb71361bb3bd82d42674b25ca069823831c8907f52f7a68b88c0", "name": "AAA any one of FLügeL《Λrp:ΣggyØ》 (GRV 19), Failnaught (MXM 19) or 飄える翼追い掛けて (MXM 19)", "playtype": "Single" }, { "charts": { - "data": "F587912aa1af1c26daf085428f3f3ae420cbe905775782f9e2506108d3ff03a5a", + "data": "F19d35f0cf2a89b2f040", "type": "folder" }, "criteria": { @@ -3607,17 +3523,17 @@ "value": 7 }, "game": "sdvx", - "goalID": "Gb454a482cd31c757cafd931aa41542fa60c219c13d46214c8514c10b5c792d59", + "goalID": "G9c178aa1967a795a17a9eee9c84bbabd63b4cedc3223c5c6fd245839729718df", "name": "AAA 4 charts in the Level 20 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { "data": [ - "23a140b3a02df862cd037f1ccbce0993a5775a50", - "a7753c0b40e19a1912d6370d43496a07aae66cdf", - "1bd95abdd8543c1a4f441127ead5a3ce3cb61559", - "98bf510b894a8d31add4c49b301f390054cf1e5d" + "C19d35e11b7ece8eb9f2", + "C19d35e12000ee883c76", + "C19d35e11bf5d7cce40c", + "C19d35e1207e7613f6aa" ], "type": "multi" }, @@ -3628,13 +3544,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "G0c761389ef0fbbad848457ddd0e98fc55c282180271ad16ca4c9815846fec994", + "goalID": "G54b92c7e85b819464c7bb21b9c1665fc745c13f289ebe2946ea36bc6f0e074ac", "name": "EXCESSIVE CLEAR Joyeuse (VVD 18), ツマミ戦隊 タテレンジャー (MXM 18), 超越してしまった彼女と其を生み落した理由 (HVN 18) and Re:Elemental Creation (MXM 18)", "playtype": "Single" }, { "charts": { - "data": "Ff75e405b41ca512993548d6cfca8b6ee2310234b2a7c5fe9f93638abc3e49fb1", + "data": "F19d35f0cf2afd13b38c", "type": "folder" }, "criteria": { @@ -3644,17 +3560,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "G9cc798bd379dad47ba50176b236b09209a448b6eae397e932169b0552485e2bc", + "goalID": "G647d4f2a60cf456a392b6f062725978732d1723ef93b0cc2dc1bb75302e9479b", "name": "EXCESSIVE CLEAR 80% of the charts in the Level 19 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": [ - "74fe233dc115ca82383933f9a35b3b4508674cfc", - "0026f55427c41d987f5b2277d85165fcfbcfef14", - "9fbebbf8b0d8e1aa5232e8e4717f8877e18b73dd" - ], + "data": ["C19d35e12099bc435ee4", "C19d35e1215439a4b677", "C19d35e12048a4a873fb"], "type": "multi" }, "criteria": { @@ -3663,13 +3575,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "G15163c2d1ddf5856d12492deacb2317cd48c47bc325a61f515dad052864b2158", + "goalID": "G1fb1cc0ee9612ead59f8792d4fe28ba527af5fecdea0b2198f9c7ac2c4d2abd4", "name": "EXCESSIVE CLEAR any one of Xroniàl Xéro (MXM 19), 777 (MXM 19) or Ghost Family Living In Graveyard (MXM 19)", "playtype": "Single" }, { "charts": { - "data": "F587912aa1af1c26daf085428f3f3ae420cbe905775782f9e2506108d3ff03a5a", + "data": "F19d35f0cf2a89b2f040", "type": "folder" }, "criteria": { @@ -3679,13 +3591,13 @@ "value": 2 }, "game": "sdvx", - "goalID": "G33d09280105bfe16aaff54dad304739e2e091c9a1e647164caf2394ee513a2e2", + "goalID": "G5e9b6f5ab4339cbfcc62ab4f13f0ddb4c2576573e0f25f032602d11816acd846", "name": "EXCESSIVE CLEAR 4 charts in the Level 20 (EXCEED GEAR) folder", "playtype": "Single" }, { "charts": { - "data": "F587912aa1af1c26daf085428f3f3ae420cbe905775782f9e2506108d3ff03a5a", + "data": "F19d35f0cf2a89b2f040", "type": "folder" }, "criteria": { @@ -3695,7 +3607,7 @@ "value": 1 }, "game": "sdvx", - "goalID": "G7d0dc89e17f74a9548c10538e3e2c6e6b99e2c5e77f98699d104f4c06682a432", + "goalID": "Gf67e8bf457336de147cd7e3aa5fd27d3aaa33f057a66452c4a3eae73d73e0ef0", "name": "CLEAR 100% of the charts in the Level 20 (EXCEED GEAR) folder", "playtype": "Single" } diff --git a/db/seeds/quests.json b/db/seeds/quests.json index e6f0d59bc..06af4dca1 100644 --- a/db/seeds/quests.json +++ b/db/seeds/quests.json @@ -10,23 +10,23 @@ "desc": "", "goals": [ { - "goalID": "Gcea58fb099db74612d9654c4e8fec345154ce00c31d7525ea03efc7389a27c09", + "goalID": "G1a2499cae27572b8bddd861b28aaa01dbe4c885544f15fae5ed91765a7b924a9", "note": "It's all in the earth power." }, { - "goalID": "Gb0d9e72831b267ce5f1feaa84f99080df6a9b371ce3a22b6754baca3c8afffaf", + "goalID": "Gdee336867a22a0a789ab45b863775f4351b6f70fbaa0ea1928c977179320d0ae", "note": "It's all in the speed." }, { - "goalID": "Ge3165eb0547fb894c169abc11e6f541b839ec8535bef72c895b64ca06b5767ae", + "goalID": "G36f76a980802c1f06060e29f0d4d590fa06cb2ab2d5059b62e72489b98e0cdfe", "note": "It's all in the soflan." }, { - "goalID": "Gccdd7b7f8f476397bf624791745252d71fe5eb276f832c8dc05384ae8a5c9c6b", + "goalID": "G53d4c238478106b7a54b865f02d505f650c29b9bda8ff7ce229389b2bafd575a", "note": "It's all in the turntable." }, { - "goalID": "G27018d6ed0720684cd03cb99104d1f1146e391f5e53ddd0d1d488b6e06722c1e", + "goalID": "G4a86b8c4a0e2c60bbf713d1be8f4ba7e4892ede6954a499beb8100f41ce78391", "note": "It's all in the uhh..." } ] @@ -36,27 +36,27 @@ "desc": "", "goals": [ { - "goalID": "G3ba44cf2fa1a53bb8de5cc99d5d4c5208ebd06250c2052393d7a4a830ab72f63", + "goalID": "G6da6680177acd81b493da1c8163174b6ac517c6463b25f0259f377bc9f2a3966", "note": "This is a classic landmark for timing ability. Push through the intro and don't let nerves trip you!" }, { - "goalID": "G2ffadcc4aac205cc4af0c91e2c20dcb1a68564d591d7c802467e7122b70a7fda" + "goalID": "G749a8d3ac2bf391d65aee7491bd64bf0b20097ac4c097bc9dd0f42bccf5da047" }, { - "goalID": "Ge536bd96adc49bb76c4168fe6121e8e0c046a9c82c8e22a72f9150ae9ca317b8" + "goalID": "G825995e16f75db2a31953a41564f3ceabdb250a3388843daafd527098989d78f" }, { - "goalID": "G76994cf527cc8f78cc5bec3591439b980087e8e5a305e04e418d70b4b8556d61" + "goalID": "G5b99b92615fc9d1554e8bec4264885d642a5dd5ab9daba82cfc840da6261fcb0" }, { - "goalID": "G9758f5f089bdc9757ad5991b1282f4ed39f5a197343f1fbd3d9f6a5a5985f8e3" + "goalID": "G39cde64528a80e9a7b22f6a612d5567945c9e1e7a6e6bdb7275ff39029d98a71" }, { - "goalID": "G4fd0a9ec034100c7bbf40527b0bc55be71617c545f06d4c7feb4795fc5ed0601", + "goalID": "G2634ee3209ddc4d87edb90c34471ea8603bf22885ac53daae36979fcfe656182", "note": "Depending on your proficiency at weird stuff, you might be able to get this *way* sooner... or way later." }, { - "goalID": "G018c3d8a77fb97bf5534f29ff960797f82effd1c8c3852da7ddf62170bf0fbba", + "goalID": "G1dafbb9a067708d0d52610a52d9e3d9efdd1ee8c40198f23a7bb219e78c5513b", "note": "This might be the hardest one here. Good luck." } ] @@ -66,7 +66,7 @@ "desc": "", "goals": [ { - "goalID": "Ga13da591f9b95e132a7a1947603745649494154cec51d1dc427a9b83633edb64", + "goalID": "Gf460944fe4fa8d1cd8b1db041e9475b33e4d25f6dc49b383ea144d4e5180dd4e", "note": "This isn't actually that hard, it's more to make sure you're playing a wide variety of stuff. Don't get tunnel vision!" } ] @@ -76,7 +76,7 @@ "desc": "", "goals": [ { - "goalID": "G4f7d560c4b930e1c689c8aa10c87fc8bb873231da207f7375dbcdac46b6ecf35", + "goalID": "G1f254c17d443890622d34cee5ea0e216184c9637f76e1266eb99e0540da9fdc6", "note": "Use the tierlist to find the easier ones!" } ] @@ -95,23 +95,23 @@ "desc": "", "goals": [ { - "goalID": "G226a21e1f284a93142fe1259067fceef84ba44eb1dc8d5fb02059f0c30d2b39b" + "goalID": "G8a7ec915d0613ce018202caa1e16dc9dbbf5ce18cb391110bc091e9136da6e00" }, { - "goalID": "Gc3a2eeb0712812682aa514e0029cb4c1b2617f5c1ed51d80319f796f388ed29b" + "goalID": "Gcf8d759883e71996af9623499956d3d6ee6e591b7a58e6b8698469580ea2212d" }, { - "goalID": "Gb36433d9e006d2371ce7cd337b798ec63afee190a796b5cd9c4e901c86c973df", + "goalID": "Gd467e4325e60010906055a976204bffd89a5fa805fbcf898c62618a8f81a57a3", "note": "Depending on your CN proficiency, this could be way easier." }, { - "goalID": "Ge0238448ca7e640b3c2330b4e8352eda8ce728de9b49f80b25fe5e69bd34a6f0" + "goalID": "G01d884675908fc9e06a7e5b477afd10bf932a06ccbef8dfa25d5eb40ebedc703" }, { - "goalID": "G1381f81b6b7e50e8834c37fac064c264da3d5b082424b9427a5abbde12e1a46e" + "goalID": "G9cf0ca4046641e30a86ecfc206b095bc58c344907840e81a3f6f96ce7897dbc1" }, { - "goalID": "G200430b184083eaaf5e70448db9923b69aca66472f45ce981e150a53f2deb9db" + "goalID": "G9ec817b5b48790e7655be86536ea9f3b977d4b8f33bd2c0fef4b5675eeef5bb4" } ] }, @@ -120,25 +120,25 @@ "desc": "", "goals": [ { - "goalID": "G6bc92b864cdc885e5b68afcd743a552a7c9f4b42d2388cf66003472867e3bd4e" + "goalID": "Gb8464c140da50ad417aeaf8223fe260b3f88a992c6c88bb031d72cccba48a55a" }, { - "goalID": "G8c14a7e978b55b03be418b6d57b4b828b974b1d927bcf23d3b6b2489259004ef", + "goalID": "Ge0973c181b48cbf54a3f6d14946f867a0ac238c3cd39bc572f9e64601652ace3", "note": "Don't get nervous, but the ending is the scary bit." }, { - "goalID": "Gef6390558f110e73135f38ba3693a2a0bbbfebf79f5d05a18ac542037c6e82f7", + "goalID": "Gb3bb20d0b86bf071d17a94a8a97b089486dcea29956d1e4561c17b0fe02afb36", "note": "what is it then?" }, { - "goalID": "G960274ddf4dea2cacd559f121430ec00ea676f7549a98477b0bc4513784755d0", + "goalID": "G999a566fdad5912d5ba7bdd4ecad09438215ba1cca2a7085ae7e9404178d44ae", "note": "Huge milestone for timing ability." }, { - "goalID": "G3fa6d1586ce8508e3d479d5e78eb00fa137b014bbd75d932312b0ee70c386b15" + "goalID": "Gf4e2efb99bcbedcec5afe6128ad9b48c2329bc604e9f3676d8b77e4bc68708f6" }, { - "goalID": "Gd296f58f71491c79d6a957e4d1f51b0eeba55cc2c18c7cd2dd6daaa72b8c1fac", + "goalID": "Gd1c17cae156a8cc9993adafce8924bfee58558c51d59eec87c687c281499dc8e", "note": "This is MAX-60. " } ] @@ -148,7 +148,7 @@ "desc": "", "goals": [ { - "goalID": "Gb42704d7405a574587b6e56732f13326c48f56b9ecb9a8ae5ed245d56999dd46", + "goalID": "G661132cce7d7b205f4ccc463073f7eccdd8ad0700539f5f86fff216be4a62799", "note": "This isn't actually that hard. It's more to make sure you're playing a wide variety of stuff. Don't get tunnel vision!" } ] @@ -158,11 +158,11 @@ "desc": "", "goals": [ { - "goalID": "G0ed4c552766ea82145c8a9fbf4ffa1df3fe5645c5141db3f48ec7c5af0513c4d", + "goalID": "Gd064ea10a7d7a330cde62b2047047bb7116f769b56082efb3fef7a95f9c0d125", "note": "Use the tierlist to find the easier ones!" }, { - "goalID": "G31d3218b8e4a5f4c860d9b2f29247472cb9481f8b81a0e2829ec6d4e9ec1a040" + "goalID": "G34bcb7d0ec46bf486014ff7cccaf4b059572a58944d2c31ad628126ac8826358" } ] } @@ -180,23 +180,23 @@ "desc": "", "goals": [ { - "goalID": "Ga5dc820413f88b58e703f615324e0ee07aced954a62a601b42756bbefd648c60", + "goalID": "Ge2ff55571ba96a533f278c0536473d0f16379e306b775cc1c63c20c5c833d201", "note": "At least it's not SAMURAI-Scramble..." }, { - "goalID": "G71bd61d5846a87127e447c0e6be298e320b1ba8547107b4c29ff1fbc4e976e94", + "goalID": "G16d80e16e4657d413c93b203c6938432159f53b93dc05ead19dca8614b5f9819", "note": "At least it's not 疾風迅雷 [L]..." }, { - "goalID": "G3be852df2c79b97a22ce7b5b3c04846e992d0a2a989aaeac89b668bd4ac4a3aa", + "goalID": "G2f376d12b93d701297b28c53b6bbbce4b44a61156bc739288a2ed70fd4f6c5d1", "note": "Welp." }, { - "goalID": "G39cc151b00da6e08a49d2c1ffa28b376e600cbb0d30ba369e94abb4cb909522d", + "goalID": "G48490053cd8c865b0b7341b8728f8dcfc6b22c7a93a06cd14b328d0811e12b36", "note": "You know what's messed up? This is the most normal chart in this list." }, { - "goalID": "G69167cb3007cbc10ff3f56435cee6133af5a3ab44d8b90f482342cb277942c76" + "goalID": "G53a3bad5acd46acc7ec0334bc4b4b2c317583e398e097bd219e148ccde0541f1" } ] }, @@ -205,37 +205,37 @@ "desc": "", "goals": [ { - "goalID": "Gbff5754db52ea104e33fa633c50646da699ccfaefc63e5b4999ce389d0db7ce3", + "goalID": "Gfbab43227c6ad53f49a2333114f97412467b657164750f0f5288a64982f42eeb", "note": "If you can hit it this should be remarkably straightforward." }, { - "goalID": "G8621b92ec1aa8a15312e664940f216f500ca6a068405ff384e3c4fb846b81bfc" + "goalID": "G19c09c3f23a7f2216d7a0fda3a525ce614f772a4fd455ccf40aa7f8f7a26b7ab" }, { - "goalID": "G2935018dd82616079e0c56953f0de19a01afedc63b07c7e83003f9a9c76d17c8", + "goalID": "Gd29cd861d7aaf7d27050950e69126f8f92813e96804f440458f3009a4079cbd6", "note": "flex those muscles" }, { - "goalID": "G298166932cfa50abfd5fdb67f5e16e5a46bd9c8b83be93db98147156bf82c872" + "goalID": "Gfb59b854a2d0f8dfc56cab487f4b43f4098cad073bfca42b99edb125d20e9c61" }, { - "goalID": "G37750486bcc1c26f93958af699d0e1ecf8201e8a021ea2e17bc25707a87ce5a9", + "goalID": "G08ed442b3f0210371e99276442281c34d4f6703bbb301a229502ef5054f1ffd9", "note": "Fun Fact: This is the first scratch timing goal in the entire questline. Funner Fact: Some of you will find this the hardest goal here." }, { - "goalID": "Ga5db62596cf669c2593b8b03329e6608026fafc35ef5c2238416bb6a28b9f62a", + "goalID": "G2be92dc54829e1da6016d6145b324b1a43e1ecefca262dd4bfdb68a9f35cc43f", "note": "eeeeeeeeeee" }, { - "goalID": "Gbed2eb0c5ce16d22ee66f0c07b921ef9d96bd3fe80f5a943aa52f0147d889e68", + "goalID": "G8392d2f9da662e5a7034af3e0264bb447e290d539ba4dc46ddc185f5063965d5", "note": "It's back!" }, { - "goalID": "G4e6cc043b09a7cb0fa8b7ae3b0c970f5c6de6bc78ec2ab5625785586ef9fbd4e", + "goalID": "G786aa0be57281d82ebe04aefdc20f4722bb6ace8e9bcb8768db4db03b1375005", "note": "(MAX-15) we're all having fun, right?" }, { - "goalID": "Gbeb7bbd19ed0eed65f8166d02a33091b8ad03228fc19ef560167efdf6a07f33c" + "goalID": "Gf0c07a8501880c2fe56826d3c8c852563851d40b47fde07c1aeff780a83cdc2b" } ] }, @@ -244,7 +244,7 @@ "desc": "", "goals": [ { - "goalID": "G2bb7ce1fc898f028e3a525ed15bb991718136ce27200c1f8a59ef959df239589", + "goalID": "G8eb9a981f9671033f078269ac98b382f46f844649a30cea54b3be26332fdfc47", "note": "Show mastery." } ] @@ -254,7 +254,7 @@ "desc": "", "goals": [ { - "goalID": "G9a06f0cc8758cc19dce70924849e633dededec6022aab385ced2b887d4cca597", + "goalID": "G0e1c4e8dc2bd01a3437ef728654b0edb63ad1b57f869e5cee651c1fe289b2b97", "note": "I plucked 30% out of thin air. Complain to me on discord." } ] @@ -273,24 +273,24 @@ "desc": "", "goals": [ { - "goalID": "G9930408e8aaadb119e8bc22a707aad05dbf362a60c7bd7e445b6886ff2831d3c" + "goalID": "G0195d9b1603ed74eaa038f5363cdd676eeb63b30a386746cb6b349aad6f09c96" }, { - "goalID": "Gcaf0a44d2509e511fa767beb1638b22a584ff1adcbc4940f1e598135fc27b736" + "goalID": "Gbd03b00f85711c94f00fea90089076f48a9cccda65f23bd3ed3f4fb35e01ec8c" }, { - "goalID": "G754bf911321ae722562c301aa00487fb19c0ae4271de27a9733a177ed63df5db" + "goalID": "G8e816f07a69b9ef1e0004ff1b0cba95fc41149a4b01c52f98f20f3f00ae07be3" }, { - "goalID": "G654f58332e9a852e4a9b5bcf77f8b68f722afc10580beca72b5b0c422c7b2ef1", + "goalID": "G6c8e79d4902b4cf743a8afe439b1a1e0abdb7f8f21eb5039c6a629532e60a906", "note": "Veni..." }, { - "goalID": "G89cec4ec11dd11288f01c807ce2ff0f52fdc75aac33ccdbf1f100f3f829620d2", + "goalID": "G57eb13343a058c69a5698d1ac367b92fd72ceb3da411e8dac0398a0768a5a4bb", "note": "...vidi..." }, { - "goalID": "Gad1b5c3e243dc1255cb3d862df58f116af45c5629863566118e3a2feb0869226", + "goalID": "G7f083af0e8e9947647a900cdda9c31d034508dbaae52c0409443a3b3ad21d59f", "note": "...vici." } ] @@ -300,35 +300,35 @@ "desc": "", "goals": [ { - "goalID": "G0d4820d485f9cffeee870edbbf8edd923936905e9ebcdc50a83d23de7ec6a159" + "goalID": "G3d1c87e9352934da393602ab72e09d975ead0e6967d11f55fedb62c45d5859f5" }, { - "goalID": "Ga35dd5e0aa06e9c44e4bbb1112619474756fbbd301a6e7c642b817c62ef0d455", + "goalID": "G83e9153d064fa975a7c189ce7e881fa9f086350f4499aff3f3358033075b75ec", "note": "habbening" }, { - "goalID": "Gdcad64d899954044c12389197968aea20d15b4fdef98c52d5ec23153c0b69c20" + "goalID": "Gd2a24516591b18582a3eff64136f2099cc177215a0e3efcedff01890733c0788" }, { - "goalID": "G625186895fb1153ee8920313ab4a5668038fcd9712fff32e6a50887be3e3dd7e", + "goalID": "G3663a80447cd30ab89384962e8e6b2b8c2ccbb4c7abd1c96b149bc8181c5eba2", "note": "le fast jacks have appeared" }, { - "goalID": "Ga6b497bfeb3e9f2255ab4805ffd921c34c0abedd52f93588035bd547ff6724e9" + "goalID": "G4c7474d686b61360150065ea71908db503e9052767e39c7551d7cc4ccdbd2552" }, { - "goalID": "G20789dacd98c01463a6ddee5e3cdfe39a093b2620413091e6420d63da9302473" + "goalID": "G324fc7b7a4f6b9c6c5600fc99efecd473ec3485a1dc8108be00b9682bf41bbf2" }, { - "goalID": "G2568289f555406026d89a0faa647c966f9ec9a5e97f3e73d099a6d3bce811a54", + "goalID": "G886a5a0c91eb4a0ad9402c05b12e8f6d3fc652d3effea71d3485cf50a7dbd8c6", "note": "And it wont be the last time you play this chart!" }, { - "goalID": "G82327ca3c65a1b4750ca3605caf85c7817412fd4f179c55a85a9aad43ddd13bd", + "goalID": "G3af77e883209462687918d0f436c1596d6e3fd6678841f6785e08361a75af46f", "note": "welp." }, { - "goalID": "Gda3e8b2f99d380aa0c60f8780dacf3981ad7a09324805a4d0f58867914f560e5" + "goalID": "G78340b6bc4ce95763f797550a2941a04f1bcfcb8df5d4091d1a9ea5c24a1f7e2" } ] }, @@ -337,7 +337,7 @@ "desc": "", "goals": [ { - "goalID": "G6a4ff51532d93b22dfd5c8cadd408e642f6cb7a6096eb6ed079d0e98ef238d5b", + "goalID": "Ga96793d89c4924914e2a6f306277badffacf0990af24b2b3674e96f466951615", "note": "To be honest, if you can hard clear the other things in this quest, this'll happen naturally." } ] @@ -347,11 +347,11 @@ "desc": "", "goals": [ { - "goalID": "Ge18b9d2a86bb342b414e24a6549cb1f34a6c000d679a932fb4b6568ccf4063dd", + "goalID": "G472d131ccef571f9cb2ee06e7261214f49bbc1e36a119e3c0cc366c98f23f500", "note": "Better get working on it!" }, { - "goalID": "G6a29e0bc11a1e356f7fa1328e8b9a91fcc389be160b26ed3b408800ae6a6fd13" + "goalID": "G09f78b3a6b607c39b2fca68a7a4054323bb750b2fe70dc85ab13c72bf3d75860" } ] } @@ -369,22 +369,22 @@ "desc": "", "goals": [ { - "goalID": "Gc0bd97f34ac7a4e3e3bcc829640f6526d5bf3f4d6003cbc09b6aea147d3de1b2" + "goalID": "G7ac29a0cb0c338d930ffa76835b20049d1a81f81f69c794d3dcac8dfc6a0141e" }, { - "goalID": "G5f5eff96326b85ed2bc803d9424618a6da5f656e456bb4b7734cde61bc49067b", + "goalID": "Ge7067c5fb15f67bac212093da736ffbd04aa13e2915ddbc4a08450d0d3ec4a97", "note": "EX-VENI..." }, { - "goalID": "G723468dab17f8e083411c797b7cbe51da53b27b782413797a1931e90afc5b4e1", + "goalID": "Gacb2b831d861daf8f646169e0836465c9d1b75c879f9cae86ef70e37ac4526c2", "note": "...EX-VIDI..." }, { - "goalID": "Gc08c0654afce1b73d2c02f4593c5e29acd7c0fceb40ccc151e396fb017d314eb", + "goalID": "G18170eb62353e8bb450fcc44df2cd763b64c2dd853d943e310cf372599b7055f", "note": "...EX-VICI." }, { - "goalID": "G3b9b888f049bf483fc17a7eb85f344e0e5c74397e949e6e501a7b17241769c20", + "goalID": "Gad8194c5f9050efcb8b12e150be83682769a61897dbd16d19851f6732951f88a", "note": "I'm skeptical of putting any FULL COMBO goals in quests, but this seemed thematically appropriate." } ] @@ -394,18 +394,18 @@ "desc": "", "goals": [ { - "goalID": "G38feae067106ecd440229bfb4c8bcc03ab927f237ddd6c698879238c8aef3835", + "goalID": "G5014da556b19342d379352a3befaff4d7271a65174dc91d8278c0c8295d2a429", "note": "Hope you're not tired of this chart!" }, { - "goalID": "Ge43b406e4e034ec24a6deb6c447be7ec6678a7a8cdd59132f6f3cbf95165d4c1" + "goalID": "G2ff00b659f0ddf8610d606d53b02fdc8df69ff639db5c246f6c80a525c458db7" }, { - "goalID": "Ge21176059a83eef7fa8f5fc98c329b1d6909395ccfafa15ae9e0e0120177be93", + "goalID": "Gd0616dd15eec949ccd49d727b22a5fd33352fdab418e8d464313b357aca7fa13", "note": "I hope you've been practicing!" }, { - "goalID": "G6a58f1cccd41586cd4b030644249932a622dca06c523f84184c9bbc0571941d9" + "goalID": "G12f2b73cd600478a0c16c0fc6b382005b3a933865f3d11de127e499de9bcfb60" } ] }, @@ -419,11 +419,11 @@ "desc": "", "goals": [ { - "goalID": "G0f5bd097383b05e7dace9ba2cc690094d02e13756fdba1da5b3c316e84440522", + "goalID": "G2b883ffdbe765eaaebfac6e83bc0081b9c997ca0e3d1035d02ddff07d7fef2e1", "note": "Leave no stone unturned." }, { - "goalID": "G880c284bcbb6ae7e6cf6482c613f2e045c989d52754e0e9dd726a114a572b452", + "goalID": "G61da2b79d0434ff044ceff76352d9ba9748775d1e8baf43124c8ecb6b1c8d502", "note": "(Ok, you can leave some stones unturned.)" } ] @@ -442,17 +442,17 @@ "desc": "", "goals": [ { - "goalID": "G6c6b2c4dd15d96f336878468a5461c178f1d9bf66340468976592433db9dff88" + "goalID": "G1c9806798380fd61a84e0b9c110c728cca54f643eadf620373f84cf042d95e36" }, { - "goalID": "G03e076f00f51a3bacd8d0e1a3eb349b62fa6cca1241281f12dfa2f8f42060a6f", + "goalID": "Gca3a6b342a136b9b6b7427ab653d76a516aadc1c9b0a7b079b98d5c04b984118", "note": "Hope you've been playing BMS!" }, { - "goalID": "Gf41ec8a91f708368f7a60f7d8b4d77690c100916430148fef0fd8d16a7c6b568" + "goalID": "Gf151ed2cde8b6da5011d963a3f4e593c6010fd92646a7fb1c6d6738c2920a277" }, { - "goalID": "Ge2f036b10ebc5c82731cc1d99ad55852fa67e6f5e5daf8f8e1f9f2f5ea1e5725", + "goalID": "G2df81066938bb2ff7d0e166f026ec253a27fa4e8adbfe55e030872213dbb0af9", "note": "This is stupid. There's not much else in the game at this point though. Have fun!" } ] @@ -462,17 +462,17 @@ "desc": "", "goals": [ { - "goalID": "Gca3459a14f5838332035d3be80ff6705d12845b6294a29e9b60722b4fe57f79d", + "goalID": "G63a7f069f8942865d6ddf7b51bd8c5e1bc051671c5e3b565a8b1544d46c73b79", "note": "Congratulations if you get this far. This is huge." }, { - "goalID": "Gfc705ce59372c3d66ce7c92da9c0ebe753f8892235143cd302f1d12951224b22" + "goalID": "Gb6469efe0ad1fe92949ed1ccfc26115a2620cdd7be6ee041e99faaf6b571b90b" }, { - "goalID": "G394186cda313937cb8cc70f2c47064bf9acf921b9cf31cf8260107ef32ba9add" + "goalID": "G71cca18c17f15bbb8920c269f4c989fd064b750a8e4748b1ad767116f38a6e77" }, { - "goalID": "Gf2943e13de2a701a0d83872c8d162287262400b647f43f4cab66bc37b16e3d03" + "goalID": "Gb07e04f91ccfc1eb1502800e1f54f7197829b7d1443838f5b7f2835d5d702abb" } ] }, @@ -486,10 +486,10 @@ "desc": "", "goals": [ { - "goalID": "Gb7f75c5e982075bfb653516f7e3e3a99958fb9083e48b7965b1d7a30405a7529" + "goalID": "Gfa235ed890bbc75bab98cd88bc07ebd83f65ce5b14ce3249af1fdf9d816be726" }, { - "goalID": "Ga26f4cafc715e6c6662abf79debc774938cc0b259b25d00397f98ec9499f94eb" + "goalID": "Ga9ed7c31b4262f6e5dfc58b3d00a8ccec11d0ba5e0b2e47c57b220919785b373" } ] } @@ -507,35 +507,35 @@ "desc": "", "goals": [ { - "goalID": "G7ca5885c32800c012cc897dc14abc2c8cac462cad7b1f4d2f88328a1f3392c35", + "goalID": "Ga0695b5a44b0fc387d0fd8e7289fecb6a2254231bee6c5624695e44192f77d23", "note": "BAD" }, { - "goalID": "G8bae0ae05088cc8506aff9900c34228c5427744ada392a4f7f9dd35c3037a1e8", + "goalID": "Gbbda7dfac9675109f47f73efe54520db3d16f51876fd8a86e29eac5ec39a534e", "note": "MEN" }, { - "goalID": "G181df38bf004505dcdde1ff8c9729f5fd39b8fa71161848c4d4d6ca6d727ec41", + "goalID": "G7265d66249d1d9bd54c1ae06b6c1936b984beef4a154ee137412bcf05d75b8e3", "note": "QUEL" }, { - "goalID": "Gd32662938d122fcbfa18d4f8390d2772859df69192c6b924f8012093ad6c3fec", + "goalID": "G063ef72be4cc1cb237dfa267d9f400b66cf337136c952daf4334df7ae7e41bfc", "note": "TEN" }, { - "goalID": "G42df127decb5fac1601a864f63044d3acd56cf95c4708c48dec4da8e83154447", + "goalID": "Ga56e3b9b26905c5a45da42bdd7080ba5341ad94d7a3a545b365fe17992cb2b65", "note": "HAE. Badmenqueltenhae!" }, { - "goalID": "G75d3d875541961b03f620caa0fdb03681680cdafe0298ff99c442499cd5fb378", + "goalID": "Gb8d3310edb53dd5f6a5d08da0f6e18e75ebca930698e39abe36a52662718f1b2", "note": "Don't lose focus for a second at the end. If you're good at scratching, you'll find the notes the hardest part of this chart!" }, { - "goalID": "G79c4f6884c285c67a3ba9d721622fa917201769d4ddfd0dd9bfb38472d5e10da", + "goalID": "Gb22d2a97955f9932a348b1596b6476ed6a7bdbc1d080b59a73cfd9fba6ebb7de", "note": "This is honestly more than enough scratch power to get through BSB." }, { - "goalID": "G7f6b492105cb229cdc5abbd6306d9537f8b0aac16c9f74db60b53a6785f9b604", + "goalID": "G8f28381abceaa26a9c3a76d3cfb7a630450da1dddb8cd82171f9a7b5173b8649", "note": "Huh? This is a secret trick: Play this chart until you can nail it **without floating**. Congrats, you're now good enough to get through Mei!" } ] @@ -545,25 +545,25 @@ "desc": "", "goals": [ { - "goalID": "Gcd004c1887aef0e7704da2ce50153b06706a6ae86a68f747bbfbd8257bf201eb", + "goalID": "G254bc6d16c8ed54da6d7ad88903a46b8500d24049537901e070055347379986d", "note": "It's the easiest 12 to AAA. Good luck!" }, { - "goalID": "Ga0df6083017e36eab6bb9ebde9b26776bb85faffd728b1c54a3b205cabbac1b5" + "goalID": "G4b25b28d4afdfa58fee55c84a2963e1886886ae515ccab6274cd66a601b55c24" }, { - "goalID": "G55dadbcb465cc58b4c949826ca0436e808c259e909a1108143a4710e5b8b352d", + "goalID": "G7c5055482429522f7482e7bdb97036d9a2c7e93f294ad4b39660d8e971884c29", "note": "Watch out for the subtle BPM change!" }, { - "goalID": "G08fd73ecdd710f045816430e5cbbe1b70d2053e466cf3153b4564993b36d8ba9" + "goalID": "G60b4be3a6aca95dd43fa1bf62215deabba42f7a40ab3e3eef78916705d0a38ea" }, { - "goalID": "Gc567056c12238d066060d988a8743ec2a4e9c1d09ca44d6dadd08be8ab603f13", + "goalID": "G332b29e41e4069bf16d5b4718408c8a8fe446aff4492c26b7ab4389a7df4286d", "note": "warning: song slaps" }, { - "goalID": "Gf53be8d6288499987c8136e0d96676662d57c929a1820303f47a18f1d9977ef6" + "goalID": "G89dd2e38a992365ad2f80eb39fee7e686cb13c3a9a1343b266913fa63afa5a79" } ] }, @@ -572,7 +572,7 @@ "desc": "", "goals": [ { - "goalID": "G2455689e65bc890fe6b768786ac26bc1ecc010b7103888019a131b6aaa9a6828", + "goalID": "G86ef585d023d76a34105a87a29532201fa5cf0977e5b1bb7c4cdbbb2bc0d7d02", "note": "Not as hard as it sounds. This should encorage you to play a wide variety of stuff!" } ] @@ -582,10 +582,10 @@ "desc": "", "goals": [ { - "goalID": "G2273a629e2c47d4e4a05cd86bb2dd07020945dfd1bb0c2b6e62eb3e319d67280" + "goalID": "G45fecf25975b96b93ce9c81fac3fccac618516935bf278c025e5707312de92f2" }, { - "goalID": "G653396c9ea159a062a7f0f7455cbf13a100fdaf48b6aef95e7ba98f1f4e01c3f", + "goalID": "Gb19e2023305b87213154c3a28e8c858141cf3fa113b6a15815b5d2b12009938d", "note": "This isn't as hard as it sounds. Find some charts you're good at in the folder and focus hard on timing in your warmups!" } ] @@ -604,23 +604,23 @@ "desc": "", "goals": [ { - "goalID": "G5852c506437c45c321c465e787a7226ce6f018f73f7299919264c357ef70fe82", + "goalID": "G29a70b3f17eae4f3b8256d0d832470d278a48147e839d4238e8a082193a96502", "note": "Won't be the last time you see this chart. The intro is the hardest bit! Power through it." }, { - "goalID": "Gc501abbcdb1ffbfcadf48e69a49ed5c8822e5804a016223acd727f24aff037bc", + "goalID": "Gcf40d565e86bf83a3eef2a7cb82deccf0addefa7221e7d9d4ea81d0d26da3d8d", "note": "Don't get filtered by the jacks." }, { - "goalID": "Gaab422b2ec34f6a3140193a0bdf62e7228d28a62461b1894de6a224a22e7fed7", + "goalID": "Gbabb92af4e18584e9641f97cc7bd63ebadffa8ca1fe410f56bc8cee0b8f308bf", "note": "go fast. go hard." }, { - "goalID": "Gd4d221f92419a6d4db84679512e9484023dc0c77dc911fb0af5332d4049895ff", + "goalID": "Gcd568e1f0bc9ed6c4c1e2736f7cfda2eef649931562bfbd79b4e40c07c8ecd4b", "note": "Make sure your scratching is up to... scratch. Heh." }, { - "goalID": "G7e4e5698240f19602fc4775b101507f1008d6726e7d53b77699d3d41201f90d0", + "goalID": "Gfaf0ebec02f07c36c9f09efe37acd925cefcebbf092ccc3b40a23bf3b072835f", "note": "it's worth getting good at janky stuff. it only gets more janky from here..." } ] @@ -630,17 +630,17 @@ "desc": "", "goals": [ { - "goalID": "G2267969a8bf2299a24209062578dae910a343d8aa070b815aecd9cb0fb1da8e3" + "goalID": "Gea588214e18bc3879ee62164eb06c7356230f35be3ae75a71d83d01645532628" }, { - "goalID": "G80d42b930287a1ef469f86d18d062661426b46e1f09c5591fd5380b93ae4f7b0" + "goalID": "G9973d2b6a16c70d30b0d302ace1ac6d523a4d6d7d1847134dc3c67d3fde218e7" }, { - "goalID": "G161dbccdf18052986d896dfc5323f038c1abb36f2e764ed46d92c2e90b3106a6", + "goalID": "Gfbbd23ea00d0c1e07e787e7f9e8a969bb1d3e50016037e7f831abcc8a76051d0", "note": "You might prefer nonran for this." }, { - "goalID": "G096952757cb1363652f1c8d7c6a4d31f8ca0ae627f7e41568614a0bb8d283fe7" + "goalID": "G7a1c16f3d0dfac597511a5ef7f102b8f05f1859aa5bb347ee980e6ffe9124153" } ] }, @@ -649,7 +649,7 @@ "desc": "", "goals": [ { - "goalID": "Ge99fd25f71e64d91771546fa9dda6b5b59f2746d2259398f34d1f3c63c9ec721", + "goalID": "Gb372c466966afbf3ebf70c5d3b701dab56fd376965797a6864ed163b20d2868a", "note": "Use the tierlist to pluck out the easier ones. You'll be pushing 12C's. Make sure to play a variety of stuff!" } ] @@ -659,11 +659,11 @@ "desc": "", "goals": [ { - "goalID": "G1a51c71951ad0fc9f1e7eaaa32ff6a6c25f3659faa0f0b63e1440590352fa850", + "goalID": "Gcaf664e666e9d567941bddc60734e270cc273456074b66fa81921f0718419584", "note": "There's a bunch of easy contenders for this, but you'll need to build up the ability to time 16ths well. Good luck!" }, { - "goalID": "G65f1fb8f98fb1f8da4c19cdecf4b33cae36769e9354c825231e1738e3c27eb60" + "goalID": "G24421c36c33c601a8ec08fbc4de2713ff7fdbdf14e088fe0a6c4cd640e544d79" } ] } @@ -681,23 +681,23 @@ "desc": "", "goals": [ { - "goalID": "G5b4244720155901e8b3aa8c2143c561d88f8fce9fa80cf619ae5111aacc20694" + "goalID": "G2788eba138e2919d69fc1c78da2af3d03d0dd5923bc816adcd13d977b9a07b51" }, { - "goalID": "Geb56e070e7fe0fbf39443b4e86c96544b6e9ffb1a055270bcfe4152b5a8b0e89" + "goalID": "G16b48da8eaa90d33762ebdd9157f318a6796c18233c2b7342fd60342c19f5a84" }, { - "goalID": "Ge89c922fa298f38f8a478219be94871f084206c3f6e49b9a8244c27feae5c5f3" + "goalID": "G89e418fe4063bd4ec408ab6f14fc2adeb8c6b083b78c035e86baa7fb08c49be5" }, { - "goalID": "G8f8c8fb87a12f4074370ca73e1f168d5d9d2c4e5ca3a2b4d7fc4a6c5e80fb82a" + "goalID": "Gf6b5d259f395e82c4a0253318916d1ba879518018e59db87bd074794a1b4f744" }, { - "goalID": "G1cc8b945a0c578e781694a198d70a4967367868e959b0fcee177700cf2942ab7", + "goalID": "G7036ef4e689adc7193a8e8eb21e35fee512ee1bf77f936a902e703da6039602e", "note": "(ok, let's go)" }, { - "goalID": "G9a070f7500b05c13f92c14d8cb646012633394110e10de7cb263dbd721b66b18", + "goalID": "G167348b8b1dd68884dd7ec4c7889fff8f71ff8d96aece8aac2de550acadab729", "note": "The stream is brutal. Hold yourself through it and recover afterwards." } ] @@ -707,16 +707,16 @@ "desc": "", "goals": [ { - "goalID": "Gd8f31713af340aa31482fbce46a07af342004e310b744c6e5b516d7385a57502" + "goalID": "G809602ca08ef16ac69cbb376195cf646e9a259905283e123c9dff879c6c13594" }, { - "goalID": "Gf431ae7eb6403be496d0d3bcee22bbcc503bf48e721e8e5a6eb8dcc80efbd08c" + "goalID": "Gf8ae63ad9fb57853c7431ea5a8ba962da3e2bd0d29df547fa9239642acd1aab8" }, { - "goalID": "G378188b4d0c49bbfa1284d1155ae558dca50abb5a245be1c37c5209ede57085f" + "goalID": "G03e0b772b28192e1503552d94e7ca9785d79e74f6682d922b3d8e550723c095c" }, { - "goalID": "G1c238bc8ca9b8f89e1eab6789e5a60f5624573a9b68e121044d50344cb61fe42", + "goalID": "G712841f80690c78026f34dcf8ed885545168bcb065afa9987038c577c703fe0f", "note": "Hey, it's not 8ths!" } ] @@ -726,11 +726,11 @@ "desc": "", "goals": [ { - "goalID": "Gbc23d96a89094c2c3bb6366096f6fcdca45b30ecb55192e2229e4fddd0310faa", + "goalID": "G46b5ce2a764563eb431d3270836e49fd911c64cd502495d10bf2da1bba4dc01c", "note": "Use the tierlist to find the easier charts. Make sure you're playing a wide variety of stuff!" }, { - "goalID": "G33a9358057928f0ffb8b5691315eaedc74b903fa717158d0fe077eb3eca3e517", + "goalID": "G04446d20628c449092f4e5a74c5416315fde66b6fa9e9977f6d074619451f6bd", "note": "Keep the HARD CLEARs up, they're useful for training your consistency!" } ] @@ -740,7 +740,7 @@ "desc": "", "goals": [ { - "goalID": "G6a1d19018159d80457f7045b7908ba2a32ab4889bac33153be140967f7cedcc7" + "goalID": "G911d105e08b4c74f2ddb698efbb952b51753cb7dba4e35ad512e6104fd1064b6" } ] } @@ -758,29 +758,29 @@ "desc": "", "goals": [ { - "goalID": "G2db28d9a6929d00d781cb8134d17f3c7b683ecd6bc389984240fce4dc0475403", + "goalID": "Gab1c4a139570cfbee48db6c96f66cd39486d99911771340f11e05533aa4b3287", "note": "It's time to really get good at chordstreams." }, { - "goalID": "Gf3b0dfb58f116c1c7134db24401590403234c478a10c076758543f29b2c235bb", + "goalID": "G3b2d3447cae04f856cb6c51a4d924aeabefc2034c1dd8a823bd134c04cc3b9db", "note": "The scratches are much, much slower than you think." }, { - "goalID": "G1e2eda29c30949f974cb30a042fe5dc78551d53a76ded24fc46cc4b73845259c", + "goalID": "G51eae5902c3a14284b515cf85001a6e94cc195f8b408865bd808074a11b0f947", "note": "Arguably the least infuriating \"easy 12\". No gimmicks, no nonsense, just density." }, { - "goalID": "G7f67f44a0fd3eea6c22c83398ebcafd71136248f0911fb8f790a6d4c837b01aa" + "goalID": "Gc275e614ee2303f13e217d7883a35ff4d3507a96f58db9e701023596c91d9a69" }, { - "goalID": "G0ae78134b5a4c3884353d6d5900dcfd12f8d53695a7ce9691f9327edc86cc289" + "goalID": "G84b57d80510556b7aa49b3c01d76d4e4c29521d70c34fa97647a37676ea9362a" }, { - "goalID": "Ge0417bddb65b1021a308a19495d824130c6d0b1ca23e606249f316bb21f2a43c", + "goalID": "Gf5cc6c22777f61ca37c672458970ffae60a6ffb238343fb0b22a838bba83b9c2", "note": "Pick up one of these brutal 11s on your way to pushing early 12s." }, { - "goalID": "G47c8f8f25c445a4558206e8dfbd429068349dff7abdd0ed482c7d3900d8e6f59" + "goalID": "G15ae6ddaa4b63de46dd2371010df7864752996f8e904906faae0feb72c511d1a" } ] }, @@ -789,13 +789,13 @@ "desc": "", "goals": [ { - "goalID": "Ga1bc389b21e3012240f33f493de6db7efd1389a372b67f6fbe9856c400d29f0f" + "goalID": "G0d44be3bae50f7554f0e696e39b726926f6bb750fd49426c848c84ed9bcbbaad" }, { - "goalID": "G00d676101f4850abd718e1fe908539d4f91ad4ea58f73af7b9cbc52544e74684" + "goalID": "G2a931695fbcb409c4e88590d1af4887a4b41c5787c625a45b2ca7b3a672fd034" }, { - "goalID": "G22531dc83f001d7bc2f73a28b7a3d71f2835b174a2eba151393319b710b375ec" + "goalID": "G00122bef622fadcfd27c3ae3472bc8f5a929a64ad5dd62f7dd7e6c67e595766f" } ] }, @@ -804,7 +804,7 @@ "desc": "", "goals": [ { - "goalID": "G873e92ff8bb8352eef55b97e11329e216bd2a037145e0385147b094bd592da82", + "goalID": "G39e38988b5fff299596322caf2d7e522d5af8c373560b3bd592fa58992a67a58", "note": "Use the tierlist to break into 12s and pick up the (relatively) easier ones!" } ] @@ -814,7 +814,7 @@ "desc": "", "goals": [ { - "goalID": "G5bc76bd2a019f34bef39f4a4cf731c1d726a82abb47b75834f5e7ce200af93a2", + "goalID": "Ge1e31f0d0cbb42a4ade9ef4955f2e24527fdd26f43eac8d5be6f8df7b897d8f0", "note": "Pick up some AAAs to work on your timing. Some 9s are far easier to time than others, so try and snag those!" } ] @@ -833,24 +833,24 @@ "desc": "", "goals": [ { - "goalID": "Gac4eda4ba8dfc2158542ae76bd49fbbbdea750f4332338cd330c1f4c8bc1a84b", + "goalID": "G1d5ca7dbac64333501d2addcdc5204b8f67ff97d8816b31148de032b3f88143b", "note": "Your timing journey starts here. Something easy seems like a nice start!" }, { - "goalID": "G25943e5bfe8824d0f67c282728f9cd3650666519e71aacf8a4273cfd5d08b8de" + "goalID": "Ga5f928c598496bb359148b3a369b2c39803a9e65829150f0719b5de89856350e" }, { - "goalID": "G3c37a44164e6ae1115dd1ccbd67a54d4131a15b228e2afe0d9f271894d8629ef", + "goalID": "Gf9361811b028db747f42efac21279b2309cc60b6c4928c2995f7d5dd44d003b1", "note": "Mastering the basics is important! Don't underestimate the importance of good fundamentals." }, { - "goalID": "Geb61eb5bc6b35cad82582243c0cf6879dee5a1743c78645ca625b6de31ca4f6a" + "goalID": "G37a14b574a6fa69869b81853cd18acc23ca6cc8e5a0e66ba52bc50d2adb37bb4" }, { - "goalID": "G28366badbe5c87fa61a5025a58cdf8f52f14b3330b19083567de401115f9c72c" + "goalID": "G7fe33015d02f9b19626121a96480e12cbe06905f3a33252b64ec9927857050e3" }, { - "goalID": "G8f9f1a73b569a99206b8a99461d713dec97720bd049df1f8e433632993633001" + "goalID": "G26c265e7188e5a1536a818eb38fda52eaee3bfd7e71b3e1f029e0262ea6a782c" } ] }, @@ -859,19 +859,19 @@ "desc": "", "goals": [ { - "goalID": "Gd837975332349aff6f091232ebe8e4275096bfd5e8866eb96f8f7988163d5ff6", + "goalID": "G94c2a3d6e936cf946d7b870caaed917eb9a00d644f68ac514387a13a1356b2c1", "note": "Use ARS!" }, { - "goalID": "G1fa2d8289b79aaf5a3067df209f175eedc834d18b34ed43d3e62978fa6302e32", + "goalID": "G909ef14ed7b214a25332693fe623ae65e2e0468f91def016d7a77f8d5779af46", "note": "This isn't actually that hard. It's more to make sure you're playing a wide variety of stuff. Don't get tunnel vision!" }, { - "goalID": "G1f62ddb6de93461446da5898c67392c8030f0c6bad3a11ba0433565230f9fac6", + "goalID": "G604f3063c34494062b11e181543cf881d408b67311695f750927b3f907e75cf5", "note": "Use the tierlist to find the easier ones!" }, { - "goalID": "Gb4c1a4e8e178dbf99d0ed3f5c1a59fc83d88f93a6ab0b4567dc4e70db837b2c6", + "goalID": "Gd95a09ddc1ce7d3d139c7c622883d58c5541127c94d3fea1c3828f912ca412f0", "note": "These are the 8th dan boss songs. Good luck!" } ] @@ -890,25 +890,25 @@ "desc": "", "goals": [ { - "goalID": "G87456157681bafef37d52d99370088e547d5e9ad55fd45369e12dd2bd3895a3c", + "goalID": "G6f51d83a87ea6468d36357d2fbebad8248a1db01503d409f7bced22a17d21199", "note": "The first UC goal. Don't hesitate to try new 13s - don't get stuck on the ones you can't do now." }, { - "goalID": "G488d565d68431ce46422f085f04c04e80366b57a64a025e8914ff8d0297ee382" + "goalID": "G16448646d8f0ee92742e50607460ed9ab36ce77078f83336592c060360326d59" }, { - "goalID": "G26df4f768894142b4161e7b4b362a6db09bccae563dc106e0de662c10924c88f", + "goalID": "G39a50e57d5a90612ce18d55ad410299f854a5d88c439582fd51facdbf9d338f1", "note": "Mastering the basics is important! Don't underestimate the importance of good fundamentals." }, { - "goalID": "G66acf9104f38a848ab38caeb409c7ccecccea02a6b7c1453d5d4ef4caaa95b35", + "goalID": "Gcfebd2e376a3480b9e6c033f117469b765582902c36339422ad9a9898402ee1f", "note": "First laser test!" }, { - "goalID": "Gd20bb734f7924f085951fae9e2c019d9e63aa010e617ae92f81b9249e56bc172" + "goalID": "Gcf2e16e52eeb5b2c5af03c96e37381eb1a2ca7ed1e5941fafbc297a5553381b0" }, { - "goalID": "Gfe5cb6e3f57cf27279b3f79c99791fd5c30acb5bea982f34a99ae9c9dd7f5105" + "goalID": "G60ea64a9d5c8db898668210740f06c79924b37229dca4bda9c6908b2f958bfc6" } ] }, @@ -917,18 +917,18 @@ "desc": "", "goals": [ { - "goalID": "Ge0dbddf1ca9773ebaf2ee8d1bfe42b372eb6b3094c2de1b0275766d326daa706", + "goalID": "Gfe519c3456dff488fd95160e05e82566c09f994ef97daea3342ac39b7e5d73ff", "note": "This isn't actually that hard. It's more to make sure you're playing a wide variety of stuff. Don't get tunnel vision!" }, { - "goalID": "G4670b732730d59913fab058592f9c12e1d1c1885f5342faae6e64aba0c6e855f" + "goalID": "G60e9c01536ddd674ae7da090f6fed232d8de0358a0d253949d780133d12e5bf8" }, { - "goalID": "Gd5be9b5c9a426041a9ca79acd1fa0f371b0e524898e6ffe5f0321b5f55fbc67e", + "goalID": "Ga6d0023afac9a643d7bfa255e678433ff5d320d4b3de803df57e1385e97a781b", "note": "Use the tierlist to find the easier ones!" }, { - "goalID": "Gd6850c9703150082ab42078509f94a098ceafb612b6ce4ca0db8e94b357ff278", + "goalID": "G93ee06f15bec2deed7fb4c59221da49da778147972ebc070d3602f8beb78c13c", "note": "These are the 9th dan boss songs. Good luck!" } ] @@ -947,24 +947,24 @@ "desc": "", "goals": [ { - "goalID": "G4c2683550000efb9ceea5b1b6f4d67444f2487db1d3d38bcea144f98ebb22f2b" + "goalID": "G2753016668b125fa4c054f8c5044e59ebee545a1cb4aa50f9a95af5e2afd600d" }, { - "goalID": "G9113ca9a758f555f24c8ba3037da9a052562b3d686e6303027206572389811e1" + "goalID": "Ga70c91a1d0c0240cfb3c0b3628b4b00e4250208335e09d2efd04cd137325d789" }, { - "goalID": "G28c623459d09d203b4220d0419835925a486ff24b553c70910b34ce87d1e34c6", + "goalID": "G19bb3009f3b3af570179c740657bcd8ce468e0ae1a0cfddf138c31331657bdbc", "note": "Mastering the basics is important! Don't underestimate the importance of good fundamentals." }, { - "goalID": "Gd65e62814382a3e165c32cb2be3c66e4d6a9428fa9a4970e933ac0a580afc585", + "goalID": "G9f3ef95935d9c7f41602019787938467d1fce5294f7c91850fe3d5840ee13fce", "note": "Those are the hardest 16s in the game, you'll need to have a well-rounded skillset to tackle these!" }, { - "goalID": "Ge8ea38a06a88f3bf81f8f59bf39290b53f8d8a62e315dc957eaec03b7b8df3a4" + "goalID": "Gf5463e029bcc05da520e5cd0a7964c2cbdc36618c46506212fd5488c22679391" }, { - "goalID": "G8145fdbea66519513f3aa8f47c191d8029d522f08e4c1b14a1694686373fb781", + "goalID": "G9c1ef5e70be5684ec6d0267e0fc0c526ed9d095338d7d9832ba4fa225daf12d1", "note": "Fairly hard 17s." } ] @@ -974,17 +974,17 @@ "desc": "", "goals": [ { - "goalID": "G68fecfe183d3d874fd24f1c1f516854d79d643df0e6fbefb64b50d1ddffb305b" + "goalID": "G7a12b47023c873d34f89671f2dd3d967af1fbc190ca6565d3a9d4e47a764372f" }, { - "goalID": "G7998550b0215cb2dd7a7fd39de62577972c95b69e127a044362ae2d09157590e" + "goalID": "G4f2f1e9c58fe75fc1d4adb980abbd4ccef0c8e2a458eebd2dfe5c61e402a1ba4" }, { - "goalID": "G39ecef606cbc725421f980588673387079ee68fcbad3f0284a65abef8cd1f867", + "goalID": "G7107e4419481cd400f06fe820457cfcc57868665d07f987ef19b129432a7ebf2", "note": "Use the tierlist. There's quite a couple of easier ones!" }, { - "goalID": "Gd0acd0dfd8f9f91ae9b6aa7734fb726c0a8e83e67d7771d8c72d64311708e750", + "goalID": "G6888d8c1566ce138427dfdd51ff1e7ebe5a7272b1ebf5ef88c119e0565f2d69c", "note": "These are the 10th dan boss songs. Good luck!" } ] @@ -1003,28 +1003,28 @@ "desc": "", "goals": [ { - "goalID": "Gc3663f8188539baf2fc8dc21b5be903a9086ca25367b8311db9f7bf0f834f761" + "goalID": "G943e04101cb28a01426725ec45387a3a9de9bdbb999acaa0478f2e482c4da76b" }, { - "goalID": "G5a250fcecae6a6b81897f1995aa49a885def0f49e612d0002d401d5f522fd38d" + "goalID": "Ge8bef7d934fe71987f467f45f1c7d335062cc66470775c67f85fde23e8eea88a" }, { - "goalID": "G7a2666b1e2568598202eccdcf1de4fdcfe40812f4a33e9d8ca4e2073e5a7e864", + "goalID": "G202700527e32bf7de07b254e7b5c4a3629e8d15c428a5a1f22d2b20b21232a63", "note": "We're getting up in difficulty, but there's still more patterns that you should learn to do mindlessly. Don't underestimate the importance of fundamentals." }, { - "goalID": "G23c50bcf3db50731d51d3e0c33d2572c83fdbcc12350d9e11190d3566fae9032", + "goalID": "G247303e9e1b7a1c4863c6024ec6e9bba3e8da377cf8ea44b925b296c98594d7e", "note": "Second laser test! It's much harder this time." }, { - "goalID": "G3c62ea2e1321ac2629743faa53c42016a006c714881cd99ec0437919285ed5d5" + "goalID": "G09ee2358ba001fc10bf09472c2f4b1edcf4a512c9a77661baf3cd37464ec0584" }, { - "goalID": "G6407753fe57a62da297fde4b8c638ccc14211e2288c1af3fb575784e4e88d154", + "goalID": "G56caf3712a9a9805e1ee8eb7fe86673b64314baad10bbfc0e8c86b679051b444", "note": "These are some of the easiest 18s." }, { - "goalID": "Gcdc3cf2ac89b63f88e687f718102c99ecb24769ea33e97ed4642b0e8380af63d", + "goalID": "G698f0bd2b5a3859478a49898f499bad6c84ec7989cec5822b5390ae33fcca96f", "note": "Play a wide range of stuff!" } ] @@ -1034,14 +1034,14 @@ "desc": "", "goals": [ { - "goalID": "Ge7541ba6c58aaf29c0e66f1b2709ffb88f558d932caeeccf2e4cd1af5f94d087" + "goalID": "G65278d2891251faf37d35823634a41b75f05843239b9ec80961e1ffc99f023f2" }, { - "goalID": "G722a7884a3552c4e449b11c83ee0883e8e0d3e246db5c53ea1814cc295784c91", + "goalID": "Ge7914262f1dabad65f3c12a74f2fe9dc72e622e35bac5a5ab7da601907687c8f", "note": "Clear a wide range of stuff too." }, { - "goalID": "G36a064626e4138a85f72f40a610fa4b320a8627d7b143043f226bbff50e8d457", + "goalID": "G9096ca58f8aa719866a945404c0a03fdfb2ab9c6c0cd5628e8e82cfa22ca6162", "note": "Solidify your ability to clear 18s by breaking in a couple more of them. Use the tierlist to find some easier ones." } ] @@ -1060,28 +1060,28 @@ "desc": "", "goals": [ { - "goalID": "G82d70fe20b48a11d416a61ea462a0b8cc1bd6eb4a9b3eb545a30263cafab28a7" + "goalID": "G780126a57153930857d9bb1e3a8aa071f60fe58bf3dbf11a49033b0f249fb483" }, { - "goalID": "G34ce6ded60134f35b48b4b9fb7f32e0757f9b2c254ba14b7898f93a49c03433d" + "goalID": "G475ca03a09c5035de96d162dbae307e016cb16f14d7215817b5fbf613c88ae35" }, { - "goalID": "G8abfe67c7680a61f6c1d1e98906475f7beacabe66e31f5845456bb7fbd13f0bc", + "goalID": "G2f368ae70e177729fbbe2b809198761ee4e374e8d64ecda15495df7a40e02cf2", "note": "We're getting up in difficulty, but there's still more patterns that you should learn to do mindlessly. Don't underestimate the importance of fundamentals." }, { - "goalID": "G8cd2cfb82e8a476a66c7a8c3f6b60f7958a8acf2a4120181fbda9f59db564e39", + "goalID": "G130f6090577b551ba3a65bb2da44e5807d407c41f9dac35aa4ed259e0eb21d8f", "note": "If you can AAA these, the 17 folder is pretty much yours now." }, { - "goalID": "G67afe3fcc2835cf40b85cf1253a352e9555d1ac66336d6482581069dc032232d" + "goalID": "Gc0447d29f102fc5bec6236e7cc2652568c0c32e01fcfb5f1fbc78d204b91b0d0" }, { - "goalID": "Gcd4e04fc14af725f3d1ea3671e23adc4a5c536d6fe9a7e9f6cbd87483f028fc7", + "goalID": "Gf1dbcfa03d5e4496dea64d2093673b70762d6d66a3c5cc19cb329bd34d89841e", "note": "Hard 18s. Good luck." }, { - "goalID": "G0b172f023b29fa35dc5c18dd4a764435b4877636c1d99e2960e91795ca83dd80", + "goalID": "G8980504c85d097d4b41a7a803ae476a050d2f82d00cb8c648feee4abc228d64c", "note": "Some lower 19s to get the ball rolling." } ] @@ -1091,18 +1091,18 @@ "desc": "", "goals": [ { - "goalID": "G3ce2fa38830ea34a4c106e80c57c082fb71e6a0f13570e3877d8d116c325de20", + "goalID": "G1ba1112c67f42f72661953a758f9d0fa41798e73337a2e42851bef711e489f92", "note": "This isn't actually that hard. It's more to make sure you're playing a wide variety of stuff. Don't get tunnel vision!" }, { - "goalID": "G37eedc8030f77487c846f6c0b47e51e420501991ea5b2a6bcf1b2c7204f203c0" + "goalID": "G0f51e650348ed098980ff3ef3a09392e01cc66279959e3e6dde3cc502ead527c" }, { - "goalID": "Gbb19cb56b35c8e0c75234944c7b3499a68cd0db804b9812079d2f4b8e4a2ebab", + "goalID": "G1a79132331135eca1bb77173e9ce844490b4624a7ecc5d7ffae02e40f4378404", "note": "You should know by now, but use the tierlist to pluck out the easier ones." }, { - "goalID": "G3d3c2ca6e7e7ca8c13b6abf0fba5f35492a2e6e0ea3918b4370fef8575e014dd", + "goalID": "Ge06603c39c07860b82c2d49cfe4303c386e8bd88cfafaf991932963112fbdf39", "note": "These are the 11th dan boss songs. Good luck!" } ] @@ -1121,28 +1121,28 @@ "desc": "", "goals": [ { - "goalID": "Ge9b0dffe433395244dde749564ada73c29933739e8207d257bbc94c35642a83e" + "goalID": "G3bf0295c50cf8431f0d2c6aa13595d7dc4dcdf0738ed4288453a9047c3f1fe86" }, { - "goalID": "G34ce6ded60134f35b48b4b9fb7f32e0757f9b2c254ba14b7898f93a49c03433d", + "goalID": "G475ca03a09c5035de96d162dbae307e016cb16f14d7215817b5fbf613c88ae35", "note": "S is for Superb!" }, { - "goalID": "Gd11be9670bd13f96a70dab4ca5f60d0d929b9cb26dd35f9c2775e539f68ab491", + "goalID": "G044dccb6b25fc04451e9100bbfaa3b634dd3556c161f7b4a6c1da40d502913d6", "note": "You've been playing lasers, right?" }, { - "goalID": "G685db835351b0918d0e71aefc4b10cbfac7a9e7e23294b99423eac41ce63df08" + "goalID": "G0a1437544cfd3bc60d98e11db00ec4aef55df24454e63fa986d6de3cbd9c02f7" }, { - "goalID": "Gd04e7f4fdee9c4173dee2b1a40111f88617c0d6cbffa4880c26130e1c7969b80" + "goalID": "Gfbf6843a90e7657e22782f9cee31c2e6448047a4afae1432d7dffc6b7c1f8305" }, { - "goalID": "G3c76b02aba28d133f4df7d4ae457d990f73a5e9d85cb1390d7b86ef4e6134151", + "goalID": "G1bd1da343629dd0fa15bcfe24cc5cc0e67f0d67b255c9528b0428e47be9813ad", "note": "Some mid-high 18s." }, { - "goalID": "G31b1b568376afa7187b1ee3aa89eeed0b8d0daa42cff051679511bf6856e6101", + "goalID": "G0515f7a6c498cc4ff2712a4178010910de0454c135b1fd0ea9710a96b019966a", "note": "Geting an AA+ on an 18 should be easy by now... right? Right??" } ] @@ -1152,18 +1152,18 @@ "desc": "", "goals": [ { - "goalID": "Gb722c2d8c46d04ad1d0d4147b92f42b2dfba8cb5cd0160fbe1424b2f93af81b2", + "goalID": "Ge89577e221b6a55ff840aebd8f9ba403f6e219f9d034ea835e978292ec1cb479", "note": "This isn't actually that hard. It's more to make sure you're playing a wide variety of stuff. Don't get tunnel vision!" }, { - "goalID": "Ge120b5fd900dbf3d2bd80e08dc8abd58709ea5d9ca97c7a0bf3e916a02a1cfe2" + "goalID": "G8924c90d4678ac3d0495f6ad67ae4ea07c752a5a6d28b2629f26b8d98dbd9e5c" }, { - "goalID": "G6783d25ac9d92c0bc1927fe553c50cda7bb878748df6275f4a983e67ee985248", + "goalID": "G607d0a0684e41637139fef8ff230f41bb909942c9a94f57f7d0485a0628264ce", "note": "The first of many..." }, { - "goalID": "G984819589a24c33cf390855b5ce944ac5505632b912fc30f6abbeebdb08ecb1d", + "goalID": "G72bbfc671c0f211355085393012b9835ac65d8491ff2431f878626f89df7eb07", "note": "Welp." } ] @@ -1182,26 +1182,26 @@ "desc": "", "goals": [ { - "goalID": "G446cf9f06120cf2bbbdcb991c949d15943b1b4324d3d3d20e478a07efa9d0f88" + "goalID": "G90bcbf690a855cc9be365f22c9fcfe47e1e73b158a2cc2ea228e14452825f354" }, { - "goalID": "Ga18826414797270c316fd9b3838e73214ab45478c317590fb3cd9d9a0953aff0", + "goalID": "G6358c8004675bb53ae6bde30a8cd93735aa8fd123438cd93256d70fbd6f29cc6", "note": "S is for Stupendous!" }, { - "goalID": "G4a4220694788149fc08096febe84ddc584b26b6c9ab40c4b0a46bd2acb897b20" + "goalID": "Gfb2a810bd50e3d3813a6fbd939119c340bf250cfa9843b65f61bdc8301b74ef9" }, { - "goalID": "G77a7f616a426fd5d305ab73ed4f4cbad6c22357a9bf20d0fea763e9d2c0ca2fd" + "goalID": "G4a159b7973fabd82ea7e51bc96d58fc94df7fe15077769d711a155445b9eff63" }, { - "goalID": "G380901c2d644407924611992a93ba2d23709366d261e73d671561589c4657df7" + "goalID": "G259ab31e08b751f67b3fc496de2c4ae262274962ec5589e2e4a2880b70d96857" }, { - "goalID": "Gbcae3cba420afc06b123e43ba6c2305d14972e0f75ce87bf97dd09fc9c7381bd" + "goalID": "G899d13fe5225f7fca4cc5a3ad77afe02ef7f03a874d3ee2ac5a1a177b9240acf" }, { - "goalID": "G53b10706ce6041409c4921177d36d0d700e8c9366efe7ad8e34660b07df1087b" + "goalID": "G9540d88dcc938ebb0ef124b5f031eadc0966c826023e9719c59e349487cee00d" } ] }, @@ -1210,18 +1210,18 @@ "desc": "", "goals": [ { - "goalID": "G1e61ca0b8b1941e8fa72261d8f08285fcdd919f8438017a46bcbbb059961910c", + "goalID": "G1f9940b18b32059b73851cfd2d813ed952f6ae55a917fba2f2318091d5323df9", "note": "This is the very end of the 18 folder, you'll need to train every part of your skillset to get those 5 songs." }, { - "goalID": "Gd39322849e696f84aeb5f0a80ec4420ee56c6ef7c05fd430c436d2fb7ea2bbdd", + "goalID": "G00b9df1cc7183235c56613655d77b35df8a9716a4ff456b86629269fa579a1a4", "note": "Tip: do this and the above goal at the same time." }, { - "goalID": "G2667d24e2fb0015183e6d9d9e0bf277e1026ed2b1020d1787673ffa6692b9506" + "goalID": "G465cbe195779daf0dd8449ead358f77dd3047c01538d8a8177fcca7f6c7732b9" }, { - "goalID": "G05c2ab5e047aa67b80ff338e4c4e108f784b129105970a8ea7dad02bb488b3ef", + "goalID": "G9957c837ce04f4494df641b057cfc3161fdc8a4ec9898dc136548f46f75bc41e", "note": "These are the ∞ dan boss songs. Good luck!" } ] @@ -1240,24 +1240,24 @@ "desc": "", "goals": [ { - "goalID": "Gb287f5494eee0a93e4a6f890546af25d5ae3a4f2e78b58f2fd31040c40fa354e" + "goalID": "G0f88e481ccc93429f62c7124103bb0d29c80eaf46265cb42ebf873fc1cded1ef" }, { - "goalID": "G56a9563f7da4e29b97be8dcdc95a384e7bac136566e1170c9b60d38fc79314c5", + "goalID": "Gba598c9c26a325bb63a54c9fb6be633ef19f4c23e13f569204d3d586a4b6670e", "note": "S is for Splendid!" }, { - "goalID": "Ga08f0d8f1b4616acfac36826684318e4335f31106235eca6fbda80bb7dd36018", + "goalID": "G13a74d151e58d9e4edea8c247804cacc51d435c831e355408ecfe4aef8131942", "note": "Oops, all lasers!" }, { - "goalID": "G42f1df315e5648de270061cdc00adbdd07c4fcb5ba34dee344063e94b076ac4c" + "goalID": "G6b92ca680c48f3df1ccf2772438f75abbcc1de394ec6c2ddd6e5fd7727cfef63" }, { - "goalID": "G32afd19762c11d926ae794f659b99c31a0c21097866a792e95b931a49dddbdfd" + "goalID": "G011d1f17d3a353d69ad87465ea6962d8e83843c4f6cf0caf0e8a45792800ea09" }, { - "goalID": "G0e1f2bf6e45848b28311505260a5c04951ce9f5fd199363e5fc8fee420c39a17" + "goalID": "Ge11fd188ac70c54f6b37465777598f77980ab49565ab206118a5b85a73d47194" } ] }, @@ -1266,19 +1266,19 @@ "desc": "", "goals": [ { - "goalID": "Gb29e1c2989fc5cf181ad34af23025d56a39ba5481e10f67ee468b229328b404d", + "goalID": "G8bbc3e020dba18d3d294f2a51f722e1460b047324a80b15c44db4eb81de65729", "note": "We're getting close to the end. Luckily for you, you can skip one song this time." }, { - "goalID": "Gb00614020a091ce7df081bbab49e16c50715f4d9bae0fb4a643c55ee3d4ce88a", + "goalID": "G5882d96f0ef38eafb99037fdfc4e61a1fcc0038d5c19bef289f5ab055163f83d", "note": "None of these are easy, and they all require different skills. Try them all." }, { - "goalID": "Gf827d9b6ab5f37ced9ffc825bf9bff85d16d597a6f49c4008c771d38becf1e95", + "goalID": "G3a85c5204e9708532cf7f0c038bc6e65cfc929a40d090faaf361a07fb6b8bfa6", "note": "Yup. It's a lot but have faith!" }, { - "goalID": "G0d74c382453d4fea5706533e1d748519b2b566351eb09a1f13704630c20eda31" + "goalID": "G10db40bef5ef910ec3b247621889e66bc431220746a5e536c45d4f8bf0386f36" } ] } @@ -1296,23 +1296,23 @@ "desc": "", "goals": [ { - "goalID": "Geee0d6a1d6729fa68e90d32453453f0ea89d18d98092ea65db8944d3d9923b74" + "goalID": "G5234d7e017a9d520b2714f84bd66ff9ac28e04573e4afff04249bcef6ed7c0ed" }, { - "goalID": "G8a9df565dad7b7eeb7e4f55328f0a0a218e807792875881c3011a5220f387f0b", + "goalID": "G10b47f5c3084d79bc92d45e616dcfb7dc3a7040aa3155fd49397352be6f5b6fa", "note": "S is for... uh..." }, { - "goalID": "G06c67b34e5d84643f0a6b863f7323a7497483d96c92ea3e1018d1b209801534e" + "goalID": "G582b57b4ce8cf63c44690d329d9e80f9701a841ca3f8c8dc291dbdcfb545a38d" }, { - "goalID": "Gcc178c0d77ede6a01125d09e2c9df9cf6257620178b00a7d5a791b1d532bd737" + "goalID": "G7d14143c6d71c5a9412d4ee87816eba1b926e1f9e282d482cf4c8f8c1bdaebb0" }, { - "goalID": "Gc987927c7c4b07a08d4fd8c8bd21999f7c59e8dd7201510b2dd18aeb590e30ff" + "goalID": "G04fe718f0483fb71361bb3bd82d42674b25ca069823831c8907f52f7a68b88c0" }, { - "goalID": "Gb454a482cd31c757cafd931aa41542fa60c219c13d46214c8514c10b5c792d59" + "goalID": "G9c178aa1967a795a17a9eee9c84bbabd63b4cedc3223c5c6fd245839729718df" } ] }, @@ -1321,20 +1321,20 @@ "desc": "", "goals": [ { - "goalID": "G0c761389ef0fbbad848457ddd0e98fc55c282180271ad16ca4c9815846fec994", + "goalID": "G54b92c7e85b819464c7bb21b9c1665fc745c13f289ebe2946ea36bc6f0e074ac", "note": "All of those have at least one deadly pattern that can single-handedly swipe your Excessive Clear. But this time you can't skip any." }, { - "goalID": "G9cc798bd379dad47ba50176b236b09209a448b6eae397e932169b0552485e2bc" + "goalID": "G647d4f2a60cf456a392b6f062725978732d1723ef93b0cc2dc1bb75302e9479b" }, { - "goalID": "G15163c2d1ddf5856d12492deacb2317cd48c47bc325a61f515dad052864b2158" + "goalID": "G1fb1cc0ee9612ead59f8792d4fe28ba527af5fecdea0b2198f9c7ac2c4d2abd4" }, { - "goalID": "G33d09280105bfe16aaff54dad304739e2e091c9a1e647164caf2394ee513a2e2" + "goalID": "G5e9b6f5ab4339cbfcc62ab4f13f0ddb4c2576573e0f25f032602d11816acd846" }, { - "goalID": "G7d0dc89e17f74a9548c10538e3e2c6e6b99e2c5e77f98699d104f4c06682a432", + "goalID": "Gf67e8bf457336de147cd7e3aa5fd27d3aaa33f057a66452c4a3eae73d73e0ef0", "note": "Good luck." } ] diff --git a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx index b82402467..c63bda27b 100644 --- a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx +++ b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx @@ -1,3 +1,4 @@ +import { ErrorPage } from "#app/pages/ErrorPage"; import FolderInfoHeader from "#components/game/folder/FolderInfoHeader"; import QuickTooltip from "#components/layout/misc/QuickTooltip"; import DifficultyCell from "#components/tables/cells/DifficultyCell"; @@ -145,6 +146,15 @@ export default function SpecificFolderPage({ reqUser, game }: Props) { const base = `${useUGPTBase({ reqUser, game })}/folders/${folderSlug}`; + if (error?.statusCode === 404) { + return ( + + ); + } + if (error) { return ; } diff --git a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/targets/UGPTGoalsPage.tsx b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/targets/UGPTGoalsPage.tsx index 4082b622f..e8ff95ce7 100644 --- a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/targets/UGPTGoalsPage.tsx +++ b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/targets/UGPTGoalsPage.tsx @@ -1,4 +1,4 @@ -import AddNewGoalForQuestModal from "#components/targets/AddNewGoalForQuestModal"; +import GoalBuilder from "#components/targets/GoalBuilder"; import DeleteGoalsModal from "#components/targets/DeleteGoalsModal"; import GoalSubInfo from "#components/targets/GoalSubInfo"; import ApiError from "#components/util/ApiError"; @@ -12,13 +12,14 @@ import { type UGPT } from "#types/react"; import { APIFetchV1 } from "#util/api"; import { CreateGoalSubDataset } from "#util/data"; import React, { useContext, useReducer, useState } from "react"; -import { Button, Col } from "react-bootstrap"; +import { Button, Col, Modal } from "react-bootstrap"; import { Link } from "react-router-dom"; -import { FormatGame } from "tachi-common"; +import { type GoalDocument, FormatGame } from "tachi-common"; export default function UGPTGoalsPage({ reqUser, game }: UGPT) { - const [show, setShow] = useState(false); + const [showAdd, setShowAdd] = useState(false); const [showDelete, setShowDelete] = useState(false); + const [editingGoal, setEditingGoal] = useState(null); const { reloadTargets } = useContext(TargetsContext); const [refresh, refetchGoals] = useReducer((x) => x + 1, 0); @@ -37,15 +38,57 @@ export default function UGPTGoalsPage({ reqUser, game }: UGPT) { } const userMap = new Map([[reqUser.id, reqUser]]); - const dataset = CreateGoalSubDataset(data, userMap); + const handleAddGoal = async (rawGoal: { goal: Pick }) => { + await APIFetchV1( + `/users/${reqUser.id}/games/${game}/targets/goals/add-goal`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + criteria: rawGoal.goal.criteria, + charts: rawGoal.goal.charts, + }), + }, + true, + true, + ); + + setShowAdd(false); + refetchGoals(); + reloadTargets(); + }; + + const handleUpdateGoal = async ( + oldGoalID: string, + rawGoal: { goal: Pick }, + ) => { + await APIFetchV1( + `/users/${reqUser.id}/games/${game}/targets/goals/${oldGoalID}`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + criteria: rawGoal.goal.criteria, + charts: rawGoal.goal.charts, + }), + }, + true, + true, + ); + + setEditingGoal(null); + refetchGoals(); + reloadTargets(); + }; + return (
- - - {show && ( - { - await APIFetchV1( - `/users/${reqUser.id}/games/${game}/targets/goals/add-goal`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - criteria: rawGoal.goal.criteria, - charts: rawGoal.goal.charts, - }), - }, - true, - true, - ); - - refetchGoals(); - reloadTargets(); - }} - setShow={setShow} - show={show} + onEditGoal={(goal) => setEditingGoal(goal)} /> + + + {/* Add goal modal */} + setShowAdd(false)} show={showAdd} size="xl"> + + Add New Goal + + + + + + + {/* Update goal modal */} + {editingGoal && ( + setEditingGoal(null)} show size="xl"> + + Update Goal + + + handleUpdateGoal(editingGoal.goalID, rawGoal)} + /> + + )} + {showDelete && ( { await APIFetchV1( `/users/${reqUser.id}/games/${game}/targets/goals/${goalID}`, - { - method: "DELETE", - }, + { method: "DELETE" }, true, true, ); refetchGoals(); - reloadTargets(); }} setShow={setShowDelete} diff --git a/typescript/client/src/app/pages/dashboard/utils/QuestEditor.tsx b/typescript/client/src/app/pages/dashboard/utils/QuestEditor.tsx index 5fcc3d00c..a597ccdce 100644 --- a/typescript/client/src/app/pages/dashboard/utils/QuestEditor.tsx +++ b/typescript/client/src/app/pages/dashboard/utils/QuestEditor.tsx @@ -1,28 +1,54 @@ +/** + * Quest & Questline Editor + * + * Three-panel layout: + * Left — Quest list + "New Quest" inline form + * Centre — Selected quest editor (EditableQuest) + * Right — Questline composer + * + * Everything is saved to localStorage under LOCAL_QUEST_KEY / LOCAL_QUESTLINE_KEY. + * Export produces separate quests.json and questlines.json files. + */ + import useSetSubheader from "#components/layout/header/useSetSubheader"; -import AddNewQuestModal from "#components/targets/AddNewQuestModal"; import EditableQuest from "#components/targets/quests/editor/EditableQuest"; import Divider from "#components/util/Divider"; +import EditableText from "#components/util/EditableText"; import Icon from "#components/util/Icon"; +import Muted from "#components/util/Muted"; import { TachiConfig } from "#lib/config"; -import { type SetState } from "#types/react"; -import { type RawQuestDocument } from "#types/tachi"; +import { type RawQuestDocument, type RawQuestlineDocument } from "#types/tachi"; import { ChangeAtPosition, DeleteInPosition } from "#util/misc"; import { p, type PrudenceSchema } from "prudence"; import React, { useEffect, useMemo, useState } from "react"; -import { Alert, Button, Col, Form, Modal, Row } from "react-bootstrap"; -import { FormatPrError, GetGameGroupConfig } from "tachi-common"; +import { Alert, Badge, Button, Col, Form, Modal, Row } from "react-bootstrap"; +import { + FormatGame, + type GameGroup, + FormatPrError, + GetGameGroupConfig, + LEGACY_GameGroupPTToGame, + type LEGACY_GPTString, + type LEGACY_Playtype, + type V3Game, +} from "tachi-common"; + +// ─── localStorage keys ──────────────────────────────────────────────────────── const LOCAL_QUEST_KEY = "LOCAL_QUESTS"; +const LOCAL_QUESTLINE_KEY = "LOCAL_QUESTLINES"; + +// ─── Validation schemas ─────────────────────────────────────────────────────── const PR_LOCAL_QUESTS_SCHEMA: PrudenceSchema = { json: [ { game: p.isIn(TachiConfig.GAME_GROUPS), playtype: (self, parent) => { - const gameConfig = GetGameGroupConfig(parent.game as any); + const gameConfig = GetGameGroupConfig(parent.game as GameGroup); - if (!gameConfig.playtypes.includes(self)) { - return `Invalid playtype '${self}' for ${parent.game}`; + if (!(gameConfig.playtypes as ReadonlyArray).includes(self)) { + return `Invalid playtype '${String(self)}' for ${String(parent.game)}`; } return true; @@ -38,29 +64,16 @@ const PR_LOCAL_QUESTS_SCHEMA: PrudenceSchema = { goal: { name: "string", charts: p.or( - { - type: p.is("folder"), - data: "string", - }, - { - type: p.is("multi"), - data: ["string"], - }, - { - type: p.is("single"), - data: "string", - }, + { type: p.is("folder"), data: "string" }, + { type: p.is("multi"), data: ["string"] }, + { type: p.is("single"), data: "string" }, ), criteria: p.or( - { - mode: p.is("single"), - key: "string", // temp - value: "number", - }, + { mode: p.is("single"), key: "string", value: "number" }, { mode: p.isIn("absolute", "proportion"), countNum: p.isPositive, - key: "string", // temp + key: "string", value: "number", }, ), @@ -74,7 +87,9 @@ const PR_LOCAL_QUESTS_SCHEMA: PrudenceSchema = { ], }; -function GetLocalQuests(): Array { +// ─── localStorage helpers ───────────────────────────────────────────────────── + +function loadLocalQuests(): Array { try { const data = window.localStorage.getItem(LOCAL_QUEST_KEY); @@ -83,194 +98,632 @@ function GetLocalQuests(): Array { } const json = JSON.parse(data); - - // check that this data is what it seems const err = p({ json }, PR_LOCAL_QUESTS_SCHEMA); if (err) { - const e = confirm( - `Failed to validate your local quests: ${FormatPrError( - err, - )}. DELETE ALL AND START AGAIN?`, - ); - - if (e) { + if ( + confirm( + `Failed to validate local quests: ${FormatPrError(err)}. Delete all and start again?`, + ) + ) { return []; } } - return json; - } catch (err) { - console.error(err); - + return json as RawQuestDocument[]; + } catch { return []; } } +function loadLocalQuestlines(): Array { + try { + const data = window.localStorage.getItem(LOCAL_QUESTLINE_KEY); + + if (!data) { + return []; + } + + return JSON.parse(data) as RawQuestlineDocument[]; + } catch { + return []; + } +} + +function downloadJson(data: unknown, filename: string) { + const href = `data:application/json;charset=UTF-8,${encodeURIComponent(JSON.stringify(data, null, "\t"))}`; + const a = document.createElement("a"); + a.href = href; + a.download = filename; + a.click(); +} + +// ─── Main component ─────────────────────────────────────────────────────────── + export default function QuestEditor() { - useSetSubheader(["Developer Utils", "Quest Creator"]); + useSetSubheader(["Developer Utils", "Quest & Questline Editor"]); - const INIT_STATE = useMemo(() => GetLocalQuests(), []); - const [quests, setQuests] = useState(INIT_STATE); - const [show, setShow] = useState(false); - const [showImport, setShowImport] = useState(false); + const initQuests = useMemo(() => loadLocalQuests(), []); + const initQuestlines = useMemo(() => loadLocalQuestlines(), []); + const [quests, setQuests] = useState>(initQuests); + const [questlines, setQuestlines] = useState>(initQuestlines); + const [selectedQuestIdx, setSelectedQuestIdx] = useState(null); + + const [showImportQuests, setShowImportQuests] = useState(false); + const [showImportQuestlines, setShowImportQuestlines] = useState(false); + + // ── Persist to localStorage on every change ──────────────────────────────── useEffect(() => { window.localStorage.setItem(LOCAL_QUEST_KEY, JSON.stringify(quests)); }, [quests]); + useEffect(() => { + window.localStorage.setItem(LOCAL_QUESTLINE_KEY, JSON.stringify(questlines)); + }, [questlines]); + + // ── Derived ──────────────────────────────────────────────────────────────── + const selectedQuest = selectedQuestIdx !== null ? quests[selectedQuestIdx] ?? null : null; + + const addQuest = (gptString: LEGACY_GPTString) => { + const [game, playtype] = gptString.split(":") as [GameGroup, LEGACY_Playtype]; + const v3Game: V3Game = LEGACY_GameGroupPTToGame(game, playtype); + + const newQuest: RawQuestDocument = { + game: v3Game, + name: "Untitled Quest", + desc: "Please set a description.", + rawQuestData: [], + }; + + setQuests((prev) => { + const updated = [...prev, newQuest]; + setSelectedQuestIdx(updated.length - 1); + return updated; + }); + }; + return ( - + -

{TachiConfig.NAME} Quest Editor

- - - This tool is for creating your own quests and questlines. -
- These can be saved to a .json file, and sent to an admin to be - considered for inclusion on the site! -
+

Quest & Questline Editor

+

+ Build quests and questlines locally, then download the JSON files and submit + them as a PR to be included on the site. +

- -
- {quests.length > 0 && ( - <> - - Download Quests - -
{ - if (confirm("Are you sure you want to start from scratch?")) { - setQuests([]); - } - }} - > - Start Again -
- - )} -
setShowImport(true)}> - Import Quests -
-
-
- If you want these quests to be added to the site, make a post about your quests - in the discord! + {/* ── Left panel: Quest list ─────────────────────────────────────── */} + +
+
Quests
+ {quests.length}
+ + + - - {quests.map((quest, i) => ( - - { - setQuests(ChangeAtPosition(quests, quest, i)); - }} - onDelete={() => { - setQuests(DeleteInPosition(quests, i)); - }} - quest={quest} - /> - - ))} - -
-
- -
+ )} + + {quests.length > 0 && ( + + )}
- setQuests([...quests, rawQuest])} - setShow={setShow} - show={show} - /> - {showImport && ( - setQuests(newData)} - setShow={setShowImport} - show={showImport} + + {/* ── Centre panel: Quest editor ─────────────────────────────────── */} + + {selectedQuest && selectedQuestIdx !== null ? ( + + setQuests(ChangeAtPosition(quests, updated, selectedQuestIdx)) + } + onDelete={() => { + setQuests(DeleteInPosition(quests, selectedQuestIdx)); + setSelectedQuestIdx(null); + }} + quest={selectedQuest} + /> + ) : ( +
+
+ +

Select a quest from the list to edit it.

+
+
+ )} + + + {/* ── Right panel: Questline composer ───────────────────────────── */} + +
+
Questlines
+ {questlines.length} +
+ + setQuestlines((prev) => [...prev, ql])} + onUpdate={(updated, idx) => + setQuestlines(ChangeAtPosition(questlines, updated, idx)) + } + onDelete={(idx) => setQuestlines(DeleteInPosition(questlines, idx))} + questlines={questlines} + quests={quests} + /> + + + +
+ {questlines.length > 0 && ( + + )} + + {questlines.length > 0 && ( + + )} +
+ + + {/* Import modals */} + {showImportQuests && ( + setShowImportQuests(false)} + onImport={(data) => { + setQuests(data as RawQuestDocument[]); + setSelectedQuestIdx(null); + }} + schema={PR_LOCAL_QUESTS_SCHEMA} + /> + )} + {showImportQuestlines && ( + setShowImportQuestlines(false)} + onImport={(data) => setQuestlines(data as RawQuestlineDocument[])} /> )} ); } -function ImportQuestsModal({ - show, - setShow, - onChange, +// ─── QuestList ──────────────────────────────────────────────────────────────── + +function QuestList({ + quests, + selectedIdx, + onSelect, + onAddQuest, }: { - onChange: (data: Array) => void; - setShow: SetState; - show: boolean; + onAddQuest: (gpt: LEGACY_GPTString) => void; + onSelect: (idx: number) => void; + quests: Array; + selectedIdx: number | null; +}) { + const [gpt, setGpt] = useState(null); + + const allGpts: Array<{ label: string; value: LEGACY_GPTString }> = TachiConfig.GAME_GROUPS.flatMap( + (gameGroup) => { + const config = GetGameGroupConfig(gameGroup); + + return config.playtypes.map((pt) => ({ + value: `${gameGroup}:${pt}` as LEGACY_GPTString, + label: FormatGame(LEGACY_GameGroupPTToGame(gameGroup, pt)), + })); + }, + ); + + return ( +
+ {quests.length === 0 && ( + No quests yet. Add one below. + )} + {quests.map((quest, i) => ( + + ))} + + {/* Inline new-quest form */} +
+ setGpt(e.target.value as LEGACY_GPTString)} + size="sm" + style={{ flex: 1 }} + value={gpt ?? ""} + > + + {allGpts.map((g) => ( + + ))} + + +
+
+ ); +} + +// ─── QuestlineComposer ──────────────────────────────────────────────────────── + +function QuestlineComposer({ + questlines, + quests, + onAddQuestline, + onUpdate, + onDelete, +}: { + onAddQuestline: (ql: RawQuestlineDocument) => void; + onDelete: (idx: number) => void; + onUpdate: (updated: RawQuestlineDocument, idx: number) => void; + questlines: Array; + quests: Array; +}) { + const [newName, setNewName] = useState(""); + const [newGame, setNewGame] = useState(""); + + const allGpts: Array<{ label: string; value: LEGACY_GPTString }> = TachiConfig.GAME_GROUPS.flatMap( + (gameGroup) => { + const config = GetGameGroupConfig(gameGroup); + + return config.playtypes.map((pt) => ({ + value: `${gameGroup}:${pt}` as LEGACY_GPTString, + label: FormatGame(LEGACY_GameGroupPTToGame(gameGroup, pt)), + })); + }, + ); + + return ( +
+ {questlines.length === 0 && ( + No questlines yet. + )} + + {questlines.map((ql, idx) => ( + onDelete(idx)} + onUpdate={(updated) => onUpdate(updated, idx)} + quests={quests} + questline={ql} + /> + ))} + + {/* New questline inline form */} +
+

New Questline

+ setNewName(e.target.value)} + placeholder="Questline name…" + size="sm" + value={newName} + /> +
+ setNewGame(e.target.value as LEGACY_GPTString)} + size="sm" + style={{ flex: 1 }} + value={newGame} + > + + {allGpts.map((g) => ( + + ))} + + +
+
+
+ ); +} + +function QuestlineCard({ + questline, + quests, + onUpdate, + onDelete, +}: { + onDelete: () => void; + onUpdate: (updated: RawQuestlineDocument) => void; + questline: RawQuestlineDocument; + quests: Array; +}) { + const availableQuests = quests.filter( + (q) => !questline.quests.includes(q.name), + ); + + const addQuestByName = (name: string) => { + if (!questline.quests.includes(name)) { + onUpdate({ ...questline, quests: [...questline.quests, name] }); + } + }; + + const removeQuest = (name: string) => { + onUpdate({ ...questline, quests: questline.quests.filter((q) => q !== name) }); + }; + + const moveQuest = (idx: number, direction: -1 | 1) => { + const arr = [...questline.quests]; + const target = idx + direction; + + if (target < 0 || target >= arr.length) { + return; + } + + [arr[idx], arr[target]] = [arr[target]!, arr[idx]!]; + onUpdate({ ...questline, quests: arr }); + }; + + return ( +
+
+
+ onUpdate({ ...questline, name })} + placeholderText="Questline name" + /> + onUpdate({ ...questline, desc })} + placeholderText="Description…" + /> + {questline.game} / {questline.playtype} +
+ +
+ + {/* Ordered quest list */} + {questline.quests.length === 0 ? ( + No quests added yet. + ) : ( +
+ {questline.quests.map((questName, i) => ( +
+ {i + 1}. + {questName} + + + +
+ ))} +
+ )} + + {/* Add quest from available */} + {availableQuests.length > 0 && ( + { + if (e.target.value) { + addQuestByName(e.target.value); + e.target.value = ""; + } + }} + size="sm" + > + + {availableQuests.map((q, i) => ( + + ))} + + )} +
+ ); +} + +// ─── ImportJsonModal ────────────────────────────────────────────────────────── + +function ImportJsonModal({ + description, + onHide, + onImport, + schema, +}: { + description: string; + onHide: () => void; + onImport: (data: unknown[]) => void; + schema?: PrudenceSchema; }) { const [err, setErr] = useState(null); return ( - setShow(false)} show={show} size="xl"> + - - Import quests.json - + Import {description} - - - - Uploading existing quests will replace whatever you're currently working - on! Make sure you've saved your existing work! - - - - - - Upload quests.json File - - { - try { - const file: File = e.target.files![0]; + + This will replace your current {description} data. Make sure you've exported + first! + + + Upload {description} + { + try { + const file = e.target.files?.[0]; - const contents = JSON.parse(await file.text()); + if (!file) { + return; + } - const err = p({ json: contents }, PR_LOCAL_QUESTS_SCHEMA); + const contents = JSON.parse(await file.text()) as unknown[]; - if (err) { - throw new Error(FormatPrError(err)); - } + if (schema) { + const prErr = p({ json: contents }, schema); - onChange(contents as RawQuestDocument[]); - - setShow(false); - } catch (e) { - const err = e as Error; - setErr(err.message); + if (prErr) { + throw new Error(FormatPrError(prErr)); } - }} - type="file" - /> - - - {err && ( - - Invalid file: {err} - - )} - + } + + onImport(contents); + onHide(); + } catch (e) { + setErr((e as Error).message); + } + }} + type="file" + /> + + {err &&

Invalid file: {err}

}
); diff --git a/typescript/client/src/components/game/targets/QuestlinePage.tsx b/typescript/client/src/components/game/targets/QuestlinePage.tsx index acd7b573b..d8c7abfc9 100644 --- a/typescript/client/src/components/game/targets/QuestlinePage.tsx +++ b/typescript/client/src/components/game/targets/QuestlinePage.tsx @@ -36,22 +36,24 @@ export default function QuestlinePage({ game }: GamePT) { } const goalMap = CreateGoalMap(data.goals); - const questMap = CreateQuestMap(data.quests); return ( - Go back to all questlines... + ← Back to all questlines + {/* Summary card with timeline — the Questline component renders the + vertical timeline nodes when a quests map is provided. */} + + {/* Full quest cards below, anchored so timeline links scroll to them */} {data.questline.quests.map((questID) => { const quest = questMap.get(questID); if (!quest) { - // shouldn't happen, but paste over it. return null; } diff --git a/typescript/client/src/components/targets/GoalBuilder.tsx b/typescript/client/src/components/targets/GoalBuilder.tsx new file mode 100644 index 000000000..05d45bf8f --- /dev/null +++ b/typescript/client/src/components/targets/GoalBuilder.tsx @@ -0,0 +1,643 @@ +/** + * GoalBuilder — a focused, step-based UI for creating or updating a goal. + * + * Step 1 – Chart target: card-toggle buttons (Single / Multiple / Folder) with + * an AsyncSelect beneath the chosen type. + * Step 2 – Criteria: pill-button metric selector + inline value picker. + * + * Live goal-name preview is fetched from the server with a 400 ms debounce. + * + * When `existingGoal` is provided the builder pre-fills from it and shows a + * diff summary in the preview header. + */ + +import AsyncSelect from "#components/util/AsyncSelect"; +import Divider from "#components/util/Divider"; +import Icon from "#components/util/Icon"; +import { type SongChartsSearch } from "#types/api-returns"; +import { type GamePT, type SetState } from "#types/react"; +import { type RawQuestGoal } from "#types/tachi"; +import { APIFetchV1 } from "#util/api"; +import { clamp, UppercaseFirst } from "#util/misc"; +import { StrSOV } from "#util/sorts"; +import React, { useEffect, useRef, useState } from "react"; +import { Badge, Button, Col, Form, InputGroup, Row } from "react-bootstrap"; +import { type GroupBase, type OptionsOrGroups } from "react-select"; +import { + type ChartDocument, + type FolderDocument, + FormatChart, + GetGameConfig, + GetScoreMetricConf, + GetScoreMetrics, + type GoalDocument, + type SongDocument, +} from "tachi-common"; +import { type ConfEnumScoreMetric } from "tachi-common/types/metrics"; + +// ─── Public interface ───────────────────────────────────────────────────────── + +interface GoalBuilderProps extends GamePT { + /** + * Called when the user confirms the goal. + * + * Receives the `RawQuestGoal` so callers can either add-goal directly or + * attach a note for the quest-editor use-case. + */ + onCreate: (rawGoal: RawQuestGoal) => void; + + /** If set, shows a note input (useful when adding a goal inside a quest). */ + showNote?: boolean; + + /** + * When provided the builder pre-fills from this goal's `charts` and + * `criteria` so the user can modify and "update" it. + */ + existingGoal?: GoalDocument; + + /** Label for the confirm button. Defaults to "Add Goal". */ + confirmLabel?: string; +} + +// ─── Component ──────────────────────────────────────────────────────────────── + +export default function GoalBuilder({ + game, + onCreate, + showNote = false, + existingGoal, + confirmLabel = "Add Goal", +}: GoalBuilderProps) { + const gameConfig = GetGameConfig(game); + + const defaultEnum = gameConfig.preferredDefaultEnum; + const enumConf = GetScoreMetricConf(gameConfig, defaultEnum) as ConfEnumScoreMetric; + + // ── State ────────────────────────────────────────────────────────────────── + + const [charts, setCharts] = useState( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + existingGoal?.charts ?? ({ type: "single", data: null } as any), + ); + + const [criteria, setCriteria] = useState( + existingGoal?.criteria ?? { + mode: "single", + key: defaultEnum, + value: enumConf.values.indexOf(enumConf.minimumRelevantValue), + }, + ); + + const [note, setNote] = useState(""); + const [goalName, setGoalName] = useState(existingGoal?.name ?? "..."); + const [goalErr, setGoalErr] = useState(null); + + // ── Sync: force single mode when chart type is single ───────────────────── + + const isFirstRender = useRef(true); + + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + + if (charts.type === "single") { + setCriteria((prev) => ({ mode: "single", key: prev.key, value: prev.value })); + } + }, [charts.type]); + + // ── Live preview (debounced 400 ms) ─────────────────────────────────────── + + useEffect(() => { + if ("data" in charts && charts.data === null) { + setGoalName("..."); + return; + } + + const id = window.setTimeout(async () => { + const res = await APIFetchV1(`/games/${game}/targets/goals/format`, { + method: "POST", + body: JSON.stringify({ criteria, charts }), + headers: { "Content-Type": "application/json" }, + }); + + if (res.success) { + setGoalName(res.body); + setGoalErr(null); + } else { + const match = /^Invalid goal: (.*)/u.exec(res.description) as + | [string, string] + | null; + + if (match) { + setGoalName("..."); + setGoalErr(match[1]); + } + } + }, 400); + + return () => window.clearTimeout(id); + }, [charts, criteria, game]); + + // ── Computed helpers ────────────────────────────────────────────────────── + + const isReady = + goalName !== "..." && + !goalErr && + !("data" in charts && charts.data === null) && + !(criteria.mode === "absolute" && criteria.countNum <= 1); + + const isDiffMode = !!existingGoal; + + // ── Render ──────────────────────────────────────────────────────────────── + + return ( +
+ {/* ── Preview header ─────────────────────────────────────────────── */} +
+

+ {isDiffMode ? "Updating goal to:" : "Goal preview:"} +

+

+ {goalName} +

+ {isDiffMode && existingGoal.name !== goalName && goalName !== "..." && ( +

+ {existingGoal.name} +

+ )} + {goalErr &&

{goalErr}

} +
+ + + + {/* ── Step 1: Chart target ───────────────────────────────────────── */} +
Step 1 — Chart Target
+ + + + + {/* ── Step 2: Criteria ──────────────────────────────────────────── */} +
Step 2 — Success Criteria
+ + + {/* ── Optional note ─────────────────────────────────────────────── */} + {showNote && ( + <> + + + Note (optional) + setNote(e.target.value)} + placeholder="Is this goal particularly noteworthy in this quest?" + value={note} + /> + + + )} + + + + {/* ── Confirm ───────────────────────────────────────────────────── */} +
+ +
+
+ ); +} + +// ─── ChartTargetPicker ──────────────────────────────────────────────────────── + +function ChartTargetPicker({ + charts, + game, + onChange, +}: { + charts: GoalDocument["charts"]; + onChange: (charts: GoalDocument["charts"]) => void; +} & GamePT) { + const [type, setType] = useState(charts.type); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [data, setData] = useState("data" in charts ? charts.data : null); + const isFirstRender = useRef(true); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => onChange({ type, data }), [type, data]); + + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + + setData(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onChange({ type, data: null as any }); + // Intentionally only react to type changes to reset data on type switch. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [type]); + + const types: Array<{ icon: string; id: GoalDocument["charts"]["type"]; label: string }> = [ + { icon: "music", id: "single", label: "Single Chart" }, + { icon: "layer-group", id: "multi", label: "Multiple Charts" }, + { icon: "folder-open", id: "folder", label: "Folder" }, + ]; + + return ( + <> + {/* Card-toggle buttons */} + + {types.map((t) => ( + +
setType(t.id)} + style={{ cursor: "pointer" }} + > +
+ +
+ {t.label} +
+ + ))} +
+ + {/* Async search */} + {type === "folder" ? ( + + ) : type === "single" ? ( + + ) : ( + + )} + + ); +} + +// ─── CriteriaPicker ─────────────────────────────────────────────────────────── + +function CriteriaPicker({ + criteria, + charts, + setCriteria, + game, +}: { + charts: GoalDocument["charts"]; + criteria: GoalDocument["criteria"]; + setCriteria: SetState; +} & GamePT) { + const gameConfig = GetGameConfig(game); + const availableMetrics = GetScoreMetrics(gameConfig, ["ENUM", "DECIMAL", "INTEGER"]); + + return ( + <> + {/* Metric pills */} +
+ {availableMetrics.map((metricKey) => ( + + ))} +
+ + {/* Value row */} +
+ + {UppercaseFirst(criteria.key)} + + ≥ + setCriteria({ ...criteria, value })} + /> +
+ + {/* Mode picker (absolute / proportion) for multi/folder */} + {charts.type !== "single" && ( + <> + + { + if (mode === "single") { + setCriteria({ mode: "single", key: criteria.key, value: criteria.value }); + } else { + setCriteria({ ...criteria, countNum: countNum ?? 0, mode }); + } + }} + /> + + )} + + ); +} + +// ─── CriteriaValuePicker ───────────────────────────────────────────────────── + +function CriteriaValuePicker({ + criteria, + game, + onChange, +}: { + criteria: GoalDocument["criteria"]; + onChange: (value: number) => void; +} & GamePT) { + const gameConfig = GetGameConfig(game); + const conf = GetScoreMetricConf(gameConfig, criteria.key); + + if (!conf) { + return Unknown metric: {criteria.key}; + } + + switch (conf.type) { + case "NULLABLE_GRAPH": + case "GRAPH": + return Cannot set goals for graph metrics.; + + case "DECIMAL": + case "INTEGER": + return ( + onChange(Number(e.target.value))} + style={{ width: "120px" }} + type="number" + value={criteria.value} + /> + ); + + case "ENUM": + return ( + onChange(Number(e.target.value))} + style={{ width: "auto" }} + value={criteria.value.toString()} + > + {conf.values + .slice(conf.values.indexOf(conf.minimumRelevantValue)) + .map((label, i) => { + const idx = i + conf.values.indexOf(conf.minimumRelevantValue); + return ( + + ); + })} + + ); + } +} + +// ─── CriteriaModePicker ─────────────────────────────────────────────────────── + +function CriteriaModePicker({ + criteria, + onChange, + charts, + game: _game, +}: { + charts: GoalDocument["charts"]; + criteria: GoalDocument["criteria"]; + onChange: (mode: GoalDocument["criteria"]["mode"], countNum?: number) => void; +} & GamePT) { + const [absCount, setAbsCount] = useState( + criteria.mode === "absolute" ? criteria.countNum : 10, + ); + const [perCount, setPerCount] = useState( + criteria.mode === "proportion" ? criteria.countNum * 100 : 10, + ); + + useEffect(() => { + if (criteria.mode === "proportion") { + onChange("proportion", perCount / 100); + } else if (criteria.mode === "absolute") { + onChange("absolute", absCount); + } + // onChange is intentionally excluded — it's a callback prop that changes every render + // and including it would cause infinite loops. absCount/perCount drive the update. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [absCount, perCount]); + + if ("data" in charts && charts.data === null) { + return null; + } + + const pluralTarget = charts.type === "multi" ? "of these charts" : "charts in this folder"; + const anyTarget = charts.type === "multi" ? "any of these charts" : "any chart in this folder"; + + return ( +
+ onChange("single")} + type="radio" + /> +
+ onChange("absolute", absCount)} + type="radio" + /> + setAbsCount(Number(e.target.value))} + style={{ width: "80px" }} + type="number" + value={absCount} + /> + {pluralTarget} +
+ {charts.type !== "multi" && ( +
+ onChange("proportion", perCount / 100)} + type="radio" + /> + setPerCount(clamp(Number(e.target.value), 0, 100))} + style={{ width: "80px" }} + type="number" + value={perCount} + /> + % of {pluralTarget} +
+ )} +
+ ); +} + +// ─── FolderSelect ───────────────────────────────────────────────────────────── + +function FolderSelect({ + game, + onChange, + initialValue, +}: { initialValue?: string | null; onChange: (data: string) => void } & GamePT) { + let lastTimeout: number | null = null; + + const loadOptions = ( + input: string, + cb: (options: OptionsOrGroups>) => void, + ) => { + if (lastTimeout !== null) { + clearTimeout(lastTimeout); + } + + lastTimeout = window.setTimeout(async () => { + const res = await APIFetchV1>( + `/games/${game}/folders?search=${encodeURIComponent(input)}`, + ); + + if (!res.success) { + throw new Error(res.description); + } + + const options = res.body + .map((e) => ({ value: e.folderID, label: e.title })) + .sort(StrSOV((x) => x.label)); + + cb(options); + }, 300); + }; + + return ( + onChange(data.value)} + placeholder="Search for a folder..." + /> + ); +} + +// ─── ChartSelect ───────────────────────────────────────────────────────────── + +function ChartSelect({ + game, + multi = false, + onChange, + initialValue, +}: { + initialValue?: string | string[] | null; + multi?: boolean; + onChange: (data: string | string[]) => void; +} & GamePT) { + let lastTimeout: number | null = null; + + const loadOptions = ( + input: string, + cb: (options: OptionsOrGroups>) => void, + ) => { + if (lastTimeout !== null) { + clearTimeout(lastTimeout); + } + + lastTimeout = window.setTimeout(async () => { + const [res, res2] = await Promise.all([ + APIFetchV1( + `/games/${game}/charts?search=${encodeURIComponent(input)}`, + ), + APIFetchV1<{ + charts: Record>; + }>(`/search/chart-hash?search=${encodeURIComponent(input)}`), + ]); + + if (!res.success || !res2.success) { + throw new Error(res.description); + } + + const hashCharts = res2.body.charts[game] ?? []; + const options = [...res.body.charts, ...hashCharts.map((e) => e.chart)] + .map((e) => ({ value: e.chartID, label: FormatChart(e) })) + .sort(StrSOV((x) => x.label)); + + cb(options); + }, 300); + }; + + const defaultValue = initialValue + ? Array.isArray(initialValue) + ? initialValue.map((v) => ({ value: v, label: v })) + : { value: initialValue, label: initialValue } + : undefined; + + return ( + + onChange(Array.isArray(data) ? data.map((e: { value: string }) => e.value) : data.value) + } + placeholder="Search for a chart..." + /> + ); +} diff --git a/typescript/client/src/components/targets/GoalSubInfo.tsx b/typescript/client/src/components/targets/GoalSubInfo.tsx index 45aeca0d4..a3c9e38a9 100644 --- a/typescript/client/src/components/targets/GoalSubInfo.tsx +++ b/typescript/client/src/components/targets/GoalSubInfo.tsx @@ -5,10 +5,15 @@ import { type GoalSubDataset } from "#types/tables"; import { NumericSOV } from "#util/sorts"; import React, { useMemo, useState } from "react"; import { Col } from "react-bootstrap"; +import { type GoalDocument } from "tachi-common"; import { InnerQuestSectionGoal } from "./quests/Quest"; -export default function GoalSubInfo({ dataset }: { dataset: GoalSubDataset } & GamePT) { +export default function GoalSubInfo({ + dataset, + game, + onEditGoal, +}: { dataset: GoalSubDataset; onEditGoal?: (goal: GoalDocument) => void } & GamePT) { const [show, setShow] = useState<"achieved" | "all" | "unachieved">("all"); const { directGoals, folderGoals } = useMemo(() => { @@ -73,6 +78,11 @@ export default function GoalSubInfo({ dataset }: { dataset: GoalSubDataset } & G e.name)} goal={e.__related.goal} + onEdit={ + e.wasAssignedStandalone && onEditGoal + ? () => onEditGoal(e.__related.goal) + : undefined + } />
))} @@ -86,6 +96,11 @@ export default function GoalSubInfo({ dataset }: { dataset: GoalSubDataset } & G e.name)} goal={e.__related.goal} + onEdit={ + e.wasAssignedStandalone && onEditGoal + ? () => onEditGoal(e.__related.goal) + : undefined + } /> ))} diff --git a/typescript/client/src/components/targets/Questline.tsx b/typescript/client/src/components/targets/Questline.tsx index 5b8db6d0a..cb0769a15 100644 --- a/typescript/client/src/components/targets/Questline.tsx +++ b/typescript/client/src/components/targets/Questline.tsx @@ -9,8 +9,8 @@ import { TargetsContext } from "#context/TargetsContext"; import { APIFetchV1 } from "#util/api"; import { GetGoalIDsFromQuest } from "#util/data"; import { FormatTime } from "#util/time"; -import React, { useContext } from "react"; -import { Button, Col } from "react-bootstrap"; +import React, { useContext, useState } from "react"; +import { Badge, Button, Collapse, ProgressBar } from "react-bootstrap"; import { type QuestDocument, type QuestlineDocument } from "tachi-common"; export default function Questline({ @@ -21,146 +21,276 @@ export default function Questline({ quests?: Map; }) { const v3Game = questline.game; + const { questSubs } = useContext(TargetsContext); + + const totalQuests = questline.quests.length; + const achievedQuests = questline.quests.filter((id) => questSubs.get(id)?.achieved).length; + const progressPct = totalQuests > 0 ? Math.round((achievedQuests / totalQuests) * 100) : 0; + const anySubscribed = questline.quests.some((id) => questSubs.has(id)); return ( -

{questline.name}

- {questline.quests.length} Quests +
+
+
+

{questline.name}

+ {totalQuests} {totalQuests === 1 ? "Quest" : "Quests"} +
+ {!quests && ( +
+ + View All Quests + +
+ )} +
+ +

{questline.desc}

+ + {/* Overall progress bar — shown when the user has subscribed to any quest */} + {anySubscribed && ( +
+ {achievedQuests === totalQuests && totalQuests > 0 ? ( + + Questline Complete! + + ) : ( + <> +
+ Questline Progress + + {achievedQuests} / {totalQuests} quests complete + +
+ 0} + style={{ height: "10px" }} + variant="primary" + /> + + )} +
+ )}
} > - {questline.desc} - - + {/* Timeline — shown on questline detail page when quests map is provided */} {quests && ( - <> - - {questline.quests.map((questID) => { - const quest = quests.get(questID); +
+ {questline.quests.map((questID, idx) => { + const quest = quests.get(questID); - // shouldn't happen. hide it. - if (!quest) { - return null; - } + if (!quest) { + return null; + } - return ( -
- -
- ); - })} - - - - )} - - {!quests && ( -
- - View Quests - + return ( + + ); + })}
)} ); } -export function InnerQuestInfo({ quest }: { quest: QuestDocument }) { +function QuestTimelineNode({ + quest, + number, + isLast, +}: { + isLast: boolean; + number: number; + quest: QuestDocument; +}) { + const { questSubs } = useContext(TargetsContext); + const questSub = questSubs.get(quest.questID); + const [open, setOpen] = useState(false); + + const isAchieved = questSub?.achieved ?? false; + const goalTotal = GetGoalIDsFromQuest(quest).length; + + const nodeColour = isAchieved + ? "success" + : questSub && questSub.progress > 0 + ? "warning" + : "secondary"; + + return ( +
+ {/* Left: connector line + node circle */} +
+
+ {isAchieved ? : number} +
+ {!isLast && ( +
+ )} +
+ + {/* Right: quest info */} +
+ + + +
+

{quest.desc}

+
+ {goalTotal} goals + + + View details ↓ + +
+
+
+
+
+ ); +} + +function QuestSubscribeButton({ quest }: { quest: QuestDocument }) { const { settings } = useLUGPTSettings(); const { questSubs, reloadTargets } = useContext(TargetsContext); + const [loading, setLoading] = useState(false); const v3Game = quest.game; const questSub = questSubs.get(quest.questID); - if (!questSub) { + if (!settings) { + return null; + } + + if (questSub) { return ( - <> -
-
- -
- - - {quest.name} - - - {settings && ( -
- -
- )} -
- + + + ); } return ( - <> -
+ + ); +} + +/** Legacy component kept for compatibility — now delegates to QuestTimelineNode via Questline. */ +export function InnerQuestInfo({ quest }: { quest: QuestDocument }) { + const { questSubs } = useContext(TargetsContext); + const questSub = questSubs.get(quest.questID); + const v3Game = quest.game; + const goalTotal = GetGoalIDsFromQuest(quest).length; + + return ( +
+ {questSub ? (
{questSub.achieved ? ( - + ) : ( - + )}
+ ) : ( + + )} - - {quest.name} - + + {quest.name} + - {!questSub.achieved && ( -
- {questSub.progress} - / {GetGoalIDsFromQuest(quest).length} -
- )} -
- + {questSub && !questSub.achieved && ( +
+ {questSub.progress} + / {goalTotal} +
+ )} +
); } diff --git a/typescript/client/src/components/targets/quests/Quest.tsx b/typescript/client/src/components/targets/quests/Quest.tsx index 400ce994f..c8d7d75d9 100644 --- a/typescript/client/src/components/targets/quests/Quest.tsx +++ b/typescript/client/src/components/targets/quests/Quest.tsx @@ -12,7 +12,7 @@ import { GetGoalIDsFromQuest } from "#util/data"; import { HumanisedJoinArray } from "#util/misc"; import { FormatTime } from "#util/time"; import React, { useContext, useState } from "react"; -import { Badge, Button } from "react-bootstrap"; +import { Badge, Button, ProgressBar } from "react-bootstrap"; import { FormatGame, type GoalDocument, @@ -31,120 +31,117 @@ export default function Quest({ quest: QuestDocument; }) { const { user } = useContext(UserContext); - const { questSubs, reloadTargets } = useContext(TargetsContext); + const { questSubs, goalSubs, reloadTargets } = useContext(TargetsContext); const [subscribing, setSubscribing] = useState(false); const questSub = questSubs.get(quest.questID); - const goalsInQuests = GetGoalIDsFromQuest(quest).length; const v3Game = quest.game; - // if this is collapsable, show it conditionally. otherwise, always show it. + const allGoalIDs = GetGoalIDsFromQuest(quest); + const totalGoals = allGoalIDs.length; + const achievedGoals = allGoalIDs.filter((id) => goalSubs.get(id)?.achieved).length; + const progressPct = totalGoals > 0 ? Math.round((achievedGoals / totalGoals) * 100) : 0; + const [show, setShow] = useState(!collapsible); + const subscribeBtn = user && ( + questSub ? ( + + ) : ( + + ) + ); + return ( -

{quest.name}

- -
{quest.desc}
-
- Game: {FormatGame(quest.game)} +
+
+
+

{quest.name}

+

{quest.desc}

+ {FormatGame(quest.game)} +
+
{subscribeBtn}
+ {/* Overall progress bar — visible when subscribed */} {questSub && ( -
-

- Progress:{" "} - {questSub.achieved ? ( - COMPLETE! - ) : ( - - {questSub.progress} - /{goalsInQuests} - - )} -

+
+ {questSub.achieved ? ( + + Quest Complete! + + ) : ( + <> +
+ Progress + + {achievedGoals} / {totalGoals} goals + +
+ 0 && progressPct < 100} + style={{ height: "12px" }} + variant={progressPct === 100 ? "success" : "primary"} + /> + + )}
)} - -
- {user && - (questSub ? ( - - ) : ( - - ))} -
} > {show && - quest.questData.map((e, i) => ( + quest.questData.map((section, i) => ( - - + + {i < quest.questData.length - 1 && } ))} {collapsible && (
setShow(!show)} + style={{ cursor: "pointer" }} >
@@ -161,35 +158,138 @@ function QuestSectionComponent({ goals: Map; section: QuestSection; } & GamePT) { + const { goalSubs } = useContext(TargetsContext); + + const achievedInSection = section.goals.filter((g) => goalSubs.get(g.goalID)?.achieved).length; + const totalInSection = section.goals.length; + return ( -
-
{section.title}
- {section.desc &&
{section.desc}
} -
+
+ {/* Section header with mini-progress badge */} +
+
+ {section.title} +
+ {totalInSection > 0 && ( + + {achievedInSection} / {totalInSection} + + )} +
+ {section.desc && ( +

{section.desc}

+ )} +
+ {section.goals.length === 0 ? ( - No Goals... + No Goals… ) : ( -
- {section.goals.map((e, i) => { - const goal = goals.get(e.goalID); +
+ {section.goals.map((goalRef, i) => { + const goal = goals.get(goalRef.goalID); if (!goal) { return ( -
- Unknown goal '{e.goalID}'. This should never happen. +
+ Unknown goal '{goalRef.goalID}'
); } return ( -
- -
+ ); })}
)} -
+
+ ); +} + +/** A richer goal card showing progress, chart context, and an optional edit button. */ +function GoalCard({ + goal, + note, + game: _game, + onEdit, +}: { + goal: GoalDocument; + note?: string; + onEdit?: () => void; +} & GamePT) { + const { goalSubs } = useContext(TargetsContext); + const goalSub = goalSubs.get(goal.goalID); + + const isAchieved = goalSub?.achieved ?? false; + const inProgress = goalSub && !isAchieved && goalSub.progress !== null && goalSub.progress > 0; + + return ( +
+ {/* Status icon */} +
+ {goalSub ? ( + +
+ {isAchieved ? ( + + ) : inProgress ? ( + + ) : ( + + )} +
+
+ ) : ( + + )} +
+ + {/* Goal name + note */} +
+
+ + {onEdit && ( + + )} +
+ {note && {note}} +
+ + {/* Progress chip — only shown when subscribed and not achieved */} + {goalSub && !isAchieved && ( +
+ + {goalSub.progressHuman} + + / {goalSub.outOfHuman} +
+ )}
); } @@ -199,11 +299,13 @@ export function InnerQuestSectionGoal({ note, dependencies, goalSubOverride, + onEdit, }: { dependencies?: string[]; goal: GoalDocument; goalSubOverride?: GoalSubscriptionDocument; note?: string; + onEdit?: () => void; }) { const { goalSubs } = useContext(TargetsContext); @@ -212,57 +314,72 @@ export function InnerQuestSectionGoal({ if (!goalSub) { return ( <> -
-
- -
- +
+ + {onEdit && ( + + )}
{note && {note}} ); } + const isAchieved = goalSub.achieved; + const inProgress = !isAchieved && goalSub.progress !== null && goalSub.progress > 0; + return ( <> -
+
- {goalSub.achieved ? ( - + {isAchieved ? ( + + ) : inProgress ? ( + ) : ( - + )}
- {!goalSub.achieved && ( -
- {goalSub.progressHuman} + {onEdit && ( + + )} + + {!isAchieved && ( +
+ + {goalSub.progressHuman} + / {goalSub.outOfHuman}
)} @@ -282,8 +399,9 @@ export function InnerQuestSectionGoal({ function FormatGoalDependencies({ deps, isStandalone }: { deps: string[]; isStandalone: boolean }) { let str; + if (isStandalone && deps.length === 0) { - str = `You set this goal directly.`; + str = "You set this goal directly."; } else if (isStandalone && deps.length > 0) { str = `You set this goal, but it's also in ${HumanisedJoinArray(deps, "and")}.`; } else if (deps.length === 0) { diff --git a/typescript/client/src/components/targets/quests/editor/EditableQuest.tsx b/typescript/client/src/components/targets/quests/editor/EditableQuest.tsx index 0dca224ea..b1f19f3bd 100644 --- a/typescript/client/src/components/targets/quests/editor/EditableQuest.tsx +++ b/typescript/client/src/components/targets/quests/editor/EditableQuest.tsx @@ -1,6 +1,6 @@ import QuickTooltip from "#components/layout/misc/QuickTooltip"; import Card from "#components/layout/page/Card"; -import AddNewGoalForQuestModal from "#components/targets/AddNewGoalForQuestModal"; +import GoalBuilder from "#components/targets/GoalBuilder"; import Divider from "#components/util/Divider"; import EditableText from "#components/util/EditableText"; import Icon from "#components/util/Icon"; @@ -9,7 +9,7 @@ import { type GamePT } from "#types/react"; import { type RawQuestDocument, type RawQuestGoal, type RawQuestSection } from "#types/tachi"; import { ChangeAtPosition, CopyToClipboard, DeleteInPosition } from "#util/misc"; import React, { useState } from "react"; -import { Button } from "react-bootstrap"; +import { Button, Collapse, Modal } from "react-bootstrap"; import { FormatGame } from "tachi-common"; export default function EditableQuest({ @@ -26,35 +26,23 @@ export default function EditableQuest({ header={
- onChange({ - ...quest, - name, - }) - } + onSubmit={(name) => onChange({ ...quest, name })} placeholderText={quest.name || "Untitled Quest"} /> - - onChange({ - ...quest, - desc, - }) - } + onSubmit={(desc) => onChange({ ...quest, desc })} placeholderText={quest.desc || "Please set a description."} /> - - Game: {FormatGame(quest.game)} + {FormatGame(quest.game)}
} > - {quest.rawQuestData.map((e, i) => ( + {quest.rawQuestData.map((section, i) => ( - onChange({ - ...quest, - rawQuestData: DeleteInPosition(quest.rawQuestData, i), - }) + onChange({ ...quest, rawQuestData: DeleteInPosition(quest.rawQuestData, i) }) } - section={e} + onMoveDown={ + i < quest.rawQuestData.length - 1 + ? () => { + const arr = [...quest.rawQuestData]; + [arr[i], arr[i + 1]] = [arr[i + 1]!, arr[i]!]; + onChange({ ...quest, rawQuestData: arr }); + } + : undefined + } + onMoveUp={ + i > 0 + ? () => { + const arr = [...quest.rawQuestData]; + [arr[i - 1], arr[i]] = [arr[i]!, arr[i - 1]!]; + onChange({ ...quest, rawQuestData: arr }); + } + : undefined + } + section={section} /> ))} +
+ -
-
- - - -
+ +
+ + +
+ +
-
+ {section.rawGoals.length === 0 ? ( - No Goals... + No goals yet. ) : ( -
    - {section.rawGoals.map((e, i) => ( - + {section.rawGoals.map((rawGoal, i) => ( + - onChange({ - ...section, - rawGoals: ChangeAtPosition(section.rawGoals, newRawGoal, i), - }) - } - onInnerGoalDelete={() => { + onDelete={() => onChange({ ...section, rawGoals: DeleteInPosition(section.rawGoals, i), - }); - }} - rawGoal={e} + }) + } + onMoveDown={ + i < section.rawGoals.length - 1 + ? () => { + const arr = [...section.rawGoals]; + [arr[i], arr[i + 1]] = [arr[i + 1]!, arr[i]!]; + onChange({ ...section, rawGoals: arr }); + } + : undefined + } + onMoveUp={ + i > 0 + ? () => { + const arr = [...section.rawGoals]; + [arr[i - 1], arr[i]] = [arr[i]!, arr[i - 1]!]; + onChange({ ...section, rawGoals: arr }); + } + : undefined + } + onUpdate={(newGoal) => + onChange({ + ...section, + rawGoals: ChangeAtPosition(section.rawGoals, newGoal, i), + }) + } + rawGoal={rawGoal} /> ))} -
+
)} -
-
-
- {show && ( - { - onChange({ - ...section, - rawGoals: [...section.rawGoals, rawGoal], - }); - }} - setShow={setShow} - show={show} - /> - )} - + + {/* GoalBuilder modal */} + setShowGoalBuilder(false)} show={showGoalBuilder} size="xl"> + + Add Goal to "{section.title}" + + + { + onChange({ ...section, rawGoals: [...section.rawGoals, rawGoal] }); + setShowGoalBuilder(false); + }} + showNote + /> + + +
); } -function InnerQuestSectionGoal({ +function EditableGoalRow({ rawGoal, game, - onInnerGoalChange, - onInnerGoalDelete, + onUpdate, + onDelete, + onMoveUp, + onMoveDown, }: { - onInnerGoalChange: (newRawGoal: RawQuestGoal) => void; - onInnerGoalDelete: () => void; + onDelete: () => void; + onMoveDown?: () => void; + onMoveUp?: () => void; + onUpdate: (newGoal: RawQuestGoal) => void; rawGoal: RawQuestGoal; } & GamePT) { - const [show, setShow] = useState(false); + const [showEdit, setShowEdit] = useState(false); return ( -
  • -
    -
    {rawGoal.goal.name}
    +
    + + {rawGoal.goal.name} + {rawGoal.note && {rawGoal.note}} -
    -
    - setShow(true)} type="pencil-alt" /> -
    -
    - { - if ( - confirm( - `Are you sure you want to remove the goal "${rawGoal.goal.name}"?`, - ) - ) { - onInnerGoalDelete(); - } - }} - type="trash" - /> -
    -
    -
    - {rawGoal.note && {rawGoal.note}} - {show && ( - { - onInnerGoalChange(newRawGoal); +
    + + + + +
    + + {showEdit && ( + setShowEdit(false)} show size="xl"> + + Edit Goal + + + { + onUpdate(updated); + setShowEdit(false); + }} + showNote + /> + + )} -
  • +
    ); } -function FormatQuest(quest: RawQuestDocument) { - let str = `# QUEST: ${quest.name} -${quest.desc} -(Game: ${FormatGame(quest.game)})`; +function FormatQuestAsText(quest: RawQuestDocument): string { + let str = `# ${quest.name}\n${quest.desc}\n(${FormatGame(quest.game)})`; for (const section of quest.rawQuestData) { str += `\n\n### ${section.title}`; @@ -295,10 +363,10 @@ ${quest.desc} str += "\n"; for (const goal of section.rawGoals) { - str += `\n-- ${goal.goal.name}`; + str += `\n- ${goal.goal.name}`; if (goal.note) { - str += `\n${goal.note}`; + str += `\n ${goal.note}`; } } } diff --git a/typescript/client/src/types/tachi.ts b/typescript/client/src/types/tachi.ts index 04e9097ee..ad3fc0434 100644 --- a/typescript/client/src/types/tachi.ts +++ b/typescript/client/src/types/tachi.ts @@ -71,3 +71,18 @@ export type RawQuestGoal = { goal: Pick; note?: string; }; + +/** + * A questline document without a server-assigned `questlineID`. Used when + * authoring questlines locally in the Quest Editor before uploading as seeds. + */ +export type RawQuestlineDocument = { + /** User-chosen slug / identifier (becomes `questlineID` in seeds). */ + questlineID: string; + name: string; + desc: string; + game: string; + playtype: string; + /** Ordered list of `RawQuestDocument` references by their local index or slug. */ + quests: Array; +}; diff --git a/typescript/seeds-scripts/rerunners/v3/7-remap-goals-folder-and-chart-ids.ts b/typescript/seeds-scripts/rerunners/v3/7-remap-goals-folder-and-chart-ids.ts new file mode 100644 index 000000000..e8418692b --- /dev/null +++ b/typescript/seeds-scripts/rerunners/v3/7-remap-goals-folder-and-chart-ids.ts @@ -0,0 +1,291 @@ +/** + * After charts + folders gained Postgres-style ids (`id` plus `legacyChartID` / + * `legacyFolderID`), `goals.json` may still reference legacy chart hashes and legacy + * folder ids inside `charts.data`. Remap those references, recalculate `goalID` hashes + * (same `{ charts, criteria, game }` convention as ../../util CreateGoalID), rewrite + * `quests.json` goal refs when ids change, and write `goal-id-remap.json` so + * mongo→postgres can translate subscriptions. + */ + +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { spawnSync } from "child_process"; + +import { CreateGoalID } from "../../util"; + +type GoalCharts = + | { data: Array; type: "multi" } + | { data: string; folderSlug?: string; type: "folder" } + | { data: string; type: "single" }; + +interface GoalSeed { + charts: GoalCharts; + criteria: + | { + countNum: number; + key: string; + mode: "absolute" | "proportion"; + value: number; + } + | { + key: string; + mode: "single"; + value: number; + }; + game: string; + goalID: string; + name: string; + playtype: string; +} + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SEEDS_DIR = path.join(__dirname, "../../../../db/seeds"); + +const LEGACY_CHART_HEX_RE = /^[0-9a-f]{40}$/u; + +function readJsonArray(filePath: string): Array { + return JSON.parse(fs.readFileSync(filePath, "utf8")) as Array; +} + +function buildChartLegacyToIdMaps(): { + canonicalChartIds: Set; + legacyToSeedId: Map; +} { + const legacyToSeedId = new Map(); + const canonicalChartIds = new Set(); + + for (const name of fs.readdirSync(SEEDS_DIR)) { + if (!/^charts-/u.test(name) || !name.endsWith(".json")) { + continue; + } + + const charts = readJsonArray<{ id: string; legacyChartID?: string }>( + path.join(SEEDS_DIR, name), + ); + + for (const c of charts) { + canonicalChartIds.add(c.id); + const leg = c.legacyChartID; + + if (leg) { + legacyToSeedId.set(leg.toLowerCase(), c.id); + } + } + } + + return { canonicalChartIds, legacyToSeedId }; +} + +function buildFolderLegacyToIdMaps(): { + canonicalFolderIds: Set; + legacyToSeedId: Map; +} { + const legacyToSeedId = new Map(); + const canonicalFolderIds = new Set(); + const folders = readJsonArray<{ + id: string; + legacyFolderID: string; + }>(path.join(SEEDS_DIR, "folders.json")); + + for (const f of folders) { + canonicalFolderIds.add(f.id); + + const leg = f.legacyFolderID; + + if (leg !== undefined && leg !== "") { + legacyToSeedId.set(leg.trim(), f.id); + } + } + + return { canonicalFolderIds, legacyToSeedId }; +} + +function resolveChartGoalRef( + candidate: string, + legacyToSeedId: Map, + canonicalChartIds: Set, + context: string, + legacyHexRe: RegExp, +): string { + const t = candidate.trim(); + const lowered = t.toLowerCase(); + + if (canonicalChartIds.has(t)) { + return t; + } + + const migrated = legacyToSeedId.get(lowered); + + if (migrated !== undefined) { + return migrated; + } + + if (!legacyHexRe.test(t)) { + console.warn( + `${context}: chart ref ${JSON.stringify(t)} not in canonical set and not a known legacy SHA — leaving as-is`, + ); + + return t; + } + + throw new Error(`${context}: unknown legacy chart SHA ${JSON.stringify(t)}`); +} + +function resolveFolderGoalRef( + candidate: string, + legacyToSeedId: Map, + canonicalFolderIds: Set, + context: string, +): string { + const t = candidate.trim(); + + if (canonicalFolderIds.has(t)) { + return t; + } + + const migrated = legacyToSeedId.get(t); + + if (migrated !== undefined) { + return migrated; + } + + throw new Error(`${context}: unknown folder reference ${JSON.stringify(t)}`); +} + +function remapChartsBlock( + charts: GoalCharts, + context: string, + legacyHexRe: RegExp, + legacyToSeedChart: Map, + canonicalChartIds: Set, + legacyToSeedFolder: Map, + canonicalFolderIds: Set, +): GoalCharts { + switch (charts.type) { + case "single": + return { + ...charts, + data: resolveChartGoalRef( + charts.data, + legacyToSeedChart, + canonicalChartIds, + context, + legacyHexRe, + ), + }; + case "multi": + return { + ...charts, + data: charts.data.map((cid, i) => + resolveChartGoalRef( + cid, + legacyToSeedChart, + canonicalChartIds, + `${context} [multi ${i}]`, + legacyHexRe, + ), + ), + }; + case "folder": + return { + ...charts, + data: resolveFolderGoalRef(charts.data, legacyToSeedFolder, canonicalFolderIds, context), + }; + } +} + +const translateMap = new Map(); + +const goalsPath = path.join(SEEDS_DIR, "goals.json"); +const goals = readJsonArray(goalsPath); + +const { canonicalChartIds, legacyToSeedId: chartLegacy } = buildChartLegacyToIdMaps(); +const { canonicalFolderIds, legacyToSeedId: folderLegacy } = buildFolderLegacyToIdMaps(); + +for (const goal of goals) { + const oldId = goal.goalID; + + goal.charts = remapChartsBlock( + goal.charts, + `goal ${JSON.stringify(goal.name)} (${oldId})`, + LEGACY_CHART_HEX_RE, + chartLegacy, + canonicalChartIds, + folderLegacy, + canonicalFolderIds, + ); + + const newId = CreateGoalID(goal.charts, goal.criteria, goal.game); + + goal.goalID = newId; + + if (newId !== oldId) { + translateMap.set(oldId, newId); + } +} + +console.log(`goals.json: remapped chart/folder refs; ${translateMap.size} goal IDs will change`); + +fs.writeFileSync(goalsPath, `${JSON.stringify(goals, null, "\t")}\n`); + +type QuestSeed = { + desc: string; + game: string; + name: string; + playtype: string; + questData: Array<{ + desc?: string; + goals: Array<{ goalID: string; note?: string }>; + title: string; + }>; + questID: string; +}; + +const questsPath = path.join(SEEDS_DIR, "quests.json"); +const quests = readJsonArray(questsPath); + +let questsUpdated = 0; + +for (const quest of quests) { + let touched = false; + + for (const qd of quest.questData) { + for (let i = 0; i < qd.goals.length; i++) { + const gRef = qd.goals[i]; + const nextId = translateMap.get(gRef.goalID); + + if (nextId !== undefined && nextId !== gRef.goalID) { + qd.goals[i] = { ...gRef, goalID: nextId }; + touched = true; + } + } + } + + if (touched) { + questsUpdated++; + } +} + +fs.writeFileSync(questsPath, `${JSON.stringify(quests, null, "\t")}\n`); +console.log(`quests.json: updated goal refs on ${questsUpdated} quests`); + +const remapPath = path.join(SEEDS_DIR, "goal-id-remap.json"); +fs.writeFileSync(remapPath, `${JSON.stringify(Object.fromEntries(translateMap), null, "\t")}\n`); + +console.log(`goal-id-remap.json: wrote ${translateMap.size} translations`); + +const repoRoot = path.resolve(SEEDS_DIR, "..", ".."); +const biome = path.join(repoRoot, "node_modules", ".bin", "biome"); +const biomeResult = spawnSync( + biome, + ["format", "--write", goalsPath, questsPath, remapPath], + { + cwd: repoRoot, + stdio: "inherit", + }, +); + +if (biomeResult.status !== 0) { + throw new Error(`biome format exited ${biomeResult.status}`); +} diff --git a/typescript/seeds-scripts/sort-seeds.js b/typescript/seeds-scripts/sort-seeds.js index 7feca09ea..11e685f83 100644 --- a/typescript/seeds-scripts/sort-seeds.js +++ b/typescript/seeds-scripts/sort-seeds.js @@ -77,6 +77,11 @@ function SortSeeds() { const collPath = path.join(collectionsDir, collection); let content = JSON.parse(fs.readFileSync(collPath)); + // Auxiliary maps (non-array roots) coexist with seed collections — skip them. + if (!Array.isArray(content)) { + continue; + } + if (collection.startsWith("charts-")) { content.sort(ChartSort); } else if (collection.startsWith("songs-")) { diff --git a/typescript/seeds-scripts/test/collections.test.ts b/typescript/seeds-scripts/test/collections.test.ts index 743a123d8..afd9c19f0 100644 --- a/typescript/seeds-scripts/test/collections.test.ts +++ b/typescript/seeds-scripts/test/collections.test.ts @@ -20,9 +20,12 @@ function FormatPrError(err, foreword = "Error") { let exitCode = 0; const suites: Array<{ good: boolean; name: string; report: unknown }> = []; +/** Maps / metadata — not seed document arrays validated by {@link V3_SCHEMAS}. */ +const NON_SEED_JSON = new Set(["goal-id-remap.json"]); + const collections = fs .readdirSync(path.join(__dirname, "../../../db/seeds")) - .filter((e) => e.endsWith(".json")) + .filter((e) => e.endsWith(".json") && !NON_SEED_JSON.has(path.basename(e))) .map((e) => path.basename(e)) as Array; for (const collection of collections) { diff --git a/typescript/server/src/actions/update-goal-subscription.test.ts b/typescript/server/src/actions/update-goal-subscription.test.ts new file mode 100644 index 000000000..4be0f1101 --- /dev/null +++ b/typescript/server/src/actions/update-goal-subscription.test.ts @@ -0,0 +1,175 @@ +import DB from "#services/pg/db"; +import { seedMinimalIidxSpChart, seedUser } from "#test-utils/pg-fixtures"; +import { GetGoalForIDGuaranteed, GetGoalSubscriptionForIDGuaranteed } from "#utils/db"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { ACTION_AddGoal } from "./add-goal"; +import { ACTION_UpdateGoalSubscription } from "./update-goal-subscription"; + +describe("ACTION_UpdateGoalSubscription", () => { + let userID: number; + let username: string; + let chartId: string; + let taker: { ip: string; acct: { id: number; username: string } }; + + const baseInput = { + game: "iidx-sp" as const, + charts: { type: "single" as const, data: "" }, + criteria: { key: "lamp" as const, value: 7, mode: "single" as const }, + }; + + beforeEach(async () => { + ({ id: userID, username } = await seedUser({ username: `goal_update_${Date.now()}` })); + chartId = await seedMinimalIidxSpChart(); + taker = { ip: "127.0.0.1", acct: { id: userID, username } }; + }); + + it("throws 403 when targeting another user as non-admin", async () => { + const other = await seedUser({ username: `goal_upd_other_${Date.now()}` }); + + await expect( + ACTION_UpdateGoalSubscription(taker, { + ...baseInput, + userID: other.id, + charts: { type: "single", data: chartId }, + oldGoalID: "G_fake", + }), + ).rejects.toMatchObject({ code: 403 }); + }); + + it("throws 404 when the user is not subscribed to the old goal", async () => { + await expect( + ACTION_UpdateGoalSubscription(taker, { + ...baseInput, + userID, + charts: { type: "single", data: chartId }, + oldGoalID: "G_does_not_exist", + }), + ).rejects.toMatchObject({ code: 404 }); + }); + + it("throws 400 when the goal was assigned by a quest (not standalone)", async () => { + const { goalID } = await ACTION_AddGoal(taker, { + ...baseInput, + userID, + charts: { type: "single", data: chartId }, + }); + + await DB.updateTable("goal_sub") + .set({ was_assigned_standalone: false }) + .where("goal_sub.goal_id", "=", goalID) + .where("goal_sub.user_id", "=", userID) + .execute(); + + const secondChart = await seedMinimalIidxSpChart(); + + await expect( + ACTION_UpdateGoalSubscription(taker, { + ...baseInput, + userID, + oldGoalID: goalID, + charts: { type: "single", data: secondChart }, + }), + ).rejects.toMatchObject({ code: 400 }); + }); + + it("returns changed: false when the new definition is identical to the old one", async () => { + const { goalID } = await ACTION_AddGoal(taker, { + ...baseInput, + userID, + charts: { type: "single", data: chartId }, + }); + + const result = await ACTION_UpdateGoalSubscription(taker, { + ...baseInput, + userID, + oldGoalID: goalID, + charts: { type: "single", data: chartId }, + }); + + expect(result).toMatchObject({ changed: false, newGoalID: goalID }); + + const sub = await GetGoalSubscriptionForIDGuaranteed(goalID, userID); + expect(sub).toBeDefined(); + }); + + it("swaps subscription from old goal to new goal", async () => { + const { goalID: oldGoalID } = await ACTION_AddGoal(taker, { + ...baseInput, + userID, + charts: { type: "single", data: chartId }, + }); + + const secondChart = await seedMinimalIidxSpChart(); + + const result = await ACTION_UpdateGoalSubscription(taker, { + ...baseInput, + userID, + oldGoalID, + charts: { type: "single", data: secondChart }, + }); + + expect(result.changed).toBe(true); + expect(result.newGoalID).not.toBe(oldGoalID); + + const newGoal = await GetGoalForIDGuaranteed(result.newGoalID); + expect(newGoal.charts).toMatchObject({ type: "single", data: secondChart }); + + const newSub = await GetGoalSubscriptionForIDGuaranteed(result.newGoalID, userID); + expect(newSub).toBeDefined(); + + const oldSub = await DB.selectFrom("goal_sub") + .selectAll() + .where("goal_sub.goal_id", "=", oldGoalID) + .where("goal_sub.user_id", "=", userID) + .executeTakeFirst(); + + expect(oldSub).toBeUndefined(); + }); + + it("writes a GOOD action row on success", async () => { + const { goalID: oldGoalID } = await ACTION_AddGoal(taker, { + ...baseInput, + userID, + charts: { type: "single", data: chartId }, + }); + + const secondChart = await seedMinimalIidxSpChart(); + + await ACTION_UpdateGoalSubscription(taker, { + ...baseInput, + userID, + oldGoalID, + charts: { type: "single", data: secondChart }, + }); + + const actionRow = await DB.selectFrom("action") + .selectAll() + .where("kind", "=", "UPDATE_GOAL_SUBSCRIPTION") + .orderBy("action.ts_start", "desc") + .executeTakeFirstOrThrow(); + + expect(actionRow).toMatchObject({ + result: "GOOD", + ip: "127.0.0.1", + user_id: userID, + }); + }); + + it("throws 400 when the new chart does not exist", async () => { + const { goalID: oldGoalID } = await ACTION_AddGoal(taker, { + ...baseInput, + userID, + charts: { type: "single", data: chartId }, + }); + + await expect( + ACTION_UpdateGoalSubscription(taker, { + ...baseInput, + userID, + oldGoalID, + charts: { type: "single", data: "C_fake_chart" }, + }), + ).rejects.toMatchObject({ code: 400 }); + }); +}); diff --git a/typescript/server/src/actions/update-goal-subscription.ts b/typescript/server/src/actions/update-goal-subscription.ts new file mode 100644 index 000000000..c1ea2a9c1 --- /dev/null +++ b/typescript/server/src/actions/update-goal-subscription.ts @@ -0,0 +1,73 @@ +import { MakeAction } from "#lib/actions/actions"; +import { SubscribeFailReasons } from "#lib/constants/err-codes"; +import { SELECT_GOAL_SUB_WITH_GOAL_GAME } from "#lib/db-formats/goal"; +import { ToGoalSubscriptionDocument } from "#lib/db-formats/target-documents"; +import { ConstructGoal, SubscribeToGoal } from "#lib/targets/goals"; +import DB from "#services/pg/db"; +import { IsUserAdmin } from "#utils/user"; +import { ExpectedErr } from "bliss"; +import { type GoalDocument } from "tachi-common"; + +export const ACTION_UpdateGoalSubscription = MakeAction( + "UPDATE_GOAL_SUBSCRIPTION", + async (taker, input) => { + const { userID, game, oldGoalID, charts, criteria } = input; + + if (taker.acct.id !== userID && !(await IsUserAdmin(taker.acct.id))) { + throw new ExpectedErr(403, "You are not authorised to modify this user's goals."); + } + + const existingSubRow = await DB.selectFrom("goal_sub") + .innerJoin("goal", "goal.id", "goal_sub.goal_id") + .select(SELECT_GOAL_SUB_WITH_GOAL_GAME) + .where("goal_sub.user_id", "=", userID) + .where("goal_sub.goal_id", "=", oldGoalID) + .where("goal.game", "=", game) + .executeTakeFirst(); + + if (!existingSubRow) { + throw new ExpectedErr(404, "You are not subscribed to this goal."); + } + + const existingSub = ToGoalSubscriptionDocument(existingSubRow); + + if (!existingSub.wasAssignedStandalone) { + throw new ExpectedErr( + 400, + "This goal was assigned by a quest and cannot be updated directly. Unsubscribe from the quest to modify its goals.", + ); + } + + let newGoal: GoalDocument; + + try { + newGoal = await ConstructGoal( + charts as GoalDocument["charts"], + criteria as GoalDocument["criteria"], + game, + ); + } catch (e) { + throw new ExpectedErr(400, (e as Error).message); + } + + if (newGoal.goalID === oldGoalID) { + return { newGoalID: oldGoalID, changed: false }; + } + + await DB.deleteFrom("goal_sub") + .where("goal_sub.goal_id", "=", oldGoalID) + .where("goal_sub.user_id", "=", userID) + .execute(); + + const subResult = await SubscribeToGoal(userID, newGoal, true); + + if ( + subResult === SubscribeFailReasons.ALREADY_SUBSCRIBED || + subResult === SubscribeFailReasons.ALREADY_ACHIEVED + ) { + throw new ExpectedErr(409, "Your updated goal conflicts with an existing subscription."); + } + + return { newGoalID: newGoal.goalID, changed: true }; + }, +); diff --git a/typescript/server/src/lib/actions/actions.ts b/typescript/server/src/lib/actions/actions.ts index 1dfd8605d..bc541ff2b 100644 --- a/typescript/server/src/lib/actions/actions.ts +++ b/typescript/server/src/lib/actions/actions.ts @@ -349,6 +349,19 @@ export const ActionSignatures = { }), output: z.object({}), }, + UPDATE_GOAL_SUBSCRIPTION: { + input: z.object({ + userID: z.number().int(), + game: z.enum(ALL_GAMES), + oldGoalID: z.string(), + charts: z.any(), + criteria: z.any(), + }), + output: z.object({ + newGoalID: z.string(), + changed: z.boolean(), + }), + }, UPDATE_UGPT_SHOWCASE: { input: z.object({ userID: z.number().int(), diff --git a/typescript/server/src/lib/targets/quests.ts b/typescript/server/src/lib/targets/quests.ts index 22d3bbe9d..9176af625 100644 --- a/typescript/server/src/lib/targets/quests.ts +++ b/typescript/server/src/lib/targets/quests.ts @@ -50,6 +50,10 @@ export function GetGoalIDsFromQuest(quest: QuestDocument) { export async function GetGoalsInQuest(quest: QuestDocument) { const goalIDs = GetGoalIDsFromQuest(quest); + if (goalIDs.length === 0) { + return []; + } + const goals = await DB.selectFrom("goal") .select(SELECT_GOAL) .where("goal.id", "in", goalIDs) @@ -124,7 +128,7 @@ export async function EvaluateQuestProgress(userID: integer, quest: QuestDocumen const goalSubMap = new Map(); - if (isSubscribedToQuest) { + if (isSubscribedToQuest && goals.length > 0) { const goalSubRows = await DB.selectFrom("goal_sub") .innerJoin("goal", "goal.id", "goal_sub.goal_id") .select(SELECT_GOAL_SUB_WITH_GOAL_GAME) @@ -382,16 +386,18 @@ export async function UnsubscribeFromQuest( .where("quest_sub.user_id", "=", questSub.userID) .execute(); - const goalSubRows = await DB.selectFrom("goal_sub") - .innerJoin("goal", "goal.id", "goal_sub.goal_id") - .select(SELECT_GOAL_SUB_WITH_GOAL_GAME) - .where("goal_sub.user_id", "=", questSub.userID) - .where("goal_sub.goal_id", "in", goalIDs) - .execute(); + if (goalIDs.length > 0) { + const goalSubRows = await DB.selectFrom("goal_sub") + .innerJoin("goal", "goal.id", "goal_sub.goal_id") + .select(SELECT_GOAL_SUB_WITH_GOAL_GAME) + .where("goal_sub.user_id", "=", questSub.userID) + .where("goal_sub.goal_id", "in", goalIDs) + .execute(); - const goalSubs = goalSubRows.map((r) => ToGoalSubscriptionDocument(r)); + const goalSubs = goalSubRows.map((r) => ToGoalSubscriptionDocument(r)); - await Promise.all(goalSubs.map((e) => UnsubscribeFromGoal(e, true))); + await Promise.all(goalSubs.map((e) => UnsubscribeFromGoal(e, true))); + } } /** diff --git a/typescript/server/src/scripts/load-seeds-pg.ts b/typescript/server/src/scripts/load-seeds-pg.ts index 671861b43..bc2a3b22b 100644 --- a/typescript/server/src/scripts/load-seeds-pg.ts +++ b/typescript/server/src/scripts/load-seeds-pg.ts @@ -19,9 +19,9 @@ * promotes user #1 to admin so seed import can run. */ -import { buildChartIdMap, importSeeds, ImportSeedsSubsetForTests } from "../services/pg/seeds"; +import { buildChartIdMap, buildGoalIdRemap, importSeeds, ImportSeedsSubsetForTests } from "../services/pg/seeds"; -export { buildChartIdMap, importSeeds, ImportSeedsSubsetForTests as importSeedsSubset }; +export { buildChartIdMap, buildGoalIdRemap, importSeeds, ImportSeedsSubsetForTests as importSeedsSubset }; // ── Standalone entrypoint ────────────────────────────────────────────────── diff --git a/typescript/server/src/scripts/migrate-to-postgres.ts b/typescript/server/src/scripts/migrate-to-postgres.ts index db21952b0..6ad9fb9f5 100644 --- a/typescript/server/src/scripts/migrate-to-postgres.ts +++ b/typescript/server/src/scripts/migrate-to-postgres.ts @@ -104,7 +104,7 @@ import type { MongoUserSettingsCollectionDocument, } from "./migrate-to-postgres.mongo-docs"; -import { buildChartIdMap, importSeeds } from "./load-seeds-pg"; +import { buildChartIdMap, buildGoalIdRemap, importSeeds } from "./load-seeds-pg"; // ────────────────────────────────────────────────────────────────────────────── // Connection setup @@ -400,6 +400,8 @@ async function main(): Promise { // Used to resolve chart_id FK references in scores and PBs. const chartIdMap = buildChartIdMap(SEEDS_DIR); + const goalIdRemap = buildGoalIdRemap(SEEDS_DIR); + // ══════════════════════════════════════════════════════════════════════════ // LEVEL 0 - No FK dependencies // ══════════════════════════════════════════════════════════════════════════ @@ -1164,15 +1166,18 @@ async function main(): Promise { console.log("\n[goal_sub]"); const goalSubs = await mongoDB.get("goal-subs").find({}); - const uniqueGoalIds = [...new Set(goalSubs.map((gs) => gs.goalID))]; + const uniqueMongoGoalIds = [...new Set(goalSubs.map((gs) => gs.goalID))]; + const translatedIds = uniqueMongoGoalIds.map((gid) => goalIdRemap.get(gid) ?? gid); + const candidateGoalIdsForLookup = [...new Set([...uniqueMongoGoalIds, ...translatedIds])]; + const existingGoalIds = new Set( - uniqueGoalIds.length === 0 + candidateGoalIdsForLookup.length === 0 ? [] : ( await pg .selectFrom("goal") .select("id") - .where("id", "in", uniqueGoalIds) + .where("id", "in", candidateGoalIdsForLookup) .execute() ).map((r) => r.id), ); @@ -1180,13 +1185,17 @@ async function main(): Promise { const goalSubRows: Array = []; for (const gs of goalSubs) { - if (!existingGoalIds.has(gs.goalID)) { - console.warn(` [goal_sub] Skipping - goal ${gs.goalID} not found in DB`); + const resolvedGoalId = goalIdRemap.get(gs.goalID) ?? gs.goalID; + + if (!existingGoalIds.has(resolvedGoalId)) { + console.warn( + ` [goal_sub] Skipping - goal ${gs.goalID} (resolved=${resolvedGoalId}) not found in DB`, + ); continue; } goalSubRows.push({ - goal_id: gs.goalID, + goal_id: resolvedGoalId, user_id: gs.userID, last_interaction: ts(gs.lastInteraction), progress: gs.progress, diff --git a/typescript/server/src/server/router/api/v1/games/_gameGroup/_playtype/targets/goals/router.test.ts b/typescript/server/src/server/router/api/v1/games/_gameGroup/_playtype/targets/goals/router.test.ts index 794ac27bb..772e6b0e5 100644 --- a/typescript/server/src/server/router/api/v1/games/_gameGroup/_playtype/targets/goals/router.test.ts +++ b/typescript/server/src/server/router/api/v1/games/_gameGroup/_playtype/targets/goals/router.test.ts @@ -1,5 +1,216 @@ -import { describe, it } from "vitest"; +import DB from "#services/pg/db"; +import mockApi, { CloseServerConnection } from "#test-utils/mock-api"; +import { seedMinimalIidxSpChart, seedUser } from "#test-utils/pg-fixtures"; +import { afterAll, describe, expect, it } from "vitest"; -describe("Game-group targets goals", () => { - it.todo("port router.oldtest.ts"); +afterAll(() => CloseServerConnection()); + +async function seedGoalWithSub(chartId: string, userId: number) { + const goalId = `G_test_${Date.now()}_${Math.random().toString(36).slice(2)}`; + + await DB.insertInto("goal") + .values({ + id: goalId, + game: "iidx-sp", + name: "Test Goal", + charts: JSON.stringify({ type: "single", data: chartId }), + criteria: JSON.stringify({ mode: "single", key: "lamp", value: 7 }), + }) + .execute(); + + await DB.insertInto("goal_sub") + .values({ + goal_id: goalId, + user_id: userId, + achieved: false, + time_achieved: null, + progress: null, + progress_human: "NO DATA", + out_of: 7, + out_of_human: "FULL COMBO", + last_interaction: null, + was_instantly_achieved: false, + was_assigned_standalone: true, + }) + .execute(); + + return goalId; +} + +describe("GET /api/v1/games/:game/targets/goals/popular", () => { + it("returns 200 with an array of goals", async () => { + const res = await mockApi.get("/api/v1/games/iidx-sp/targets/goals/popular"); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(Array.isArray(res.body.body)).toBe(true); + }); + + it("returns the most-subscribed goals sorted by popularity", async () => { + const chartId = await seedMinimalIidxSpChart(); + const goalId = `G_pop_${Date.now()}`; + + await DB.insertInto("goal") + .values({ + id: goalId, + game: "iidx-sp", + name: "Popular Goal", + charts: JSON.stringify({ type: "single", data: chartId }), + criteria: JSON.stringify({ mode: "single", key: "lamp", value: 7 }), + }) + .execute(); + + const { id: uid1 } = await seedUser({ username: `pop_u1_${Date.now()}` }); + const { id: uid2 } = await seedUser({ username: `pop_u2_${Date.now()}` }); + + for (const uid of [uid1, uid2]) { + await DB.insertInto("goal_sub").values({ + goal_id: goalId, + user_id: uid, + achieved: false, + time_achieved: null, + progress: null, + progress_human: "NO DATA", + out_of: 7, + out_of_human: "FULL COMBO", + last_interaction: null, + was_instantly_achieved: false, + was_assigned_standalone: true, + }).execute(); + } + + const res = await mockApi.get("/api/v1/games/iidx-sp/targets/goals/popular"); + + expect(res.status).toBe(200); + + const goalIds = (res.body.body as Array<{ goalID: string }>).map((g) => g.goalID); + + expect(goalIds).toContain(goalId); + }); +}); + +describe("POST /api/v1/games/:game/targets/goals/format", () => { + it("returns a human-readable goal name for valid input", async () => { + const chartId = await seedMinimalIidxSpChart(); + + const res = await mockApi + .post("/api/v1/games/iidx-sp/targets/goals/format") + .send({ + charts: { type: "single", data: chartId }, + criteria: { mode: "single", key: "lamp", value: 7 }, + }); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(typeof res.body.body).toBe("string"); + expect(res.body.body.length).toBeGreaterThan(0); + }); + + it("returns 400 for an invalid chart reference", async () => { + const res = await mockApi + .post("/api/v1/games/iidx-sp/targets/goals/format") + .send({ + charts: { type: "single", data: "C_does_not_exist" }, + criteria: { mode: "single", key: "lamp", value: 7 }, + }); + + expect(res.status).toBe(400); + expect(res.body.success).toBe(false); + }); + + it("returns 400 for an invalid criteria key", async () => { + const chartId = await seedMinimalIidxSpChart(); + + const res = await mockApi + .post("/api/v1/games/iidx-sp/targets/goals/format") + .send({ + charts: { type: "single", data: chartId }, + criteria: { mode: "single", key: "not_a_real_metric", value: 0 }, + }); + + expect(res.status).toBe(400); + expect(res.body.success).toBe(false); + }); +}); + +describe("GET /api/v1/games/:game/targets/goals/:goalID", () => { + it("returns goal + subscribers + parentQuests", async () => { + const chartId = await seedMinimalIidxSpChart(); + const { id: detailUserId } = await seedUser({ username: `goal_detail_${Date.now()}` }); + const goalId = await seedGoalWithSub(chartId, detailUserId); + + const res = await mockApi.get(`/api/v1/games/iidx-sp/targets/goals/${goalId}`); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.body.goal.goalID).toBe(goalId); + expect(Array.isArray(res.body.body.goalSubs)).toBe(true); + expect(Array.isArray(res.body.body.users)).toBe(true); + expect(Array.isArray(res.body.body.parentQuests)).toBe(true); + }); + + it("returns 404 when the goal does not exist", async () => { + const res = await mockApi.get("/api/v1/games/iidx-sp/targets/goals/G_no_such_goal"); + + expect(res.status).toBe(404); + expect(res.body.success).toBe(false); + }); + + it("returns 404 when the goal belongs to a different game", async () => { + const chartId = await seedMinimalIidxSpChart(); + const { id: gameCheckUserId } = await seedUser({ username: `goal_game_check_${Date.now()}` }); + const goalId = await seedGoalWithSub(chartId, gameCheckUserId); + + const res = await mockApi.get(`/api/v1/games/sdvx/targets/goals/${goalId}`); + + expect(res.status).toBe(404); + }); + + it("parentQuestlines on quest detail returns proper QuestlineDocument shape", async () => { + const suffix = `${Date.now()}`; + const qlId = `ql-goal-test-${suffix}`; + const qId = `q-goal-test-${suffix}`; + const chartId = await seedMinimalIidxSpChart(); + const goalId = `G_ql_shape_${suffix}`; + + await DB.insertInto("goal").values({ + id: goalId, + game: "iidx-sp", + name: "Shape test goal", + charts: JSON.stringify({ type: "single", data: chartId }), + criteria: JSON.stringify({ mode: "single", key: "lamp", value: 4 }), + }).execute(); + + await DB.insertInto("quest").values({ + id: qId, + game: "iidx-sp", + name: "Shape test quest", + description: "d", + quest_data: JSON.stringify([{ title: "s", goals: [{ goalID: goalId }] }]), + }).execute(); + + await DB.insertInto("questline").values({ + id: qlId, + game: "iidx-sp", + name: "Shape test questline", + description: "d", + }).execute(); + + await DB.insertInto("questline_quest").values({ + questline_id: qlId, + quest_id: qId, + sort_order: 0, + }).execute(); + + const res = await mockApi.get(`/api/v1/games/iidx-sp/targets/quests/${qId}`); + + expect(res.status).toBe(200); + + const ql = (res.body.body.parentQuestlines as Array<{ questlineID: string; game: string; quests: string[] }>) + .find((x) => x.questlineID === qlId); + + expect(ql).toBeDefined(); + expect(ql?.game).toBe("iidx-sp"); + expect(Array.isArray(ql?.quests)).toBe(true); + }); }); diff --git a/typescript/server/src/server/router/api/v1/games/_gameGroup/_playtype/targets/quests/router.test.ts b/typescript/server/src/server/router/api/v1/games/_gameGroup/_playtype/targets/quests/router.test.ts index 198c3cb3f..bad58c294 100644 --- a/typescript/server/src/server/router/api/v1/games/_gameGroup/_playtype/targets/quests/router.test.ts +++ b/typescript/server/src/server/router/api/v1/games/_gameGroup/_playtype/targets/quests/router.test.ts @@ -1,5 +1,150 @@ -import { describe, it } from "vitest"; +import DB from "#services/pg/db"; +import mockApi, { CloseServerConnection } from "#test-utils/mock-api"; +import { seedMinimalIidxSpChart } from "#test-utils/pg-fixtures"; +import { afterAll, describe, expect, it } from "vitest"; -describe("Game-group targets quests", () => { - it.todo("port router.oldtest.ts"); +afterAll(() => CloseServerConnection()); + +async function seedQuest(suffix: string, goalId?: string) { + const questId = `q-rt-${suffix}`; + const questData = goalId + ? [{ title: "Section", goals: [{ goalID: goalId }] }] + : []; + + await DB.insertInto("quest") + .values({ + id: questId, + game: "iidx-sp", + name: `Quest ${suffix}`, + description: `Description for ${suffix}`, + quest_data: JSON.stringify(questData), + }) + .execute(); + + return questId; +} + +describe("GET /api/v1/games/:game/targets/quests", () => { + it("returns quests matching the search term", async () => { + const suffix = `${Date.now()}`; + const questId = await seedQuest(suffix); + + const res = await mockApi.get( + `/api/v1/games/iidx-sp/targets/quests?search=Quest+${suffix}`, + ); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + + const ids = (res.body.body.quests as Array<{ questID: string }>).map((q) => q.questID); + + expect(ids).toContain(questId); + }); + + it("returns empty arrays when no quests match the search", async () => { + const res = await mockApi.get( + "/api/v1/games/iidx-sp/targets/quests?search=this_will_never_match_zzz_xyz", + ); + + expect(res.status).toBe(200); + expect(res.body.body.quests).toHaveLength(0); + expect(res.body.body.goals).toHaveLength(0); + }); + + it("returns associated goals in the response", async () => { + const chartId = await seedMinimalIidxSpChart(); + const goalId = `G_q_search_${Date.now()}`; + const suffix = `${Date.now()}-g`; + + await DB.insertInto("goal").values({ + id: goalId, + game: "iidx-sp", + name: "Search goal", + charts: JSON.stringify({ type: "single", data: chartId }), + criteria: JSON.stringify({ mode: "single", key: "lamp", value: 4 }), + }).execute(); + + const questId = await seedQuest(suffix, goalId); + + const res = await mockApi.get( + `/api/v1/games/iidx-sp/targets/quests?search=Quest+${suffix}`, + ); + + expect(res.status).toBe(200); + + const questIds = (res.body.body.quests as Array<{ questID: string }>).map((q) => q.questID); + const goalIds = (res.body.body.goals as Array<{ goalID: string }>).map((g) => g.goalID); + + expect(questIds).toContain(questId); + expect(goalIds).toContain(goalId); + }); +}); + +describe("GET /api/v1/games/:game/targets/quests/:questID", () => { + it("returns quest + subscribers + goals + parentQuestlines", async () => { + const suffix = `${Date.now()}-d`; + const questId = await seedQuest(suffix); + + const res = await mockApi.get(`/api/v1/games/iidx-sp/targets/quests/${questId}`); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.body.quest.questID).toBe(questId); + expect(Array.isArray(res.body.body.questSubs)).toBe(true); + expect(Array.isArray(res.body.body.users)).toBe(true); + expect(Array.isArray(res.body.body.goals)).toBe(true); + expect(Array.isArray(res.body.body.parentQuestlines)).toBe(true); + }); + + it("returns 404 when quest does not exist", async () => { + const res = await mockApi.get("/api/v1/games/iidx-sp/targets/quests/q_no_such_quest"); + + expect(res.status).toBe(404); + expect(res.body.success).toBe(false); + }); + + it("returns 404 when quest belongs to a different game", async () => { + const suffix = `${Date.now()}-x`; + const questId = await seedQuest(suffix); + + const res = await mockApi.get(`/api/v1/games/sdvx/targets/quests/${questId}`); + + expect(res.status).toBe(404); + }); + + it("parentQuestlines entries have V3Game shape (not legacy gameGroup+playtype)", async () => { + const suffix = `${Date.now()}-v3`; + const qlId = `ql-v3-${suffix}`; + const questId = await seedQuest(suffix); + + await DB.insertInto("questline").values({ + id: qlId, + game: "iidx-sp", + name: "V3 test questline", + description: "d", + }).execute(); + + await DB.insertInto("questline_quest").values({ + questline_id: qlId, + quest_id: questId, + sort_order: 0, + }).execute(); + + const res = await mockApi.get(`/api/v1/games/iidx-sp/targets/quests/${questId}`); + + expect(res.status).toBe(200); + + const ql = ( + res.body.body.parentQuestlines as Array<{ questlineID: string; game: string; quests: string[] }> + ).find((x) => x.questlineID === qlId); + + expect(ql).toBeDefined(); + // game should be the V3Game string, not a legacy GameGroup + expect(ql?.game).toBe("iidx-sp"); + // must NOT have a top-level playtype field + expect((ql as any)?.playtype).toBeUndefined(); + // must carry the quests array + expect(Array.isArray(ql?.quests)).toBe(true); + expect(ql?.quests).toContain(questId); + }); }); diff --git a/typescript/server/src/server/router/api/v1/games/router.ts b/typescript/server/src/server/router/api/v1/games/router.ts index cd7f54eae..0b6ac216a 100644 --- a/typescript/server/src/server/router/api/v1/games/router.ts +++ b/typescript/server/src/server/router/api/v1/games/router.ts @@ -11,7 +11,6 @@ import { LoadPbsOnChartForUserSearch, } from "#lib/db-formats/pb"; import { SELECT_QUEST, SELECT_QUEST_SUB_WITH_QUEST_GAME } from "#lib/db-formats/quest"; -import { SELECT_QUESTLINE_ROW } from "#lib/db-formats/questline"; import { GetSongByID, GetSongsByIDs } from "#lib/db-formats/song"; import { GetTableDocumentsForGame, @@ -54,7 +53,11 @@ import { } from "#utils/db"; import { EscapeForILIKE, IsString } from "#utils/misc"; import { FindChartsOnPopularity } from "#utils/queries/charts"; -import { GetQuestlineById, GetQuestlinesForGame } from "#utils/queries/questlines"; +import { + GetQuestlineById, + GetQuestlinesForGame, + GetQuestlinesThatContainQuest, +} from "#utils/queries/questlines"; import { CheckStrProfileAlg, CheckStrScoreAlg, @@ -673,9 +676,11 @@ API_V1_ROUTER.add("GET /games/:game/targets/recently-raised", withGame, async ({ * @name GET /api/v1/games/:game/targets/goals/popular */ API_V1_ROUTER.add("GET /games/:game/targets/goals/popular", withGame, async ({ ctx }) => { - const { gameGroup: _gameGroup, playtype } = LEGACY_GameToGameGroupPT(ctx.game); + const { gameGroup, playtype } = LEGACY_GameToGameGroupPT(ctx.game); - const goals = await GetMostSubscribedGoals({ game: ctx.game, playtype }); + // GetMostSubscribedGoals internally converts GameGroup+playtype to V3Game for filtering + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const goals = await GetMostSubscribedGoals({ game: gameGroup as any, playtype }); return success(`Returned ${goals.length} goals.`, goals); }); @@ -800,23 +805,7 @@ API_V1_ROUTER.add("GET /games/:game/targets/quests/:questID", withGame, async ({ const users = await GetUsersWithIDs(questSubs.map((e) => e.userID)); const goals = await GetGoalsInQuest(quest); - const qlRows = await DB.selectFrom("questline") - .innerJoin("questline_quest", "questline_quest.questline_id", "questline.id") - .select(SELECT_QUESTLINE_ROW) - .where("questline_quest.quest_id", "=", quest.questID) - .execute(); - - const parentQuestlines = qlRows.map((ql) => { - const { gameGroup: gg, playtype: pt } = LEGACY_GameToGameGroupPT(ql.game); - - return { - desc: ql.description, - game: gg, - name: ql.name, - playtype: pt, - questlineID: ql.id, - }; - }); + const parentQuestlines = await GetQuestlinesThatContainQuest(quest.questID); return success(`Retrieved information about ${quest.name}.`, { goals, diff --git a/typescript/server/src/server/router/api/v1/spec.ts b/typescript/server/src/server/router/api/v1/spec.ts index e589d9344..df53a8616 100644 --- a/typescript/server/src/server/router/api/v1/spec.ts +++ b/typescript/server/src/server/router/api/v1/spec.ts @@ -1174,6 +1174,19 @@ export const API_V1_SPEC = { }), }, + "PUT /users/:userID/games/:game/targets/goals/:goalID": { + description: "Update a standalone goal subscription by replacing its definition.", + input: z.object({ + criteria: z.unknown(), + charts: z.unknown(), + }), + output: z.strictObject({ + changed: z.boolean(), + goal: doc(), + goalSub: doc(), + }), + }, + "DELETE /users/:userID/games/:game/targets/goals/:goalID": { description: "Unsubscribe from a goal.", input: z.object({}), @@ -1187,7 +1200,7 @@ export const API_V1_SPEC = { output: z.strictObject({ questSubs: docArray(), quests: docArray(), - goals: z.record(z.string(), docArray()), + goals: docArray(), }), }, diff --git a/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts b/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts index 1187bdd33..e119ae8c6 100644 --- a/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts +++ b/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts @@ -1,7 +1,399 @@ -import { describe, it } from "vitest"; +import { newGameProfilePreferenceColumns } from "#lib/game-settings/create-game-settings"; +import DB from "#services/pg/db"; +import mockApi, { CloseServerConnection } from "#test-utils/mock-api"; +import { seedMinimalIidxSpChart, seedUser } from "#test-utils/pg-fixtures"; +import { afterAll, beforeEach, describe, expect, it } from "vitest"; -describe("UGPT targets goals", () => { - it.todo( - "port large router.oldtest.ts (GET goals, POST add-goal, …) with Postgres goal/goal_sub", - ); +afterAll(() => CloseServerConnection()); + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +async function loginAs(username: string, password = "password123") { + const res = await mockApi.post("/api/v1/auth/login").send({ + username, + "!password": password, + captcha: "test", + }); + + return res.headers["set-cookie"] as unknown as string[]; +} + +async function seedGameProfile(userId: number) { + await DB.insertInto("game_profile") + .values({ + user_id: userId, + game: "iidx-sp", + ratings: JSON.stringify({}), + classes: JSON.stringify({}), + ...newGameProfilePreferenceColumns("iidx-sp"), + }) + .execute(); +} + +// ─── GET goals ──────────────────────────────────────────────────────────────── + +describe("GET /api/v1/users/:userID/games/:game/targets/goals", () => { + let userId: number; + + beforeEach(async () => { + const suffix = Math.random().toString(36).slice(2, 10); + ({ id: userId } = await seedUser({ + username: `goals_get_${suffix}`, + withCredential: true, + withSettings: true, + })); + await seedGameProfile(userId); + }); + + it("returns 200 with empty arrays when user has no goals", async () => { + const res = await mockApi.get(`/api/v1/users/${userId}/games/iidx-sp/targets/goals`); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.body.goals).toHaveLength(0); + expect(res.body.body.goalSubs).toHaveLength(0); + }); + + it("returns the user's subscribed goals when they have some", async () => { + const chartId = await seedMinimalIidxSpChart(); + const goalId = `G_get_${Date.now()}`; + + await DB.insertInto("goal").values({ + id: goalId, + game: "iidx-sp", + name: "Listed goal", + charts: JSON.stringify({ type: "single", data: chartId }), + criteria: JSON.stringify({ mode: "single", key: "lamp", value: 7 }), + }).execute(); + + await DB.insertInto("goal_sub").values({ + goal_id: goalId, + user_id: userId, + achieved: false, + time_achieved: null, + progress: null, + progress_human: "NO DATA", + out_of: 7, + out_of_human: "FULL COMBO", + last_interaction: null, + was_instantly_achieved: false, + was_assigned_standalone: true, + }).execute(); + + const res = await mockApi.get(`/api/v1/users/${userId}/games/iidx-sp/targets/goals`); + + expect(res.status).toBe(200); + expect(res.body.body.goalSubs.length).toBeGreaterThanOrEqual(1); + }); +}); + +// ─── POST add-goal ──────────────────────────────────────────────────────────── + +describe("POST /api/v1/users/:userID/games/:game/targets/goals/add-goal", () => { + let userId: number; + let cookie: string[]; + + beforeEach(async () => { + const suffix = Math.random().toString(36).slice(2, 10); + const { username } = await seedUser({ + username: `goals_add_${suffix}`, + withCredential: true, + withSettings: true, + }); + + // seedUser returns the inserted id but we need to look it up since id is serial + const row = await DB.selectFrom("account") + .select("account.id") + .where("account.username", "=", username) + .executeTakeFirstOrThrow(); + + userId = Number(row.id); + cookie = await loginAs(username); + await seedGameProfile(userId); + }); + + it("returns 401 when not authenticated", async () => { + const chartId = await seedMinimalIidxSpChart(); + + const res = await mockApi + .post(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/add-goal`) + .send({ + charts: { type: "single", data: chartId }, + criteria: { mode: "single", key: "lamp", value: 7 }, + }); + + expect(res.status).toBe(401); + }); + + it("returns 200 and creates the goal + sub when input is valid", async () => { + const chartId = await seedMinimalIidxSpChart(); + + const res = await mockApi + .post(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/add-goal`) + .set("Cookie", cookie) + .send({ + charts: { type: "single", data: chartId }, + criteria: { mode: "single", key: "lamp", value: 7 }, + }); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.body.goal.game).toBe("iidx-sp"); + expect(res.body.body.goalSub.userID).toBe(userId); + }); + + it("returns 409 when the user is already subscribed to that goal", async () => { + const chartId = await seedMinimalIidxSpChart(); + const body = { + charts: { type: "single", data: chartId }, + criteria: { mode: "single", key: "lamp", value: 7 }, + }; + + await mockApi + .post(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/add-goal`) + .set("Cookie", cookie) + .send(body); + + const res = await mockApi + .post(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/add-goal`) + .set("Cookie", cookie) + .send(body); + + expect(res.status).toBe(409); + expect(res.body.success).toBe(false); + }); +}); + +// ─── GET single goal ────────────────────────────────────────────────────────── + +describe("GET /api/v1/users/:userID/games/:game/targets/goals/:goalID", () => { + let userId: number; + + beforeEach(async () => { + const suffix = Math.random().toString(36).slice(2, 10); + ({ id: userId } = await seedUser({ username: `goals_single_${suffix}` })); + await seedGameProfile(userId); + }); + + it("returns 404 when user is not subscribed to the goal", async () => { + const res = await mockApi.get( + `/api/v1/users/${userId}/games/iidx-sp/targets/goals/G_no_such`, + ); + + expect(res.status).toBe(404); + }); + + it("returns the goal sub when subscribed", async () => { + const chartId = await seedMinimalIidxSpChart(); + const goalId = `G_single_${Date.now()}`; + + await DB.insertInto("goal").values({ + id: goalId, + game: "iidx-sp", + name: "Single test goal", + charts: JSON.stringify({ type: "single", data: chartId }), + criteria: JSON.stringify({ mode: "single", key: "lamp", value: 4 }), + }).execute(); + + await DB.insertInto("goal_sub").values({ + goal_id: goalId, + user_id: userId, + achieved: false, + time_achieved: null, + progress: null, + progress_human: "NO DATA", + out_of: 4, + out_of_human: "HARD CLEAR", + last_interaction: null, + was_instantly_achieved: false, + was_assigned_standalone: true, + }).execute(); + + const res = await mockApi.get( + `/api/v1/users/${userId}/games/iidx-sp/targets/goals/${goalId}`, + ); + + expect(res.status).toBe(200); + expect(res.body.body.goal.goalID).toBe(goalId); + expect(res.body.body.goalSub.userID).toBe(userId); + }); +}); + +// ─── PUT update goal ────────────────────────────────────────────────────────── + +describe("PUT /api/v1/users/:userID/games/:game/targets/goals/:goalID", () => { + let userId: number; + let cookie: string[]; + + beforeEach(async () => { + const suffix = Math.random().toString(36).slice(2, 10); + const { username } = await seedUser({ + username: `goals_put_${suffix}`, + withCredential: true, + withSettings: true, + }); + + const row = await DB.selectFrom("account") + .select("account.id") + .where("account.username", "=", username) + .executeTakeFirstOrThrow(); + + userId = Number(row.id); + cookie = await loginAs(username); + await seedGameProfile(userId); + }); + + it("returns 401 when not authenticated", async () => { + const res = await mockApi + .put(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/G_fake`) + .send({ + charts: { type: "single", data: "C_fake" }, + criteria: { mode: "single", key: "lamp", value: 7 }, + }); + + expect(res.status).toBe(401); + }); + + it("returns 404 when not subscribed to the old goal", async () => { + const chartId = await seedMinimalIidxSpChart(); + + const res = await mockApi + .put(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/G_nonexistent`) + .set("Cookie", cookie) + .send({ + charts: { type: "single", data: chartId }, + criteria: { mode: "single", key: "lamp", value: 7 }, + }); + + expect(res.status).toBe(404); + }); + + it("swaps the goal subscription and returns the new goal", async () => { + const chartA = await seedMinimalIidxSpChart(); + const chartB = await seedMinimalIidxSpChart(); + + // Add initial goal via the add-goal endpoint + const addRes = await mockApi + .post(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/add-goal`) + .set("Cookie", cookie) + .send({ + charts: { type: "single", data: chartA }, + criteria: { mode: "single", key: "lamp", value: 4 }, + }); + + expect(addRes.status).toBe(200); + + const oldGoalID = addRes.body.body.goal.goalID as string; + + // Update to a different chart + const putRes = await mockApi + .put(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/${oldGoalID}`) + .set("Cookie", cookie) + .send({ + charts: { type: "single", data: chartB }, + criteria: { mode: "single", key: "lamp", value: 4 }, + }); + + expect(putRes.status).toBe(200); + expect(putRes.body.success).toBe(true); + expect(putRes.body.body.changed).toBe(true); + expect(putRes.body.body.goal.charts).toMatchObject({ type: "single", data: chartB }); + expect(putRes.body.body.goalSub.userID).toBe(userId); + + // Old sub should be gone + const oldSub = await DB.selectFrom("goal_sub") + .selectAll() + .where("goal_sub.goal_id", "=", oldGoalID) + .where("goal_sub.user_id", "=", userId) + .executeTakeFirst(); + + expect(oldSub).toBeUndefined(); + }); + + it("returns changed: false when the definition is unchanged", async () => { + const chartId = await seedMinimalIidxSpChart(); + + const addRes = await mockApi + .post(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/add-goal`) + .set("Cookie", cookie) + .send({ + charts: { type: "single", data: chartId }, + criteria: { mode: "single", key: "lamp", value: 4 }, + }); + + const goalID = addRes.body.body.goal.goalID as string; + + const putRes = await mockApi + .put(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/${goalID}`) + .set("Cookie", cookie) + .send({ + charts: { type: "single", data: chartId }, + criteria: { mode: "single", key: "lamp", value: 4 }, + }); + + expect(putRes.status).toBe(200); + expect(putRes.body.body.changed).toBe(false); + expect(putRes.body.body.goal.goalID).toBe(goalID); + }); +}); + +// ─── DELETE goal ────────────────────────────────────────────────────────────── + +describe("DELETE /api/v1/users/:userID/games/:game/targets/goals/:goalID", () => { + let userId: number; + let cookie: string[]; + + beforeEach(async () => { + const suffix = Math.random().toString(36).slice(2, 10); + const { username } = await seedUser({ + username: `goals_del_${suffix}`, + withCredential: true, + withSettings: true, + }); + + const row = await DB.selectFrom("account") + .select("account.id") + .where("account.username", "=", username) + .executeTakeFirstOrThrow(); + + userId = Number(row.id); + cookie = await loginAs(username); + await seedGameProfile(userId); + }); + + it("returns 401 when not authenticated", async () => { + const res = await mockApi.delete( + `/api/v1/users/${userId}/games/iidx-sp/targets/goals/G_fake`, + ); + + expect(res.status).toBe(401); + }); + + it("removes the goal sub and returns 200", async () => { + const chartId = await seedMinimalIidxSpChart(); + + const addRes = await mockApi + .post(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/add-goal`) + .set("Cookie", cookie) + .send({ + charts: { type: "single", data: chartId }, + criteria: { mode: "single", key: "lamp", value: 4 }, + }); + + const goalID = addRes.body.body.goal.goalID as string; + + const delRes = await mockApi + .delete(`/api/v1/users/${userId}/games/iidx-sp/targets/goals/${goalID}`) + .set("Cookie", cookie); + + expect(delRes.status).toBe(200); + + const sub = await DB.selectFrom("goal_sub") + .selectAll() + .where("goal_sub.goal_id", "=", goalID) + .where("goal_sub.user_id", "=", userId) + .executeTakeFirst(); + + expect(sub).toBeUndefined(); + }); }); diff --git a/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts b/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts index 6a8a4faf6..5f0b537f3 100644 --- a/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts +++ b/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts @@ -144,6 +144,56 @@ API_V1_ROUTER.add( }, ); +/** + * Update a standalone goal subscription by replacing its definition. + * + * Creates a new goal from the provided charts/criteria and atomically swaps + * the user's subscription from the old goal to the new one. Progress is + * re-evaluated from scores on the next import — no migration is needed. + * + * Only standalone goals (not quest-assigned goals) may be updated this way. + * + * @name PUT /api/v1/users/:userID/games/:game/targets/goals/:goalID + */ +API_V1_ROUTER.add( + "PUT /users/:userID/games/:game/targets/goals/:goalID", + withUserGameProfile, + async ({ ctx, params, input, req }) => { + const { requestedUser: user, game } = ctx; + + const sessionUser = req.session.tachi?.user; + + if (!sessionUser) { + throw new ExpectedErr(401, "You are not authenticated."); + } + + const taker = { acct: { id: sessionUser.id, username: sessionUser.username }, ip: req.ip }; + + const { ACTION_UpdateGoalSubscription } = await import( + "#actions/update-goal-subscription" + ); + const { GetGoalForIDGuaranteed, GetGoalSubscriptionForIDGuaranteed } = await import( + "#utils/db" + ); + + const { newGoalID, changed } = await ACTION_UpdateGoalSubscription(taker, { + charts: input.charts as GoalDocument["charts"], + criteria: input.criteria as GoalDocument["criteria"], + game, + oldGoalID: params.goalID, + userID: user.id, + }); + + const newGoal = await GetGoalForIDGuaranteed(newGoalID); + const newGoalSub = await GetGoalSubscriptionForIDGuaranteed(newGoalID, user.id); + + return success( + changed ? `Updated goal to '${newGoal.name}'.` : "Goal definition is unchanged.", + { changed, goal: newGoal, goalSub: newGoalSub }, + ); + }, +); + /** * Unsubscribe from a goal. * diff --git a/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/quests/router.test.ts b/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/quests/router.test.ts index 80a676845..712d5ea30 100644 --- a/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/quests/router.test.ts +++ b/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/quests/router.test.ts @@ -1,5 +1,306 @@ -import { describe, it } from "vitest"; +import { newGameProfilePreferenceColumns } from "#lib/game-settings/create-game-settings"; +import DB from "#services/pg/db"; +import mockApi, { CloseServerConnection } from "#test-utils/mock-api"; +import { seedUser } from "#test-utils/pg-fixtures"; +import { afterAll, beforeEach, describe, expect, it } from "vitest"; -describe("UGPT targets quests", () => { - it.todo("port router.oldtest.ts (GET/PATCH quest routes) with Postgres quest/quest_sub"); +afterAll(() => CloseServerConnection()); + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +async function loginAs(username: string, password = "password123") { + const res = await mockApi.post("/api/v1/auth/login").send({ + username, + "!password": password, + captcha: "test", + }); + + return res.headers["set-cookie"] as unknown as string[]; +} + +async function seedGameProfile(userId: number) { + await DB.insertInto("game_profile") + .values({ + user_id: userId, + game: "iidx-sp", + ratings: JSON.stringify({}), + classes: JSON.stringify({}), + ...newGameProfilePreferenceColumns("iidx-sp"), + }) + .execute(); +} + +async function seedQuest(suffix: string) { + const questId = `q-ugpt-${suffix}`; + + await DB.insertInto("quest") + .values({ + id: questId, + game: "iidx-sp", + name: `UGPT Quest ${suffix}`, + description: "Test quest", + quest_data: JSON.stringify([]), + }) + .execute(); + + return questId; +} + +// ─── GET quests ─────────────────────────────────────────────────────────────── + +describe("GET /api/v1/users/:userID/games/:game/targets/quests", () => { + let userId: number; + + beforeEach(async () => { + const suffix = Math.random().toString(36).slice(2, 10); + ({ id: userId } = await seedUser({ username: `q_get_${suffix}` })); + await seedGameProfile(userId); + }); + + it("returns 200 with empty arrays when user has no quest subscriptions", async () => { + const res = await mockApi.get(`/api/v1/users/${userId}/games/iidx-sp/targets/quests`); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.body.quests).toHaveLength(0); + expect(res.body.body.questSubs).toHaveLength(0); + expect(res.body.body.goals).toHaveLength(0); + }); + + it("returns subscribed quests and their goals", async () => { + const suffix = `${Date.now()}`; + const questId = await seedQuest(suffix); + + await DB.insertInto("quest_sub") + .values({ + quest_id: questId, + user_id: userId, + achieved: false, + time_achieved: null, + progress: 0, + last_interaction: null, + was_instantly_achieved: false, + }) + .execute(); + + const res = await mockApi.get(`/api/v1/users/${userId}/games/iidx-sp/targets/quests`); + + expect(res.status).toBe(200); + + const questIds = (res.body.body.quests as Array<{ questID: string }>).map((q) => q.questID); + + expect(questIds).toContain(questId); + }); +}); + +// ─── GET single quest ───────────────────────────────────────────────────────── + +describe("GET /api/v1/users/:userID/games/:game/targets/quests/:questID", () => { + let userId: number; + + beforeEach(async () => { + const suffix = Math.random().toString(36).slice(2, 10); + ({ id: userId } = await seedUser({ username: `q_single_${suffix}` })); + await seedGameProfile(userId); + }); + + it("returns 404 when user is not subscribed", async () => { + const suffix = `${Date.now()}-ns`; + const questId = await seedQuest(suffix); + + const res = await mockApi.get( + `/api/v1/users/${userId}/games/iidx-sp/targets/quests/${questId}`, + ); + + expect(res.status).toBe(404); + }); + + it("returns 404 when quest does not exist", async () => { + const res = await mockApi.get( + `/api/v1/users/${userId}/games/iidx-sp/targets/quests/q_no_such`, + ); + + expect(res.status).toBe(404); + }); + + it("returns quest progress when subscribed", async () => { + const suffix = `${Date.now()}-sub`; + const questId = await seedQuest(suffix); + + await DB.insertInto("quest_sub") + .values({ + quest_id: questId, + user_id: userId, + achieved: false, + time_achieved: null, + progress: 0, + last_interaction: null, + was_instantly_achieved: false, + }) + .execute(); + + const res = await mockApi.get( + `/api/v1/users/${userId}/games/iidx-sp/targets/quests/${questId}`, + ); + + expect(res.status).toBe(200); + expect(res.body.body.quest.questID).toBe(questId); + expect(res.body.body.questSub.userID).toBe(userId); + expect(Array.isArray(res.body.body.goals)).toBe(true); + expect(Array.isArray(res.body.body.results)).toBe(true); + }); +}); + +// ─── PUT subscribe ──────────────────────────────────────────────────────────── + +describe("PUT /api/v1/users/:userID/games/:game/targets/quests/:questID", () => { + let userId: number; + let cookie: string[]; + + beforeEach(async () => { + const suffix = Math.random().toString(36).slice(2, 10); + const { username } = await seedUser({ + username: `q_put_${suffix}`, + withCredential: true, + withSettings: true, + }); + + const row = await DB.selectFrom("account") + .select("account.id") + .where("account.username", "=", username) + .executeTakeFirstOrThrow(); + + userId = Number(row.id); + cookie = await loginAs(username); + await seedGameProfile(userId); + }); + + it("returns 401 when not authenticated", async () => { + const suffix = `${Date.now()}-auth`; + const questId = await seedQuest(suffix); + + const res = await mockApi.put( + `/api/v1/users/${userId}/games/iidx-sp/targets/quests/${questId}`, + ); + + expect(res.status).toBe(401); + }); + + it("returns 404 when quest does not exist", async () => { + const res = await mockApi + .put(`/api/v1/users/${userId}/games/iidx-sp/targets/quests/q_no_such`) + .set("Cookie", cookie); + + expect(res.status).toBe(404); + }); + + it("subscribes the user to the quest and returns 200", async () => { + const suffix = `${Date.now()}-ok`; + const questId = await seedQuest(suffix); + + const res = await mockApi + .put(`/api/v1/users/${userId}/games/iidx-sp/targets/quests/${questId}`) + .set("Cookie", cookie); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.body.quest.questID).toBe(questId); + + const sub = await DB.selectFrom("quest_sub") + .selectAll() + .where("quest_sub.quest_id", "=", questId) + .where("quest_sub.user_id", "=", userId) + .executeTakeFirst(); + + expect(sub).toBeDefined(); + }); + + it("returns 409 with 'quest' (not 'goal') when already subscribed", async () => { + const suffix = `${Date.now()}-dup`; + const questId = await seedQuest(suffix); + + await mockApi + .put(`/api/v1/users/${userId}/games/iidx-sp/targets/quests/${questId}`) + .set("Cookie", cookie); + + const res = await mockApi + .put(`/api/v1/users/${userId}/games/iidx-sp/targets/quests/${questId}`) + .set("Cookie", cookie); + + expect(res.status).toBe(409); + // The error message must mention "quest", not "goal" (regression guard for the bug fix) + expect(res.body.description).toMatch(/quest/i); + expect(res.body.description).not.toMatch(/goal/i); + }); +}); + +// ─── DELETE unsubscribe ─────────────────────────────────────────────────────── + +describe("DELETE /api/v1/users/:userID/games/:game/targets/quests/:questID", () => { + let userId: number; + let cookie: string[]; + + beforeEach(async () => { + const suffix = Math.random().toString(36).slice(2, 10); + const { username } = await seedUser({ + username: `q_del_${suffix}`, + withCredential: true, + withSettings: true, + }); + + const row = await DB.selectFrom("account") + .select("account.id") + .where("account.username", "=", username) + .executeTakeFirstOrThrow(); + + userId = Number(row.id); + cookie = await loginAs(username); + await seedGameProfile(userId); + }); + + it("returns 401 when not authenticated", async () => { + const suffix = `${Date.now()}-dauth`; + const questId = await seedQuest(suffix); + + const res = await mockApi.delete( + `/api/v1/users/${userId}/games/iidx-sp/targets/quests/${questId}`, + ); + + expect(res.status).toBe(401); + }); + + it("returns 409 when not subscribed", async () => { + const suffix = `${Date.now()}-dns`; + const questId = await seedQuest(suffix); + + const res = await mockApi + .delete(`/api/v1/users/${userId}/games/iidx-sp/targets/quests/${questId}`) + .set("Cookie", cookie); + + expect(res.status).toBe(409); + }); + + it("removes the quest sub and returns 200", async () => { + const suffix = `${Date.now()}-dok`; + const questId = await seedQuest(suffix); + + // Subscribe first + await mockApi + .put(`/api/v1/users/${userId}/games/iidx-sp/targets/quests/${questId}`) + .set("Cookie", cookie); + + const delRes = await mockApi + .delete(`/api/v1/users/${userId}/games/iidx-sp/targets/quests/${questId}`) + .set("Cookie", cookie); + + expect(delRes.status).toBe(200); + + const sub = await DB.selectFrom("quest_sub") + .selectAll() + .where("quest_sub.quest_id", "=", questId) + .where("quest_sub.user_id", "=", userId) + .executeTakeFirst(); + + expect(sub).toBeUndefined(); + }); }); diff --git a/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/quests/router.ts b/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/quests/router.ts index 7bfe2e3b2..998174737 100644 --- a/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/quests/router.ts +++ b/typescript/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/quests/router.ts @@ -102,9 +102,13 @@ API_V1_ROUTER.add( API_V1_ROUTER.add( "PUT /users/:userID/games/:game/targets/quests/:questID", withUserGameProfile, - async ({ ctx, params }) => { + async ({ ctx, params, req }) => { const { requestedUser: user, game } = ctx; + if (!req.session.tachi?.user) { + throw new ExpectedErr(401, "You are not authenticated."); + } + const { SubscribeFailReasons } = await import("#lib/constants/err-codes"); const { SubscribeToQuest } = await import("#lib/targets/quests"); const { ServerConfig } = await import("#lib/setup/config"); @@ -146,7 +150,7 @@ API_V1_ROUTER.add( .executeTakeFirst(); if (alreadySubscribed) { - throw new ExpectedErr(409, "You are already subscribed to this goal."); + throw new ExpectedErr(409, "You are already subscribed to this quest."); } const subResult = await SubscribeToQuest(user.id, quest, false); @@ -167,9 +171,13 @@ API_V1_ROUTER.add( API_V1_ROUTER.add( "DELETE /users/:userID/games/:game/targets/quests/:questID", withUserGameProfile, - async ({ ctx, params }) => { + async ({ ctx, params, req }) => { const { requestedUser: user, game } = ctx; + if (!req.session.tachi?.user) { + throw new ExpectedErr(401, "You are not authenticated."); + } + const { UnsubscribeFromQuest } = await import("#lib/targets/quests"); const questRow = await DB.selectFrom("quest") diff --git a/typescript/server/src/services/pg/seeds.ts b/typescript/server/src/services/pg/seeds.ts index 84c0adf85..8c810a533 100644 --- a/typescript/server/src/services/pg/seeds.ts +++ b/typescript/server/src/services/pg/seeds.ts @@ -296,6 +296,22 @@ export function buildChartIdMap(seedsDir: string): Map { return map; } +/** + * Optional map from `rerunners/v3/7-remap-goals-folder-and-chart-ids.ts` (`db/seeds/goal-id-remap.json`). + * Mongo `goal-subs.goalID` may still use hashes from before legacy chart/folder ids were rewired. + */ +export function buildGoalIdRemap(seedsDir: string): Map { + const p = path.join(seedsDir, "goal-id-remap.json"); + + if (!fs.existsSync(p)) { + return new Map(); + } + + const raw = JSON.parse(fs.readFileSync(p, "utf-8")) as Record; + + return new Map(Object.entries(raw)); +} + // ── Core import logic ────────────────────────────────────────────────────── export async function importSeeds(pg: Kysely, seedsDir: string): Promise { diff --git a/typescript/server/src/utils/queries/questlines.ts b/typescript/server/src/utils/queries/questlines.ts index 143f277f1..696757693 100644 --- a/typescript/server/src/utils/queries/questlines.ts +++ b/typescript/server/src/utils/queries/questlines.ts @@ -40,6 +40,51 @@ export async function GetQuestlinesForGame(game: V3Game): Promise ToQuestlineDocument(row, byQuestline.get(row.questline_id) ?? [])); } +/** + * All questlines that contain a given quest, with `quests` ordered by `sort_order`. + */ +export async function GetQuestlinesThatContainQuest( + questID: string, +): Promise> { + const memberRows = await DB.selectFrom("questline_quest") + .select("questline_quest.questline_id") + .where("questline_quest.quest_id", "=", questID) + .execute(); + + if (memberRows.length === 0) { + return []; + } + + const questlineIds = memberRows.map((r) => r.questline_id); + + const qlRows = await DB.selectFrom("questline") + .select(SELECT_QUESTLINE) + .where("questline.id", "in", questlineIds) + .execute(); + + const qqRows = await DB.selectFrom("questline_quest") + .select([ + "questline_quest.questline_id", + "questline_quest.quest_id", + "questline_quest.sort_order", + ]) + .where("questline_quest.questline_id", "in", questlineIds) + .orderBy("questline_quest.questline_id") + .orderBy("questline_quest.sort_order") + .execute(); + + const byQuestline = new Map>(); + + for (const row of qqRows) { + const list = byQuestline.get(row.questline_id) ?? []; + + list.push(row.quest_id); + byQuestline.set(row.questline_id, list); + } + + return qlRows.map((row) => ToQuestlineDocument(row, byQuestline.get(row.questline_id) ?? [])); +} + /** * One questline by id, scoped to game. `quests` are ordered by `questline_quest.sort_order`. */