From 40b44521aa4e43c94add3015899ec953017ef107 Mon Sep 17 00:00:00 2001 From: Golumpa Date: Wed, 19 Aug 2026 12:32:16 +0100 Subject: [PATCH] Expand achievement exports with progress and metadata --- .github/workflows/update-mappings.yml | 4 +- README.md | 15 +- docs/mapping-updates.md | 18 +- docs/packet-format.md | 2 + mappings/achievements.json | 450 ++++++++++++++++++ .../decoder/achievement.py | 67 ++- .../export/json_export.py | 83 ++++ .../live_capture/runner.py | 52 +- src/nte_history_exporter/mapping_update.py | 112 ++++- src/nte_history_exporter/mappings.py | 4 + .../DataTable/DT_AchievementConfigInfo.json | 22 + .../nte_assets/Localization/en/game.json | 4 + tests/test_achievement.py | 8 + tests/test_export_contract.py | 80 ++++ tests/test_mapping_update.py | 15 +- tests/test_mappings_and_updates.py | 9 + tools/update_mappings.py | 5 +- 17 files changed, 898 insertions(+), 52 deletions(-) create mode 100644 mappings/achievements.json create mode 100644 tests/fixtures/nte_assets/DataTable/DT_AchievementConfigInfo.json diff --git a/.github/workflows/update-mappings.yml b/.github/workflows/update-mappings.yml index 8760bb3..38068bf 100644 --- a/.github/workflows/update-mappings.yml +++ b/.github/workflows/update-mappings.yml @@ -83,8 +83,8 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git checkout -B "$AUTOMATION_BRANCH" - git add mappings/arcs.json mappings/characters.json mappings/items.json - git commit -m "chore: update reward mappings from NTE_Assets" + git add mappings/arcs.json mappings/characters.json mappings/items.json mappings/achievements.json + git commit -m "chore: update mappings from NTE_Assets" git push --force origin "HEAD:refs/heads/$AUTOMATION_BRANCH" - name: Open or update pull request diff --git a/README.md b/README.md index ba9f98c..95c91c1 100644 --- a/README.md +++ b/README.md @@ -114,10 +114,17 @@ If a page response is missed, the exporter reports the missing page number while Start the live exporter before launching the game. The completed achievement list is loaded during login, captured automatically, and written to -`exports/Achievements_*.json` as an array of achievement IDs. Completed -PlayStation trophy entries are retained even though they are not shown in the -in-game achievement list. The console reports the visible and PlayStation -counts as soon as the list has been decoded. +`exports/_Achievements_*.json` as a versioned achievement export with +account metadata, completion totals, and records grouped by achievement category. +Both completed and incomplete achievements are included with their current +progress, status, and completion time when available. Generated metadata adds +the localized title and description, target, quality, and rewards from the +current `NTE_Assets` achievement table. Newly captured IDs that are not yet in +the bundled mapping still export normally; only their optional display metadata +is omitted until the mappings are refreshed. +Completed PlayStation trophy entries are retained and identified separately +even though they are not shown in the in-game achievement list. The console +reports the visible and PlayStation counts as soon as the list has been decoded. #### Linux/macOS diff --git a/docs/mapping-updates.md b/docs/mapping-updates.md index aa67d25..a4856bc 100644 --- a/docs/mapping-updates.md +++ b/docs/mapping-updates.md @@ -1,11 +1,12 @@ # Reward mapping updates -`tools/update_mappings.py` rebuilds `arcs.json`, `characters.json`, and -`items.json` directly from the current +`tools/update_mappings.py` rebuilds `arcs.json`, `characters.json`, +`items.json`, and `achievements.json` directly from the current [`Waifus-Grace/NTE_Assets`](https://github.com/Waifus-Grace/NTE_Assets) tables. -English display names come from `Localization/en/game.json`. The data tables +English display names and achievement descriptions come from +`Localization/en/game.json`. The data tables provide localization namespace/key references and quality metadata; their embedded `LocalizedString` values are deliberately ignored. @@ -54,7 +55,7 @@ To use an existing local checkout without network access: python tools/update_mappings.py --assets-root path\to\NTE_Assets ``` -The default output is `build/mapping-update/` and contains the three candidate +The default output is `build/mapping-update/` and contains the four candidate mapping files plus `mapping-update-report.json`. The report lists additions, updates, and deletions by file and records SHA-256 hashes for every source table. @@ -71,6 +72,9 @@ from the committed reward mappings. It still writes the staged artifacts. ## Source rules +- Every achievement in `DT_AchievementConfigInfo` is emitted to + `achievements.json` with its localized title and description, target, + category, quality, and rewards. - Every Arc in `DT_ForkItemData` is emitted to `arcs.json`. - Every character in `DT_Character` is emitted to `characters.json`. - Other pull rewards are selected by `GachaIllustrate` and resolved to the @@ -85,13 +89,13 @@ from the committed reward mappings. It still writes the staged artifacts. currencies, are categorized as items. - Names are resolved strictly through `Localization/en/game.json`. A missing or ambiguous key fails the update instead of falling back to a DT value. -- Character-awakening illustration entries are not independent pull rewards - and are excluded. +- Character-awakening and character-vehicle illustration entries are not + independent pull rewards and are excluded. - Orange, purple, and blue item qualities map to `S`, `A`, and `B`. ## UID compatibility boundary -This updater reads and writes only the three reward mapping files. It never +This updater reads and writes only the generated reward and achievement mapping files. It never reads or writes the permanent, limited, beginner, or Arc pool mapping files. Banner IDs, timestamps, record ordering, format version, and all UID inputs are therefore unchanged by mapping synchronization. diff --git a/docs/packet-format.md b/docs/packet-format.md index de1c643..290094b 100644 --- a/docs/packet-format.md +++ b/docs/packet-format.md @@ -86,6 +86,8 @@ Decoded fields: Arc/Gashapon history uses the same scheme at `ASCII * 2`. - The decoder decodes the key to its id string and looks up display metadata in `mappings/arcs.json`, `mappings/characters.json`, and `mappings/items.json`. + Achievement display metadata is generated separately in + `mappings/achievements.json`. Unknown rewards still export their decoded id with empty name/rank. Page and row numbers are research metadata only. They must not be used for permanent dedupe because they shift when new history appears. diff --git a/mappings/achievements.json b/mappings/achievements.json new file mode 100644 index 0000000..0b91b58 --- /dev/null +++ b/mappings/achievements.json @@ -0,0 +1,450 @@ +{ + "battle_10": {"name": "Here It Comes! V", "description": "Use Ultimate ×300.", "category": "fight", "quality": "execellent", "target": 300, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_11": {"name": "Speed Above All I", "description": "Trigger Critical Dodge ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_12": {"name": "Speed Above All II", "description": "Trigger Critical Dodge ×50.", "category": "fight", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_13": {"name": "Speed Above All III", "description": "Trigger Critical Dodge ×100.", "category": "fight", "quality": "normal", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_14": {"name": "Speed Above All IV", "description": "Trigger Critical Dodge ×200.", "category": "fight", "quality": "high", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_15": {"name": "Speed Above All V", "description": "Trigger Critical Dodge ×300.", "category": "fight", "quality": "execellent", "target": 300, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_16": {"name": "Crumbles at Touch I", "description": "In battle, Break enemies ×5.", "category": "fight", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_17": {"name": "Crumbles at Touch II", "description": "In battle, Break enemies ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_18": {"name": "Crumbles at Touch III", "description": "In battle, Break enemies ×50.", "category": "fight", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_19": {"name": "Crumbles at Touch IV", "description": "In battle, Break enemies ×100.", "category": "fight", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_20": {"name": "Crumbles at Touch V", "description": "In battle, Break enemies ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_21": {"name": "Harvest Season I", "description": "Trigger Blossom ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_22": {"name": "Harvest Season II", "description": "Trigger Blossom ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_23": {"name": "Harvest Season III", "description": "Trigger Blossom ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_24": {"name": "Devil Within I", "description": "Trigger Hexed ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_25": {"name": "Devil Within II", "description": "Trigger Hexed ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_26": {"name": "Devil Within III", "description": "Trigger Hexed ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_27": {"name": "Stopped Thinking I", "description": "Trigger Remora ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_28": {"name": "Stopped Thinking II", "description": "Trigger Remora ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_29": {"name": "Stopped Thinking III", "description": "Trigger Remora ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_30": {"name": "Death Nova I", "description": "Trigger Nova ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_31": {"name": "Death Nova II", "description": "Trigger Nova ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_32": {"name": "Death Nova III", "description": "Trigger Nova ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_33": {"name": "Scarlet Corruption I", "description": "Trigger Scorch ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_34": {"name": "Scarlet Corruption II", "description": "Trigger Scorch ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_35": {"name": "Scarlet Corruption III", "description": "Trigger Scorch ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_36": {"name": "Soul Color I", "description": "Trigger Stain ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_37": {"name": "Soul Color II", "description": "Trigger Stain ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_38": {"name": "Soul Color III", "description": "Trigger Stain ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_39": {"name": "Bumper Harvest I", "description": "Trigger Charge ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_40": {"name": "Bumper Harvest II", "description": "Trigger Charge ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_41": {"name": "Bumper Harvest III", "description": "Trigger Charge ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_42": {"name": "Malevolence Unbound I", "description": "Trigger Discord ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_43": {"name": "Malevolence Unbound II", "description": "Trigger Discord ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_44": {"name": "Malevolence Unbound III", "description": "Trigger Discord ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_49": {"name": "Variation Movement I", "description": "Cast Skill ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_50": {"name": "Variation Movement II", "description": "Cast Skill ×50.", "category": "fight", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_51": {"name": "Variation Movement III", "description": "Cast Skill ×100.", "category": "fight", "quality": "normal", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_52": {"name": "Variation Movement IV", "description": "Cast Skill ×200.", "category": "fight", "quality": "high", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_53": {"name": "Variation Movement V", "description": "Cast Skill ×300.", "category": "fight", "quality": "execellent", "target": 300, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_54": {"name": "Death is the Final Movement of All Life I", "description": "Defeat enemies with Ultimate ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_55": {"name": "Death is the Final Movement of All Life II", "description": "Defeat enemies with Ultimate ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_56": {"name": "Death is the Final Movement of All Life III", "description": "Defeat enemies with Ultimate ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_6": {"name": "Here It Comes! I", "description": "Use Ultimate ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_63": {"name": "Come At Me!", "description": "In battle, have a single character with a Shield of over 5,000 points.", "category": "fight", "quality": "normal", "target": 5000, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_64": {"name": "Non-Lethal Defense", "description": "In battle, have a single character with a Shield of over 10,000 points.", "category": "fight", "quality": "high", "target": 10000, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "battle_65": {"name": "Absolute Defense", "description": "In battle, have a single character with a Shield of over 30,000 points.", "category": "fight", "quality": "execellent", "target": 30000, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "battle_7": {"name": "Here It Comes! II", "description": "Use Ultimate ×50.", "category": "fight", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_8": {"name": "Here It Comes! III", "description": "Use Ultimate ×100.", "category": "fight", "quality": "normal", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "battle_9": {"name": "Here It Comes! IV", "description": "Use Ultimate ×200.", "category": "fight", "quality": "high", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_1": {"name": "Hethereau Beginner", "description": "Raise Hunter Level to Lvl 10.", "category": "develop", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_10": {"name": "One-Man Army IV", "description": "Raise 1 character to Lvl 80.", "category": "develop", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_11": {"name": "Four-Man Squad I", "description": "Raise 4 characters to Lvl 20.", "category": "develop", "quality": "normal", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_12": {"name": "Four-Man Squad II", "description": "Raise 4 characters to Lvl 40.", "category": "develop", "quality": "normal", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_13": {"name": "Four-Man Squad III", "description": "Raise 4 characters to Lvl 60.", "category": "develop", "quality": "high", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_14": {"name": "Four-Man Squad IV", "description": "Raise 4 characters to Lvl 80.", "category": "develop", "quality": "execellent", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "develop_15": {"name": "There are seven more fighters just as tough as me!", "description": "Raise 8 characters to Lvl 80.", "category": "develop", "quality": "execellent", "target": 8, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "develop_16": {"name": "Touching the Forbidden Edge I", "description": "Raise 1 Arc to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_17": {"name": "Touching the Forbidden Edge II", "description": "Raise 1 Arc to Lvl 40.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_18": {"name": "Touching the Forbidden Edge III", "description": "Raise 1 Arc to Lvl 60.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_19": {"name": "Touching the Forbidden Edge IV", "description": "Raise 1 Arc to Lvl 80.", "category": "develop", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_2": {"name": "The Master Shows the Way…", "description": "Raise Hunter Level to Lvl 20.", "category": "develop", "quality": "normal", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_20": {"name": "Duel Master I", "description": "Raise 4 Arcs to Lvl 20.", "category": "develop", "quality": "normal", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_21": {"name": "Duel Master II", "description": "Raise 4 Arcs to Lvl 40.", "category": "develop", "quality": "normal", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_22": {"name": "Duel Master III", "description": "Raise 4 Arcs to Lvl 60.", "category": "develop", "quality": "high", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_23": {"name": "Duel Master IV", "description": "Raise 4 Arcs to Lvl 80.", "category": "develop", "quality": "execellent", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "develop_24": {"name": "Duel Master V", "description": "Raise 8 Arcs to Lvl 80.", "category": "develop", "quality": "execellent", "target": 8, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "develop_25": {"name": "The Nameless", "description": "Upgrade 1 Orange Console Module to Lvl 10.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_26": {"name": "Stick Fighter", "description": "Upgrade 1 Orange Console Module to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_27": {"name": "Half-Hearted Devotion", "description": "Upgrade 6 Orange Console Modules to Lvl 10.", "category": "develop", "quality": "normal", "target": 6, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_28": {"name": "Retro King", "description": "Upgrade 6 Orange Console Modules to Lvl 20.", "category": "develop", "quality": "high", "target": 6, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_29": {"name": "Hardcore Gamer", "description": "Upgrade 1 Orange Console Cartridge to Lvl 10.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_3": {"name": "Ultra-Spec Growth", "description": "Raise Hunter Level to Lvl 30.", "category": "develop", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_30": {"name": "Platinum Trophy", "description": "Upgrade 1 Orange Console Cartridge to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_31": {"name": "Digital Collectibles", "description": "Upgrade 3 Orange Console Cartridges to Lvl 10.", "category": "develop", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_32": {"name": "World Dominator!", "description": "Upgrade 3 Orange Console Cartridges to Lvl 20.", "category": "develop", "quality": "high", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_33": {"name": "Definition of Power I", "description": "Upgrade any character's Skill ×5.", "category": "develop", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_34": {"name": "Definition of Power II", "description": "Upgrade any character's Skill ×20.", "category": "develop", "quality": "high", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_35": {"name": "Definition of Power III", "description": "Upgrade any character's Skill ×50.", "category": "develop", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "develop_36": {"name": "Final Turn I", "description": "Upgrade any character's Ultimate level ×5.", "category": "develop", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_37": {"name": "Final Turn II", "description": "Upgrade any character's Ultimate level ×20.", "category": "develop", "quality": "high", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_38": {"name": "Final Turn III", "description": "Upgrade any character's Ultimate level ×50.", "category": "develop", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "develop_39": {"name": "Consecutive Normal Punches I", "description": "Upgrade any character's Basic Attack ×5.", "category": "develop", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_4": {"name": "I Haven't Reached My Limit Yet", "description": "Raise Hunter Level to Lvl 40.", "category": "develop", "quality": "high", "target": 40, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_40": {"name": "Consecutive Normal Punches II", "description": "Upgrade any character's Basic Attack ×20.", "category": "develop", "quality": "high", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_41": {"name": "Consecutive Normal Punches III", "description": "Upgrade any character's Basic Attack ×50.", "category": "develop", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "develop_42": {"name": "Invited Guest", "description": "Obtain a total of 5 characters.", "category": "develop", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_43": {"name": "Travel Companions", "description": "Obtain a total of 10 characters.", "category": "develop", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_44": {"name": "Careful Maintenance", "description": "Obtain 1 Arc.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_45": {"name": "Fine Collection", "description": "Obtain a total of 10 Arcs.", "category": "develop", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_46": {"name": "True Connoisseur", "description": "Obtain a total of 50 Arcs.", "category": "develop", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_47": {"name": "Glory Hunter", "description": "Obtain a total of 100 Arcs.", "category": "develop", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_48": {"name": "Coins Keep Rolling In", "description": "Obtain a total of 100,000 Beetle Coins.", "category": "develop", "quality": "normal", "target": 100000, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_49": {"name": "Big Spender", "description": "Obtain a total of 500,000 Beetle Coins.", "category": "develop", "quality": "normal", "target": 500000, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_5": {"name": "SSS-Class Anomaly Conqueror", "description": "Raise Hunter Level to Lvl 50.", "category": "develop", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "develop_50": {"name": "Walking Collector", "description": "Obtain a total of 1,000,000 Beetle Coins.", "category": "develop", "quality": "normal", "target": 1000000, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_51": {"name": "Entomologist", "description": "Obtain a total of 10,000,000 Beetle Coins.", "category": "develop", "quality": "high", "target": 10000000, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_52": {"name": "Luck and Strategy Combined", "description": "Make 10 rolls.", "category": "develop", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_53": {"name": "Dice Never Leave Your Hand", "description": "Make 100 rolls.", "category": "develop", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "develop_54": {"name": "Fate in Your Hands a Thousand Times", "description": "Make 1,000 rolls.", "category": "develop", "quality": "execellent", "target": 1000, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "develop_6": {"name": "I Acknowledge You as the Strongest", "description": "Raise Hunter Level to Lvl 60.", "category": "develop", "quality": "execellent", "target": 60, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "develop_7": {"name": "One-Man Army I", "description": "Raise 1 character to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_8": {"name": "One-Man Army II", "description": "Raise 1 character to Lvl 40.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "develop_9": {"name": "One-Man Army III", "description": "Raise 1 character to Lvl 60.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_1": {"name": "Oraoraoraoraora!", "description": "Complete Anomaly Commission: Backstreet Boxer", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_10": {"name": "Where Did My Inspiration Go?!", "description": "Complete Anomaly Commission: Inspiration Terminator", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_11": {"name": "Red Light, Green Light", "description": "Complete Anomaly Commission: After School", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_12": {"name": "Rain's Lament", "description": "Complete Anomaly Commission: Prepare for Rain", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_13": {"name": "Pop Art", "description": "Complete Anomaly Commission: Lightspeed Tagger", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_14": {"name": "Cherry Blush Returns to Evening White", "description": "Complete Anomaly Commission: Crimson Hexblade", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_15": {"name": "It's starting to clear up now!", "description": "Complete Anomaly Commission: Dragon Gate Substitute", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_16": {"name": "Blackjack I", "description": "Open 1 \"21's Gift.\"", "category": "explore", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_17": {"name": "Blackjack II", "description": "Open 10 \"21's Gifts.\"", "category": "explore", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_18": {"name": "Blackjack III", "description": "Open 50 \"21's Gifts.\"", "category": "explore", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_19": {"name": "33729 I", "description": "Unlock 1 \"ReroRero Phone Booth.\"", "category": "explore", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_2": {"name": "Muda Muda Muda!", "description": "Complete Anomaly Commission: Rooftop Boxer", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_20": {"name": "33729 II", "description": "Unlock 5 \"ReroRero Phone Booths.\"", "category": "explore", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_21": {"name": "33729 III", "description": "Unlock 10 \"ReroRero Phone Booths.\"", "category": "explore", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_22": {"name": "Orichalcum Cage", "description": "Reach Divination Lvl 3.", "category": "explore", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_23": {"name": "Mithiril Cage", "description": "Reach Divination Lvl 5.", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_24": {"name": "Adamant Cage", "description": "Reach Divination Lvl 10.", "category": "explore", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "explore_25": {"name": "Skyline I", "description": "Unlock a total of 1 Wertheimer Tower.", "category": "explore", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_26": {"name": "Skyline II", "description": "Unlock a total of 5 Wertheimer Towers.", "category": "explore", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_27": {"name": "Urban Life Starting from Zero", "description": "Log in for 1 day in total", "category": "explore", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_28": {"name": "Day Seven", "description": "Log in for 7 days in total", "category": "explore", "quality": "normal", "target": 7, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "explore_29": {"name": "New Hethereau Resident", "description": "Log in for 30 days in total", "category": "explore", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_3": {"name": "Silent Testimony", "description": "Complete Anomaly Commission: Case of Serial Explosion", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_30": {"name": "Tow Shadow", "description": "Complete Anomaly Commission: Invasion! Octopus Trailer!", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_31": {"name": "Ghost of Whitetail Ridge", "description": "Complete Anomaly Commission: Ghost of Whitetail Ridge", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_32": {"name": "Extreme Rally Fanatic", "description": "Complete Anomaly Commission: Ghost Boulevard Wild Rally", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_33": {"name": "Beep, boop boop! Beep boop beep!", "description": "Complete Anomaly Commission: Beat King", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_34": {"name": "Little Bear's Patchwork", "description": "Complete Anomaly Commission: Sad Bears Don't Cry", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_35": {"name": "Quickcharge Compatible", "description": "Complete Anomaly Commission: Power Saver", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_36": {"name": "Night of K.O.", "description": "Complete Anomaly Commission: Party Boxer", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_37": {"name": "ONE ON ONE!", "description": "Complete Anomaly Commission: Court Boxer", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_38": {"name": "Never Stop Leaping", "description": "Complete Anomaly Commission: Heaven's Forsaken", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_39": {"name": "Another round?", "description": "Complete Anomaly Commission: Wandering Puppets", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_4": {"name": "Snap! Don't Blink!", "description": "Complete Anomaly Commission: Brand Anniversary", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_40": {"name": "You Moved First!", "description": "Complete Anomaly Commission: Change Your Perspective?", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_41": {"name": "52 Hertz", "description": "Complete Anomaly Commission: Deep Blue Sorrow", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_42": {"name": "Reverse Mystery Box", "description": "Complete Anomaly Commission: Whose Leak Is This?", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_43": {"name": "What You See Is Real", "description": "Complete Anomaly Commission: This is not a painting", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_44": {"name": "New Colors", "description": "Complete Anomaly Commission: Plot Hole Repair Project", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_45": {"name": "Nightmare Begins", "description": "Reach an ending in Anomaly Commission: Nightmare-Bound", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_46": {"name": "Meow Meow Championship", "description": "Complete Anomaly Commission: Yarn Ball or Fons?", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_47": {"name": "Clear Weather?", "description": "Complete Anomaly Commission: Prayers for Clear Skies", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_48": {"name": "Let It Go", "description": "Complete Anomaly Commission: Clearing After Snowfall", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_49": {"name": "All Good Things Must Come to an End", "description": "Complete Anomaly Commission: Kindness Hard to Refuse", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_5": {"name": "Normal Summon!", "description": "Complete Anomaly Commission: C's Cube", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_50": {"name": "Into the Empty Nest", "description": "Complete Anomaly Commission: Call of the Lost", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_51": {"name": "Butterfly in a Jar", "description": "Complete Anomaly Commission: Let's Go See a Movie", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_54": {"name": "Awakening Moment", "description": "Complete Anomaly Commission: The Book of Post-Adolescence", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_55": {"name": "Cagebreaker", "description": "Complete the Anomaly Commission: Sunken Door", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_56": {"name": "Thus Spoke Sunya", "description": "Complete the Anomaly Commission: Fawn Alone", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_57": {"name": "Falling Petals", "description": "Complete the Anomaly Commission: Twin Blooms", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_58": {"name": "Lights Out", "description": "Complete the Anomaly Commission: Lantern Parade", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_59": {"name": "This Bear Will Lead!", "description": "Complete the Anomaly Commission: Storybook Skirmish", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_6": {"name": "Special Summon!", "description": "Complete Anomaly Commission: C's Tower", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_60": {"name": "Broken Refrain", "description": "Complete the Anomaly Commission: Finale: Da Capo", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_7": {"name": "Welcome!", "description": "Complete Anomaly Commission: Red Door", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_8": {"name": "KeepOut!!", "description": "Complete Anomaly Commission: Ride Assault", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "explore_9": {"name": "In books there lies…", "description": "Complete Anomaly Commission: Video Cemetery", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_11": {"name": "Kiroumaru's Second Caretaker", "description": "Reach Sakiri Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_12": {"name": "Eternal Home", "description": "Reach Sakiri Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_14": {"name": "Friendly Sparring", "description": "Reach Daffodill Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_15": {"name": "As Far as the Eye Can See", "description": "Reach Daffodill Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_17": {"name": "Personal Encyclopedia", "description": "Reach Edgar Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_18": {"name": "Clear Answer", "description": "Reach Edgar Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_2": {"name": "The Combo Called Mint Chocolate", "description": "Reach Mint Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_23": {"name": "Keep It Up, Key Player!", "description": "Reach Nanally Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_24": {"name": "Coluccis' Irreplaceable Right Hand", "description": "Reach Nanally Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_3": {"name": "The Wind That Lingers for You", "description": "Reach Mint Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_32": {"name": "Me?", "description": "Give Mint gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_33": {"name": "All for me?", "description": "Give Mint gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_34": {"name": "Best Buds", "description": "Give Baicang gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_35": {"name": "\"Such Generosity!\"", "description": "Give Baicang gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_36": {"name": "Sunya Care Guide", "description": "Give Adler gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_37": {"name": "Enlightenment Will Remember…", "description": "Give Adler gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_38": {"name": "Beware of Hungry Ghosts", "description": "Give Sakiri gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_39": {"name": "Kiroumaru! No Eating!", "description": "Give Sakiri gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_40": {"name": "Grateful for Your Care", "description": "Give Daffodill gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_41": {"name": "Sincere Intentions", "description": "Give Daffodill gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_42": {"name": "Will Treasure This…", "description": "Give Edgar gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_43": {"name": "Treasured Memories", "description": "Give Edgar gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_46": {"name": "No B-Bribes Allowed!", "description": "Give Nanally gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_47": {"name": "Gladly Accepted!", "description": "Give Nanally gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_5": {"name": "Slacking Buddy", "description": "Reach Baicang Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_54": {"name": "Making Connections", "description": "Reach Affection Lvl 5 with 5 characters total.", "category": "social", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_55": {"name": "Social Butterfly", "description": "Reach Affection Lvl 5 with 10 characters total.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_56": {"name": "Affection Collector", "description": "Reach Affection Lvl 10 with 5 characters total.", "category": "social", "quality": "execellent", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_58": {"name": "Reliable Partner", "description": "Reach Skia Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_59": {"name": "Part of the Routine", "description": "Reach Skia Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_6": {"name": "All Prayers for You", "description": "Reach Baicang Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_60": {"name": "Social Networking", "description": "Give Skia gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_61": {"name": "Too Kind…", "description": "Give Skia gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_68": {"name": "Plans with You", "description": "Reach Hathor Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_69": {"name": "Exclusive Channel", "description": "Reach Hathor Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_70": {"name": "Rationality Offline", "description": "Give Hathor gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_71": {"name": "Delivery of the \"Exception\"", "description": "Give Hathor gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_73": {"name": "Rose's Blessing", "description": "Reach Jiuyuan Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_74": {"name": "Nightingale's Favor", "description": "Reach Jiuyuan Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_75": {"name": "Do Not Feed the Riddler", "description": "Give Jiuyuan gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_76": {"name": "Rose of the Lost", "description": "Give Jiuyuan gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_78": {"name": "Wanna Be Friends?", "description": "Reach Haniel Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_79": {"name": "Sharing Earbuds", "description": "Reach Haniel Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_8": {"name": "The Second Pillar", "description": "Reach Adler Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_80": {"name": "Does this count as free?", "description": "Give Haniel gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_81": {"name": "President of the Paranormal Studies Support Club", "description": "Give Haniel gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_88": {"name": "Within Paradise", "description": "Reach Fadia Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_89": {"name": "Loving Gaze", "description": "Reach Fadia Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_9": {"name": "Highest Priority Order", "description": "Reach Adler Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_90": {"name": "Beware of Addiction", "description": "Give Fadia gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_91": {"name": "Enjoy a Little More", "description": "Give Fadia gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_92": {"name": "Rice Ball Partner", "description": "Reach Chiz Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_93": {"name": "Where My Heart Finds Peace", "description": "Reach Chiz Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_94": {"name": "Exclusive Sales Rep", "description": "Give Chiz gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_95": {"name": "More Precious Than Fons", "description": "Give Chiz gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_96": {"name": "Bond & Harmony", "description": "Reach Aurelia Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "friendship_97": {"name": "Dearest Friend", "description": "Reach Aurelia Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "friendship_98": {"name": "In This Moment", "description": "Gift Aurelia 3 times.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "friendship_99": {"name": "Spring and Sea", "description": "Gift Aurelia 10 times.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_1": {"name": "Water Soluble", "description": "Collapse in water due to stamina depletion.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_10": {"name": "Fluffy Addiction", "description": "Pet small animals ×10.", "category": "interest", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_11": {"name": "Zootopia", "description": "Pet small animals ×50.", "category": "interest", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_12": {"name": "Just One More Pet… Please!", "description": "Pet small animals ×100.", "category": "interest", "quality": "execellent", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "interest_13": {"name": "Witness Journal", "description": "Complete the \"Photography\" series of easter egg quests.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_14": {"name": "Non-Intrusive Documentation", "description": "Complete the \"Bird Enthusiast Guide\" easter egg quest.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "interest_15": {"name": "The Dart's Fantastic Journey", "description": "Complete the \"Find the Dart\" easter egg quest.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_16": {"name": "A Pack of Spicy Sticks Promise", "description": "Complete the \"Little Boy at the Vending Machine\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_17": {"name": "Fake Car Owner Chronicles", "description": "Complete the \"Car Enthusiast\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_18": {"name": "Photo Op with Anomalies", "description": "Complete the \"Anomaly Photographer\" series easter egg quests.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "interest_19": {"name": "World Changer", "description": "Complete the \"The Look of Eclipse\" series easter egg quests.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_2": {"name": "Leap of Faith", "description": "Fall from the wrong height.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_21": {"name": "Broken Machine's Generosity", "description": "Complete the \"Broken Vending Machine\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_22": {"name": "Pets' Secret Cafeteria", "description": "Complete the \"Pet Shop Empty Bowl\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_23": {"name": "Honest Body, Lying Lips", "description": "Complete the \"Radio Listener\" easter egg quest.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_24": {"name": "Vehicle Destroyer", "description": "Complete the \"Vehicle Brothers\" easter egg quest.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "interest_25": {"name": "Gangster Dreams", "description": "Complete the \"The Great Coluccis\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_26": {"name": "Next Time for Sure!", "description": "Complete the \"Deadline Pressure\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_27": {"name": "The Devoured Melody", "description": "Complete the \"Campgrounds - Careless Tourist\" easter egg quest.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_28": {"name": "Multi-Location Check-in Expert", "description": "Complete the \"Other World Tourist\" easter egg quest.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_29": {"name": "The Other Side of the Legend", "description": "Complete the \"Nanally Anomaly Rumors\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_3": {"name": "Fate's Calling", "description": "Perform divination 1 time.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_30": {"name": "Omnic Crisis…?", "description": "Complete the \"Robot Command Conflict\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_31": {"name": "Danzaburou Will Never Be Enslaved!", "description": "Complete the \"Danzaburou\" series easter egg quests.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_32": {"name": "Wind Walker", "description": "Experience a weather change for the first time.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_33": {"name": "The Whisker's Net Catches All", "description": "After summoning Officer Whisker using the Arc Hethereau's Keeper, get punished by the Whisker.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "interest_34": {"name": "Stars Falling Like Rain", "description": "Witness a meteor shower.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "interest_35": {"name": "365 Miles", "description": "Unlock the ReroRero Phone Booth on top of the Midas Arc Workshop.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "interest_36": {"name": "The Sound of Music", "description": "Play the piano.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_37": {"name": "It's 100!", "description": "Roll 100 with dice.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_38": {"name": "I'll Be Back", "description": "Restart at the bonfire 10 times after defeat.", "category": "interest", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_39": {"name": "Mind the Hot Chocolate", "description": "Receive burn damage 33 times.", "category": "interest", "quality": "normal", "target": 33, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_4": {"name": "Worldline Convergence", "description": "Perform 10 Divinations.", "category": "interest", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_40": {"name": "Hold Your Breath", "description": "Take Mold damage 33 times.", "category": "interest", "quality": "normal", "target": 33, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_41": {"name": "Nah, I'd Win", "description": "Fail \"Test Your Luck\" at the Lucky Fuzzy 33 times.", "category": "interest", "quality": "normal", "target": 33, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_42": {"name": "Pinky Promise", "description": "Gaze at the night sky with Linko at Hethereau Skytower, and make a pinky promise beneath the stars.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_5": {"name": "Omens of Impending Change", "description": "Perform 50 Divinations.", "category": "interest", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "interest_6": {"name": "Mysterious Big Eater", "description": "Fill up the Hunger Bar ×5.", "category": "interest", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "interest_7": {"name": "Pac-man!", "description": "Fill up the Hunger Bar ×20.", "category": "interest", "quality": "high", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "interest_8": {"name": "The Art of Inhaling Food", "description": "Fill up the Hunger Bar ×50.", "category": "interest", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "interest_9": {"name": "Can I Pet It!", "description": "Pet small animals 1 time.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_1": {"name": "Trying to Breathe in the Concrete Jungle", "description": "Upgrade Life Skills once.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_100": {"name": "Silent Fall", "description": "Encounter Daffodill training in Miguel District.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_101": {"name": "Hmm! Hmm! Observation Log", "description": "Encounter Edgar at Nacupeda Park, studying and copying Nacupeda.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_102": {"name": "Guardian of Order", "description": "Encounter Skia at the street-side bulletin board, checking the announcements.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_103": {"name": "Thoughtful Butler", "description": "Encounter Adler shopping for groceries at Origami Market.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_104": {"name": "A Glass of Water: Half Full", "description": "Obtain 1 \"Glass of Water\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_105": {"name": "Our Path", "description": "Obtain 1 \"Dream Path\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_106": {"name": "Who Says True Love Weighs You Down?", "description": "Obtain 1 \"Butterflies to Flowers\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_107": {"name": "The Kapé", "description": "Obtain 1 \"Celebrations to: Kapé\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_108": {"name": "Future Audience", "description": "Obtain 1 \"Azure Strings\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_109": {"name": "Because I Love Soccer", "description": "Obtain 1 \"Golden Flying Ball\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_13": {"name": "Business Power, Full Throttle!", "description": "Accumulate 5 stars in Active Tycoon Gameplay.", "category": "city", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_14": {"name": "Chosen by the Stars", "description": "Accumulate 10 stars in Active Tycoon Gameplay.", "category": "city", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_15": {"name": "Golden Age", "description": "Accumulate 20 stars in Active Tycoon Gameplay.", "category": "city", "quality": "execellent", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Life_16": {"name": "Full of Energy!", "description": "Spend a total of 50 City Stamina.", "category": "city", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_17": {"name": "Burn Your Cosmos", "description": "Spend a total of 500 City Stamina.", "category": "city", "quality": "high", "target": 500, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_18": {"name": "High-End Business War", "description": "Spend a total of 2,000 City Stamina.", "category": "city", "quality": "execellent", "target": 2000, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Life_19": {"name": "Mysterious Touch", "description": "Upgrade Anomaly Furniture once.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_2": {"name": "Player One", "description": "Upgrade Life Skills ×10.", "category": "city", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_20": {"name": "Connecting Hearts with Furniture", "description": "Upgrade Anomaly Furniture ×10.", "category": "city", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_21": {"name": "Advanced Revelations", "description": "Upgrade Anomaly Furniture ×50.", "category": "city", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Life_22": {"name": "Going Once, Going Twice", "description": "Participate in 1 auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_23": {"name": "Bidding Fanatic", "description": "Participate in 10 auctions.", "category": "city", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_24": {"name": "Everything Has a Price", "description": "Participate in 100 auctions.", "category": "city", "quality": "execellent", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Life_3": {"name": "Max Out City Life!", "description": "Upgrade Life Skills ×50.", "category": "city", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Life_31": {"name": "Unit-01", "description": "Obtain 1 vehicle.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_32": {"name": "Mobile Trio", "description": "Obtain 3 vehicles.", "category": "city", "quality": "high", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_33": {"name": "The Seven", "description": "Obtain 7 vehicles.", "category": "city", "quality": "execellent", "target": 7, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Life_4": {"name": "Want Some Beans?", "description": "Unlock 1 coffee shop.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_5": {"name": "Coffee Connoisseur in Training", "description": "Unlock 3 coffee shops.", "category": "city", "quality": "high", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_6": {"name": "Regional Flavor Master", "description": "Unlock 5 coffee shops.", "category": "city", "quality": "execellent", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Life_67": {"name": "Real Estate Qualified", "description": "Acquire your first house.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_68": {"name": "Dual Residence Plan", "description": "Obtain a total of 2 houses.", "category": "city", "quality": "high", "target": 2, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_69": {"name": "Life Winner", "description": "Obtain a total of 4 houses.", "category": "city", "quality": "execellent", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Life_70": {"name": "More Than Four Walls", "description": "Acquire furniture for the first time.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_71": {"name": "Storage Enthusiast", "description": "Obtain a total of 50 furniture.", "category": "city", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_72": {"name": "The Room Comes Alive", "description": "Obtain a total of 100 furniture.", "category": "city", "quality": "execellent", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Life_76": {"name": "Every Move is Magic", "description": "Win a prize from the claw machine once.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_77": {"name": "Plushie Maniac", "description": "Successfully grab prizes from the claw machine ×10.", "category": "city", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_78": {"name": "Time to Teach!", "description": "Successfully grab prizes from the claw machine ×100.", "category": "city", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_79": {"name": "I Found The Fons by the Roadside", "description": "Turn in 10 Lost Items to the Security Office.", "category": "city", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_80": {"name": "Honest Finder", "description": "Turn in 50 Lost Items to the Security Office.", "category": "city", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_81": {"name": "The Beacon of Humanity", "description": "Turn in 100 Lost Items to the Security Office.", "category": "city", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_82": {"name": "Brave Passerby", "description": "Stop 10 illegal city activities.", "category": "city", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_83": {"name": "Whisker's Buddy", "description": "Stop 50 illegal city activities.", "category": "city", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Life_84": {"name": "Embodiment of Justice", "description": "Stop 100 illegal city activities.", "category": "city", "quality": "execellent", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Life_88": {"name": "\"At First, I Refused…\"", "description": "Find Mint learning Hair Care at the Pet Store.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_89": {"name": "Direct Hire", "description": "Find Nanally announcing the Coluccis' recruitment everywhere.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_90": {"name": "Bountiful Dreams", "description": "Find Nanally taking a nap.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_91": {"name": "Jelly Belly", "description": "Find Lacrimosa pondering how to carry large amounts of Tomato Jelly.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_92": {"name": "Once Upon a Dream", "description": "Find Lacrimosa fast asleep.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_93": {"name": "Expert Meets Expert", "description": "Find Hotori negotiating prices with the owner at Good Joe.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_94": {"name": "The Queen Wears Landric", "description": "Find Fadia returning from a shopping spree.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_95": {"name": "Speed Bows Down", "description": "Find Jiuyuan surrounded by Regalia service consultants.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_96": {"name": "New Energy Transport", "description": "Find Sakiri riding Kiroumaru around town.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_97": {"name": "Slacking Off Feels So Good", "description": "Find Baicang at Bagel eating his own melon.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_98": {"name": "Path of Balanced Efficiency", "description": "Encounter Hathor at the gas station after she just refueled.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Life_99": {"name": "You too?", "description": "Encounter Haniel cheering in the crowd at a Kokoro Rider show.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_1": {"name": "Mhm!", "description": "Receive Nacupeda's blessing.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_10": {"name": "Infinite Acceleration", "description": "Defeat the Meridian Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_11": {"name": "Wind Barrier", "description": "Defeat the Tempestas Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_12": {"name": "Uncrowned King", "description": "Defeat the Uncrowned King Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_13": {"name": "Lightning Start", "description": "Win your first Racing PVP battle.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_14": {"name": "Speed Demon", "description": "Win 10 Racing PVP battles.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_15": {"name": "King of Speed", "description": "Win 50 Racing PVP battles.", "category": "play", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Play_17": {"name": "Block Vanishing Act", "description": "Place 1st in Tetrominoes.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_18": {"name": "Space Management Master", "description": "Place 1st in Tetrominoes ×10.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_19": {"name": "Less Is More", "description": "Place 1st in Tetrominoes ×30.", "category": "play", "quality": "execellent", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Play_2": {"name": "Mhm! Mhm!", "description": "Receive 10 Nacupeda's blessings.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_27": {"name": "First Pot of Gold", "description": "Collect 10,000 Fons in a single round of Pink Paws Heist.", "category": "play", "quality": "normal", "target": 10000, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_28": {"name": "Fons's Warudo", "description": "Collect 50,000 Fons in a single round of Pink Paws Heist.", "category": "play", "quality": "high", "target": 50000, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_29": {"name": "Ladon Approved", "description": "Collect 100,000 Fons in a single round of Pink Paws Heist.", "category": "play", "quality": "high", "target": 100000, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_3": {"name": "Mhm! Mhm! Mhm!", "description": "Receive 30 Nacupeda's blessings.", "category": "play", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_30": {"name": "First Passenger", "description": "Drive a taxi to pick up 5 passengers.", "category": "play", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}, {"item_id": "Vehicle020_skin_1_Painting", "amount": 1}]}, + "Play_31": {"name": "Swift at Your Service", "description": "Drive a taxi to pick up 10 passengers.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}, {"item_id": "Vehicle020_skin_1_CeilingLight", "amount": 1}]}, + "Play_32": {"name": "Have a Safe Trip", "description": "Drive a taxi to pick up 30 passengers.", "category": "play", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_33": {"name": "First Order", "description": "Complete City Commission ×1.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_34": {"name": "Skilled Courier", "description": "Complete City Commission ×10.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_35": {"name": "City Neighbors", "description": "Complete City Commission ×30.", "category": "play", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_36": {"name": "Hethereau Shores", "description": "Catch a total of 50 fish in Sea Angler.", "category": "play", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_37": {"name": "Every Angler's Dream", "description": "Catch a total of 100 fish in Sea Angler.", "category": "play", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_38": {"name": "Fishing God", "description": "Catch a total of 200 fish in Sea Angler.", "category": "play", "quality": "high", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_39": {"name": "Breakout Together", "description": "Use a special spot in the scene to leave the Detention Facility and successfully escape.", "category": "city", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_4": {"name": "Mhm…?", "description": "Receive Nacupeda's punishment for the first time.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_40": {"name": "Dawn in Detention", "description": "Survive the first round of detention shenanigans and discover one surprise… like finding your socks still match.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}, {"item_id": "PrisonCoin", "amount": 1}]}, + "Play_41": {"name": "Detention Strikes Back", "description": "Complete the next round of detention chaos and uncover two surprises—bonus points if nobody notices you napping.", "category": "city", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}, {"item_id": "PrisonCoin", "amount": 1}]}, + "Play_42": {"name": "Return of the Dawn", "description": "Master all the wacky encounters in detention and reap three unexpected rewards—because yes, even detention has its plot twists.", "category": "city", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}, {"item_id": "Fons", "amount": 10000}]}, + "Play_43": {"name": "Fight Club", "description": "Participate in Fight Club for the first time.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_44": {"name": "Hethereau Brawler King", "description": "Win the championship in Fight Club for the first time.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}, {"item_id": "Title_PrisonFight_001", "amount": 1}]}, + "Play_45": {"name": "Rookie OP", "description": "Earn a total of 10,000 Praise Jams on Bagel.", "category": "play", "quality": "normal", "target": 10000, "rewards": [{"item_id": "Annulith", "amount": 5}, {"item_id": "Title_Bagel_001", "amount": 1}]}, + "Play_46": {"name": "Bagel Celebrity", "description": "Earn a total of 50,000 Praise Jams on Bagel.", "category": "play", "quality": "high", "target": 50000, "rewards": [{"item_id": "Annulith", "amount": 10}, {"item_id": "Title_Bagel_002", "amount": 1}]}, + "Play_47": {"name": "Influential User", "description": "Earn a total of 100,000 Praise Jams on Bagel.", "category": "play", "quality": "execellent", "target": 100000, "rewards": [{"item_id": "Annulith", "amount": 20}, {"item_id": "Title_Bagel_003", "amount": 1}]}, + "Play_48": {"name": "Stormwall", "description": "Defeat Lil' Nado on Easy Sparring.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_49": {"name": "Stormbreaker", "description": "Defeat Lil' Nado on Serious Match.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_5": {"name": "Mhm? Mhm?", "description": "Receive 10 Nacupeda's punishments.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_50": {"name": "I Am the Storm", "description": "Defeat Lil' Nado on Final Bout.", "category": "play", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "Play_51": {"name": "Chest Master", "description": "Open 100 chests.", "category": "play", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_54": {"name": "Appraise It!", "description": "Obtain 7 pieces of equipment with an appraisal value of 33.", "category": "play", "quality": "high", "target": 7, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_55": {"name": "Watch Your Step!", "description": "In the Chocolate Volcano, defeat 3 monsters using lava.", "category": "play", "quality": "high", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_56": {"name": "Continental Breakfast", "description": "Clear Red Mist Invasion, Hop Once: Cake Ace, Hop Twice: Cake Ace, Defend Against Bubbles, Leave None Standing, and Baaastille for the first time.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_57": {"name": "I Choose You!", "description": "Hire Mercenary Fuzzy once.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_58": {"name": "Miracle Spike", "description": "Land one perfect spike once in Volley Star.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_59": {"name": "No Continues", "description": "Clear all the stages in Volley Star.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_6": {"name": "Mhm? Mhm? Mhm?!", "description": "Receive 30 Nacupeda's punishments.", "category": "play", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_60": {"name": "Crowned Champions!", "description": "Win the championship in Volley Star's Championship mode.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_61": {"name": "Court Conqueror", "description": "Reach No. 1 in the Volley Star World Rankings.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_62": {"name": "Global Elite", "description": "Place in the top 10 of the Volley Star World Rankings.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_63": {"name": "Going Pro", "description": "Recruit 3 players in Volley Star.", "category": "play", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_64": {"name": "Full Roster", "description": "Recruit 7 players in Volley Star.", "category": "play", "quality": "high", "target": 7, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_65": {"name": "Maxed Out", "description": "Max out 1 player's talent in Volley Star.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "Play_7": {"name": "End of Fun", "description": "Defeat the Sunday Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_8": {"name": "Wild Mini Cars", "description": "Defeat the Coastal Drift Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Play_9": {"name": "Lightning Speed: Dead End", "description": "Defeat the Full Speed Ahead Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_001": {"name": "Ultimate Anomaly Hunter III", "description": "Upgrade 1 character to Lvl 60.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_002": {"name": "Ultimate Anomaly Hunter II", "description": "Upgrade 1 character to Lvl 40.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_003": {"name": "Ultimate Anomaly Hunter I", "description": "Upgrade 1 character to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_004": {"name": "Brawl III", "description": "Upgrade 1 Arc to Lvl 60.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_005": {"name": "Brawl II", "description": "Upgrade 1 Arc to Lvl 40.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_006": {"name": "Brawl I", "description": "Upgrade 1 Arc to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_007": {"name": "Classic Clear", "description": "Upgrade 1 Orange Console Module to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_008": {"name": "OG Legend", "description": "Upgrade 1 Orange Console Cartridge to Lvl 10.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_009": {"name": "Hyperdimensional Awakening", "description": "Increase Hunter Level to Lvl 35.", "category": "develop", "quality": "normal", "target": 35, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_010": {"name": "Rising Hunter", "description": "Increase Hunter Level to Lvl 25.", "category": "develop", "quality": "normal", "target": 25, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_011": {"name": "Flip-Flopping Future", "description": "Perform 10 Divinations.", "category": "interest", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_012": {"name": "33729", "description": "Unlock 5 ReroRero Phone Booths.", "category": "develop", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_013": {"name": "Mhm? Mhm? Mhm?!", "description": "Receive 30 Nacupeda's Punishments.", "category": "play", "quality": "normal", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_014": {"name": "Mhm! Mhm! Mhm!", "description": "Receive 30 Nacupeda's Blessings.", "category": "play", "quality": "normal", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_015": {"name": "Homecoming Day", "description": "Obtain your first house.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_016": {"name": "First Ride", "description": "Obtain 1 vehicle.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_017": {"name": "Speed Above All", "description": "Trigger Critical Dodge ×200.", "category": "fight", "quality": "normal", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_018": {"name": "Life-Sim", "description": "Upgrade Life Skills ×10.", "category": "develop", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_019": {"name": "Beetle Coin Researcher", "description": "Obtain a total of 1,000,000 Beetle Coins.", "category": "develop", "quality": "normal", "target": 1000000, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_020": {"name": "Hit Me!", "description": "Open 21's Gift ×50.", "category": "develop", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_021": {"name": "Let's Start a Band!", "description": "Complete the A Legendary(?) Band's First Page series quests.", "category": "quest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_022": {"name": "Alea Jacta Est", "description": "Complete the Unforeseen Yet Foretold series quests.", "category": "quest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_023": {"name": "Ultimate Racer", "description": "Obtain 7 vehicles.", "category": "city", "quality": "normal", "target": 7, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_024": {"name": "Neon Master", "description": "Upgrade Life Skills ×50.", "category": "develop", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_025": {"name": "Beetle Bounty", "description": "Obtain a total of 10,000,000 Beetle Coins.", "category": "develop", "quality": "normal", "target": 10000000, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_026": {"name": "Ultimate Anomaly Hunter IV", "description": "Upgrade 1 character to Lvl 80.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_027": {"name": "Brawl IV", "description": "Upgrade 1 Arc to Lvl 80.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_028": {"name": "Limits of 8-bit", "description": "Upgrade 3 Orange Console Cartridges to Lvl 20.", "category": "develop", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_029": {"name": "It's Snowing!", "description": "Complete the A Retrograde Poem for You series quests.", "category": "quest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_030": {"name": "Free Pass", "description": "Unlock a total of 5 Wertheimer Towers.", "category": "explore", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_031": {"name": "Fate's Echo", "description": "Reach Divination Lvl 10.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_032": {"name": "Lone Signal", "description": "Complete Anomaly Commission: Deep Blue Sorrow", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_033": {"name": "It said go for it!", "description": "Defeat enemies with Ultimate ×500.", "category": "fight", "quality": "normal", "target": 500, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_034": {"name": "Affection Master", "description": "Reach Affection Lvl 10 with 5 characters total.", "category": "social", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "Playstation_035": {"name": "Variation Movement", "description": "Cast Redirect Skill ×300.", "category": "fight", "quality": "normal", "target": 300, "rewards": [{"item_id": "Annulith", "amount": 5}]}, + "quest_1": {"name": "Where Dreams Begin!", "description": "Complete the \"Unforeseen Yet Foretold\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_10": {"name": "A Rainy Day in Hethereau", "description": "Complete the \"Weather Watch\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_11": {"name": "\"Win Friends and Influence People\"", "description": "Complete the \"Seeing and Believing\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_12": {"name": "The Wall Artist", "description": "Complete the \"Another Art on the Wall\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_13": {"name": "Alice's Baking Wonderland", "description": "Complete the \"Sweet Cakes, Bitter Cakes…\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_14": {"name": "Rules, Compliance, Containment", "description": "Complete the \"Deal? Deal!\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_15": {"name": "Want to eat… Tomato Jelly", "description": "Complete the \"Tomato Jelly Rampage: Live Report!\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_16": {"name": "Don't Stop Me Now!", "description": "Complete the \"Drift Away, Legend!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_17": {"name": "Beep! Citizen ID", "description": "Complete the \"New Anomaly Citizen ID!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_18": {"name": "The Last Bite is the Setting Sun", "description": "Complete the \"Let's Go, Delicious Investigators!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_19": {"name": "Yesterday Rose as Always", "description": "Complete the \"Books Are Like Old Friends\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_2": {"name": "Photo Studio Fright Night", "description": "Complete the \"Good Business, Better Treasure\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_20": {"name": "It's! Snowing!", "description": "Complete the \"A Retrograde Poem for You\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_21": {"name": "A Letter from the Past", "description": "Complete the \"Miles Apart\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_22": {"name": "Heroes Not Selling, Help Us Out", "description": "Complete the \"Heroes Don't Always Arrive in Moments of Glory\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_24": {"name": "Life Is One Long Journey", "description": "Complete the \"Goodbye, Yellow Flag\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_25": {"name": "The Train Keeps Moving", "description": "Complete the A Legendary(?) Band's First Page series quest.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_29": {"name": "Debts and Dues", "description": "Complete Questline: The Scarlet Letter Analysis.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_3": {"name": "The Moon and Six Fons", "description": "Complete the \"Pawn Moonlight\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_30": {"name": "Detention Blues", "description": "Complete the \"A Breeze Through the Cage\" questline.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_31": {"name": "Dreams Adrift", "description": "Complete the \"Dreamwalk Corridor\" questline.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_32": {"name": "Dragon Hunter", "description": "Clear \"999 Nights\" on Normal difficulty.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}, {"item_id": "Title_FinalTower_001", "amount": 1}]}, + "quest_33": {"name": "Fuzzy Hero", "description": "Clear \"999 Nights\" on Hard difficulty.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}, {"item_id": "Title_FinalTower_002", "amount": 1}]}, + "quest_34": {"name": "Legend of Warren", "description": "Clear \"999 Nights\" on Nightmare difficulty.", "category": "play", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}, {"item_id": "Title_FinalTower_003", "amount": 1}]}, + "quest_36": {"name": "Adventure Awaits", "description": "Leave the Fuzzy Village.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_37": {"name": "One Fuzzy Move Ahead", "description": "Complete Quest: The Fuzzy's Gambit.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_38": {"name": "Gone Bad", "description": "Complete Quest: Fuzzy Club.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_39": {"name": "Nothing Really Happened", "description": "Complete Quest: All Quiet in the Future.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_4": {"name": "Taygedo the Rizzler", "description": "Complete the \"Love that Begins with Lies\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_40": {"name": "Sword or Guitar?", "description": "Complete Quest: Rex Fuzzendae.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_41": {"name": "Fuzzy's Precious! Grrgl!", "description": "Complete Quest: Gone Fuzzy.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_42": {"name": "Hardworking Fuzzy", "description": "Complete Quest: The Hero Is a Part-Timer.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_43": {"name": "Clear Horizon", "description": "Complete the Fogden Game series quests.\n", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_5": {"name": "Just Like Autumn", "description": "Complete the \"Where Words Sprout, Fall Unfolds\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]}, + "quest_6": {"name": "300% Sync Rate!", "description": "Complete the \"We! Are! One!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_7": {"name": "Waste Sorting", "description": "Complete the \"Fluffed Away?!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_8": {"name": "Guess Who I Am?", "description": "Complete the \"Transform! Huhehaha!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}, + "quest_9": {"name": "Small Size, Big Appetite!", "description": "Complete \"The Records Collector\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]} +} diff --git a/src/nte_history_exporter/decoder/achievement.py b/src/nte_history_exporter/decoder/achievement.py index 57e6643..44cd5e9 100644 --- a/src/nte_history_exporter/decoder/achievement.py +++ b/src/nte_history_exporter/decoder/achievement.py @@ -1,5 +1,7 @@ import re import struct +from dataclasses import dataclass +from datetime import datetime, timezone ACHIEVEMENT_RECORD_MARKER = b"AchievementRecord\0" ACHIEVEMENT_ID = re.compile(rb"([A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+)\0") @@ -7,17 +9,58 @@ RECORD_COUNT_OFFSET = 22 RECORDS_OFFSET = 38 COMPLETION_TICKS_OFFSET = 8 MAX_ACHIEVEMENTS = 1_000 +DOTNET_EPOCH_TICKS = 621_355_968_000_000_000 +DOTNET_TICKS_PER_SECOND = 10_000_000 + + +@dataclass(frozen=True) +class AchievementRecord: + achievement_id: str + progress: int + completion_ticks: int + + @property + def completed(self) -> bool: + return self.completion_ticks != 0 + + @property + def status(self) -> str: + if self.completed: + return "completed" + return "in_progress" if self.progress else "not_started" + + @property + def completed_at(self) -> str | None: + if not self.completed: + return None + unix_seconds = ( + self.completion_ticks - DOTNET_EPOCH_TICKS + ) / DOTNET_TICKS_PER_SECOND + try: + return datetime.fromtimestamp(unix_seconds, timezone.utc).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (OverflowError, OSError, ValueError): + return None def extract_achievement_ids(stream: bytes) -> list[str]: + return [ + record.achievement_id + for record in extract_achievement_records(stream) + if record.completed + ] + + +def extract_achievement_records(stream: bytes) -> list[AchievementRecord]: for block in _game_data_blocks(stream): decoded = _decompress_lz4_block(block) marker = decoded.find(ACHIEVEMENT_RECORD_MARKER) if marker < 0: continue - achievement_ids = _completed_achievement_ids(decoded, marker) - if achievement_ids: - return achievement_ids + records = _achievement_records(decoded, marker) + if records: + return records return [] @@ -34,7 +77,7 @@ def reassemble_tcp_segments(segments: list[tuple[int, bytes]]) -> bytes: return bytes(output) -def _completed_achievement_ids(decoded: bytes, marker: int) -> list[str]: +def _achievement_records(decoded: bytes, marker: int) -> list[AchievementRecord]: # The component header stores its record count before a fixed schema descriptor. if marker + RECORDS_OFFSET > len(decoded): return [] @@ -44,15 +87,21 @@ def _completed_achievement_ids(decoded: bytes, marker: int) -> list[str]: matches = list(ACHIEVEMENT_ID.finditer(decoded, marker + RECORDS_OFFSET)) if len(matches) < count: return [] - completed = [] + records = [] for match in matches[:count]: - # Each name is followed by progress, then a zero timestamp until completion. + # Each name is followed by an unsigned progress value and completion ticks. + progress_offset = match.end() ticks_offset = match.end() + COMPLETION_TICKS_OFFSET if ticks_offset + 8 > len(decoded): return [] - if struct.unpack_from(" dict[str, Any]: + categories: dict[str, list[dict[str, Any]]] = {} + scan = { + "in_game": { + "total_achievements": 0, + "completed_achievements": 0, + "in_progress_achievements": 0, + "not_started_achievements": 0, + }, + "playstation": { + "total_achievements": 0, + "completed_achievements": 0, + "in_progress_achievements": 0, + "not_started_achievements": 0, + }, + } + for achievement in achievement_records: + achievement_id = achievement.achievement_id + is_playstation = achievement_id.casefold().startswith("playstation_") + summary = scan["playstation" if is_playstation else "in_game"] + summary["total_achievements"] += 1 + if achievement.completed: + summary["completed_achievements"] += 1 + elif achievement.progress: + summary["in_progress_achievements"] += 1 + else: + summary["not_started_achievements"] += 1 + category, separator, _number = achievement_id.partition("_") + category_key = category.casefold() if separator else "uncategorized" + record = { + "id": achievement_id, + "platform": "playstation" if is_playstation else "in_game", + "status": achievement.status, + "progress": achievement.progress, + "completed": achievement.completed, + "completed_at": achievement.completed_at, + } + metadata = ACHIEVEMENTS_BY_CASEFOLD.get(achievement_id.casefold()) + if metadata: + record.update( + { + "name": metadata["name"], + "description": metadata["description"], + "quality": metadata["quality"], + "target": metadata["target"], + "rewards": metadata["rewards"], + } + ) + categories.setdefault(category_key, []).append(record) + + export: dict[str, Any] = { + "format": "nte-achievement-export", + "format_version": 1, + "game": GAME_NAME, + "source": source, + } + if capture_source: + export["capture_source"] = capture_source + export["exporter"] = {"name": EXPORTER_NAME, "version": __version__} + export["scan"] = scan + + normalized_user_uid = user_uid.strip() if user_uid else "" + normalized_server_id = str(server_id).strip() if server_id else "" + if normalized_user_uid: + export["user_uid"] = normalized_user_uid + if normalized_server_id: + export["server_id"] = normalized_server_id + account_region = account_region_for_server(normalized_server_id) + if account_region: + export["account_region"] = account_region + export["categories"] = categories + return export + + def _record_for_export(row: dict[str, Any]) -> dict[str, Any]: if row.get("pool_group_id") == ARC_BANNER_ID: return { diff --git a/src/nte_history_exporter/live_capture/runner.py b/src/nte_history_exporter/live_capture/runner.py index 904d1a9..4a25c0a 100644 --- a/src/nte_history_exporter/live_capture/runner.py +++ b/src/nte_history_exporter/live_capture/runner.py @@ -9,10 +9,16 @@ from pathlib import Path from nte_history_exporter import console from nte_history_exporter.constants import POOL_META -from nte_history_exporter.decoder.achievement import extract_achievement_ids, reassemble_tcp_segments +from nte_history_exporter.decoder.achievement import ( + extract_achievement_records, + reassemble_tcp_segments, +) from nte_history_exporter.decoder.boundary import annotate_groups, select_continuous_run_from_page_1 from nte_history_exporter.export.csv_export import write_csv -from nte_history_exporter.export.json_export import build_export_json +from nte_history_exporter.export.json_export import ( + build_achievement_export_json, + build_export_json, +) from nte_history_exporter.live_capture.backends import open_capture_backend from nte_history_exporter.live_capture.diagnostics import new_diagnostics_path, write_capture_diagnostics from nte_history_exporter.live_capture.session import LiveHistorySession, UdpPacket @@ -81,7 +87,7 @@ def run_live_capture( reported_missing_pages: dict[str, tuple[int, ...]] = {} active_gap_notices: set[str] = set() tcp_segments: dict[tuple[str, int, str, int], list[tuple[int, bytes]]] = {} - achievement_ids: list[str] = [] + achievement_records = [] try: with StopKeyMonitor() as stop_key: @@ -91,7 +97,7 @@ def run_live_capture( if packet is None: continue if ( - not achievement_ids + not achievement_records and packet.protocol == "tcp" and packet.payload and packet.dst_ip == local_ip @@ -100,16 +106,17 @@ def run_live_capture( tcp_segments.setdefault(flow, []).append( (packet.sequence_number, packet.payload) ) - if not achievement_ids: - achievement_ids = extract_achievement_ids( + if not achievement_records: + achievement_records = extract_achievement_records( reassemble_tcp_segments(tcp_segments[flow]) ) - if achievement_ids: + if achievement_records: + completed = [record for record in achievement_records if record.completed] console.print_achievements_captured( - len(achievement_ids), + len(completed), sum( - value.lower().startswith("playstation_") - for value in achievement_ids + record.achievement_id.lower().startswith("playstation_") + for record in completed ), ) pair_count_before = len(session.pairs) @@ -156,18 +163,26 @@ def run_live_capture( exports = [] achievement_path = None - if achievement_ids: - achievement_path = _achievement_path() - achievement_path.write_text( - json.dumps(achievement_ids, ensure_ascii=False) + "\n", encoding="utf-8" - ) diagnostics_path = None if write_debug_csv: diagnostics_path = new_diagnostics_path() write_capture_diagnostics(diagnostics_path, session.diagnostic_report()) resolved_user_uid = user_uid or session.user_uid - if session.kinds_seen() and not resolved_user_uid: + if (session.kinds_seen() or achievement_records) and not resolved_user_uid: resolved_user_uid = console.prompt_user_uid() + if achievement_records: + achievement_path = _achievement_path(resolved_user_uid) + achievement_export = build_achievement_export_json( + achievement_records, + source="live_capture", + capture_source=CAPTURE_SOURCE_LABELS.get(capture.name, capture.name), + user_uid=resolved_user_uid, + server_id=session.server_id, + ) + achievement_path.write_text( + json.dumps(achievement_export, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) resolved_server_id = session.server_id if session.kinds_seen() and not resolved_server_id: resolved_server_id = console.prompt_server_id() @@ -270,10 +285,11 @@ def run_live_capture( } -def _achievement_path() -> Path: +def _achievement_path(user_uid: str | None = None) -> Path: export_dir = Path("exports") export_dir.mkdir(parents=True, exist_ok=True) - base = export_dir / f"Achievements_{datetime.now():%Y%m%d_%H%M%S}" + uid_prefix = _safe_filename_part(user_uid) if user_uid else "unknown" + base = export_dir / f"{uid_prefix}_Achievements_{datetime.now():%Y%m%d_%H%M%S}" path = base.with_suffix(".json") counter = 2 while path.exists(): diff --git a/src/nte_history_exporter/mapping_update.py b/src/nte_history_exporter/mapping_update.py index 8ff2986..b37b9be 100644 --- a/src/nte_history_exporter/mapping_update.py +++ b/src/nte_history_exporter/mapping_update.py @@ -20,10 +20,11 @@ ASSET_TABLES = { "appearances": "DataTable/Character/Appearance/DT_AppearanceData.json", "illustrations": "DataTable/Gacha/GachaIllustrate.json", "mystery_box_pools": "DataTable/GashaponLottery/DT_GashaponLotteryGlobal.json", + "achievements": "DataTable/DT_AchievementConfigInfo.json", "vehicle_inventory": "DataTable/Vehicle/DT_VehicleItemData.json", "localization": "Localization/en/game.json", } -REWARD_MAPPING_FILES = ("arcs.json", "characters.json", "items.json") +MAPPING_FILES = ("arcs.json", "characters.json", "items.json", "achievements.json") RANK_BY_QUALITY = { "EItemQuality::ITEM_QUALITY_ORANGE": "S", "EItemQuality::ITEM_QUALITY_PURPLE": "A", @@ -103,7 +104,7 @@ def load_assets(*, assets_root: Path | None = None, source_ref: str = DEFAULT_SO def load_current_mappings(directory: Path) -> dict[str, dict[str, dict[str, Any]]]: mappings = {} - for filename in REWARD_MAPPING_FILES: + for filename in MAPPING_FILES: path = directory / filename try: value = json.loads(path.read_text(encoding="utf-8")) @@ -126,13 +127,16 @@ def build_mapping_update( "arcs.json": _build_primary_mapping(assets.tables["arcs"], "arc", translations), "characters.json": _build_primary_mapping(assets.tables["characters"], "character", translations), "items.json": _build_item_mapping(assets.tables, translations), + "achievements.json": _build_achievement_mapping( + assets.tables["achievements"], translations + ), } validate_mappings(output) additions: dict[str, list[str]] = {} updates: dict[str, list[str]] = {} deletions: dict[str, list[str]] = {} - for filename in REWARD_MAPPING_FILES: + for filename in MAPPING_FILES: old = current[filename] new = output[filename] additions[filename] = sorted(new.keys() - old.keys(), key=str.casefold) @@ -169,7 +173,7 @@ def build_mapping_update( def write_update(result: MappingUpdateResult, directory: Path) -> None: directory.mkdir(parents=True, exist_ok=True) - for filename in REWARD_MAPPING_FILES: + for filename in MAPPING_FILES: _atomic_write(directory / filename, _dump_mapping(result.mappings[filename])) report_text = json.dumps(result.report, ensure_ascii=False, indent=2, sort_keys=True) + "\n" _atomic_write(directory / "mapping-update-report.json", report_text) @@ -180,11 +184,11 @@ def apply_update(result: MappingUpdateResult, directory: Path) -> None: directory.mkdir(parents=True, exist_ok=True) originals: dict[Path, bytes | None] = {} written: list[Path] = [] - for filename in REWARD_MAPPING_FILES: + for filename in MAPPING_FILES: path = directory / filename originals[path] = path.read_bytes() if path.exists() else None try: - for filename in REWARD_MAPPING_FILES: + for filename in MAPPING_FILES: path = directory / filename _atomic_write(path, _dump_mapping(result.mappings[filename])) written.append(path) @@ -199,11 +203,11 @@ def apply_update(result: MappingUpdateResult, directory: Path) -> None: def validate_mappings(mappings: dict[str, dict[str, dict[str, Any]]]) -> None: - if set(mappings) != set(REWARD_MAPPING_FILES): - raise MappingUpdateError(f"mapping set must be exactly {REWARD_MAPPING_FILES}") + if set(mappings) != set(MAPPING_FILES): + raise MappingUpdateError(f"mapping set must be exactly {MAPPING_FILES}") locations: dict[str, str] = {} folded: dict[str, str] = {} - for filename in REWARD_MAPPING_FILES: + for filename in MAPPING_FILES: entries = mappings[filename] if not isinstance(entries, dict): raise MappingUpdateError(f"{filename} must contain an object") @@ -232,6 +236,33 @@ def validate_mappings(mappings: dict[str, dict[str, dict[str, Any]]]) -> None: raise MappingUpdateError(f"{filename}:{item_id}.type must be item or cosmetic") if rank not in {"S", "A", "B"}: raise MappingUpdateError(f"{filename}:{item_id}.rank must be S, A, or B") + if filename == "achievements.json": + _validate_achievement_meta(filename, item_id, meta) + + +def _validate_achievement_meta(filename: str, item_id: str, meta: dict[str, Any]) -> None: + for field in ("description", "category", "quality"): + if not isinstance(meta.get(field), str) or not meta[field].strip(): + raise MappingUpdateError( + f"{filename}:{item_id}.{field} must be a non-empty string" + ) + target = meta.get("target") + if not isinstance(target, (int, float)) or isinstance(target, bool) or target < 0: + raise MappingUpdateError(f"{filename}:{item_id}.target must be a non-negative number") + rewards = meta.get("rewards") + if not isinstance(rewards, list): + raise MappingUpdateError(f"{filename}:{item_id}.rewards must be a list") + for index, reward in enumerate(rewards): + if ( + not isinstance(reward, dict) + or not isinstance(reward.get("item_id"), str) + or not reward["item_id"] + or not isinstance(reward.get("amount"), (int, float)) + or isinstance(reward.get("amount"), bool) + ): + raise MappingUpdateError( + f"{filename}:{item_id}.rewards[{index}] is invalid" + ) def _extract_rows(document: Any, relative_path: str) -> dict[str, Any]: @@ -289,7 +320,12 @@ def _build_item_mapping( candidate_ids = _reward_candidate_ids(tables) for candidate_id in candidate_ids: folded = candidate_id.casefold() - if candidate_id.isdigit() or folded.startswith("fork_") or folded.startswith("characterawaken_"): + if ( + candidate_id.isdigit() + or folded.startswith("fork_") + or folded.startswith("characterawaken_") + or folded.startswith("fashion_vehicle_") + ): continue sources = (inventory, capital, appearances, vehicle_inventory) match = next((source.get(folded) for source in sources if folded in source), None) @@ -307,6 +343,62 @@ def _build_item_mapping( return dict(sorted(result.items(), key=lambda pair: (pair[0].casefold(), pair[0]))) +def _build_achievement_mapping( + rows: dict[str, Any], + translations: dict[str, list[tuple[str, str]]], +) -> dict[str, dict[str, Any]]: + result = {} + for achievement_id in sorted(rows, key=lambda value: (value.casefold(), value)): + row = rows[achievement_id] + if not isinstance(row, dict): + raise MappingUpdateError( + f"NTE_Assets achievement row {achievement_id} must be an object" + ) + title = row.get("TitleId") + description = row.get("ContentID") + if not isinstance(title, dict) or not isinstance(description, dict): + raise MappingUpdateError( + f"NTE_Assets achievement row {achievement_id} has no title or description" + ) + target = row.get("Amout") + if not isinstance(target, (int, float)) or isinstance(target, bool): + raise MappingUpdateError( + f"NTE_Assets achievement row {achievement_id} has invalid Amout" + ) + main_type = row.get("AchievementMainType") + quality = row.get("Quality") + if not isinstance(main_type, str) or "MainType" not in main_type: + raise MappingUpdateError( + f"NTE_Assets achievement row {achievement_id} has invalid main type" + ) + if not isinstance(quality, str) or "::" not in quality: + raise MappingUpdateError( + f"NTE_Assets achievement row {achievement_id} has invalid quality" + ) + rewards = [] + for index, award in enumerate(row.get("AwardInfo", [])): + if not isinstance(award, dict): + raise MappingUpdateError( + f"NTE_Assets achievement row {achievement_id} award {index} is invalid" + ) + item_id = award.get("ItemID") + amount = award.get("Amount") + if not isinstance(item_id, str) or not item_id or not isinstance(amount, (int, float)): + raise MappingUpdateError( + f"NTE_Assets achievement row {achievement_id} award {index} is invalid" + ) + rewards.append({"item_id": item_id, "amount": amount}) + result[achievement_id] = { + "name": _translate_name(achievement_id, title, translations), + "description": _translate_name(achievement_id, description, translations), + "category": main_type.rsplit("MainType", 1)[-1].casefold(), + "quality": quality.rsplit("::", 1)[-1].casefold(), + "target": target, + "rewards": rewards, + } + return result + + def _reward_candidate_ids(tables: dict[str, dict[str, Any]]) -> list[str]: """Return rewards used by ordinary Gacha and every Mystery Box rotation.""" candidates: dict[str, str] = { diff --git a/src/nte_history_exporter/mappings.py b/src/nte_history_exporter/mappings.py index 27213a1..7165a9e 100644 --- a/src/nte_history_exporter/mappings.py +++ b/src/nte_history_exporter/mappings.py @@ -16,6 +16,10 @@ def load_mapping_file(filename: str) -> dict[str, Any]: ARC_META: dict[str, dict[str, Any]] = load_mapping_file("arcs.json") CHARACTERS: dict[str, dict[str, Any]] = load_mapping_file("characters.json") ITEMS: dict[str, dict[str, Any]] = load_mapping_file("items.json") +ACHIEVEMENTS: dict[str, dict[str, Any]] = load_mapping_file("achievements.json") +ACHIEVEMENTS_BY_CASEFOLD = { + achievement_id.casefold(): meta for achievement_id, meta in ACHIEVEMENTS.items() +} # Reward keys in Monopoly history packets are the reward id string itself # (ASCII*4 with carry, see decoder.protocol.decode_reward_key), so all reward diff --git a/tests/fixtures/nte_assets/DataTable/DT_AchievementConfigInfo.json b/tests/fixtures/nte_assets/DataTable/DT_AchievementConfigInfo.json new file mode 100644 index 0000000..ff68862 --- /dev/null +++ b/tests/fixtures/nte_assets/DataTable/DT_AchievementConfigInfo.json @@ -0,0 +1,22 @@ +[ + { + "Rows": { + "battle_25": { + "Amout": 50, + "AchievementMainType": "AchievementMainType::MainTypeFight", + "AwardInfo": [ + {"ItemID": "Annulith", "Amount": 10} + ], + "TitleId": { + "TableId": "/Game/Text/ST_Achievement.ST_Achievement", + "Key": "battle_25_Title" + }, + "ContentID": { + "TableId": "/Game/Text/ST_Achievement.ST_Achievement", + "Key": "battle_25_Content" + }, + "Quality": "EHTAchievementQuality::High" + } + } + } +] diff --git a/tests/fixtures/nte_assets/Localization/en/game.json b/tests/fixtures/nte_assets/Localization/en/game.json index 5bda83c..0bbd71e 100644 --- a/tests/fixtures/nte_assets/Localization/en/game.json +++ b/tests/fixtures/nte_assets/Localization/en/game.json @@ -1,4 +1,8 @@ { + "ST_Achievement": { + "battle_25_Title": "Devil Within II", + "battle_25_Content": "Trigger Hexed ×50." + }, "ST_Player": { "character_1003": "Old Character Renamed", "character_1099": "New Character" diff --git a/tests/test_achievement.py b/tests/test_achievement.py index 49e52cc..531895f 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -4,6 +4,7 @@ import unittest from nte_history_exporter.decoder.achievement import ( ACHIEVEMENT_RECORD_MARKER, extract_achievement_ids, + extract_achievement_records, reassemble_tcp_segments, ) @@ -22,6 +23,13 @@ class AchievementPacketTests(unittest.TestCase): self.assertEqual(extract_achievement_ids(stream), ["Battle_30"]) self.assertEqual(extract_achievement_ids(b"\x01\0\0\0\0"), []) + records = extract_achievement_records(stream) + self.assertEqual(len(records), 2) + self.assertEqual(records[0].achievement_id, "Battle_30") + self.assertTrue(records[0].completed) + self.assertEqual(records[1].achievement_id, "Playstation_035") + self.assertEqual(records[1].status, "not_started") + def test_reassembles_tcp_data(self): self.assertEqual( reassemble_tcp_segments([(104, b"bar"), (100, b"foobar"), (100, b"foobar")]), diff --git a/tests/test_export_contract.py b/tests/test_export_contract.py index 24441ed..522e2b4 100644 --- a/tests/test_export_contract.py +++ b/tests/test_export_contract.py @@ -1,5 +1,8 @@ from tests.support import * # noqa: F401,F403 from nte_history_exporter import __version__ +from nte_history_exporter.decoder.achievement import AchievementRecord +from nte_history_exporter.export.json_export import build_achievement_export_json +from nte_history_exporter.live_capture.runner import _achievement_path class ExportContractTests(unittest.TestCase): @@ -117,6 +120,83 @@ class ExportContractTests(unittest.TestCase): r"^218216016349_Limited_\d{8}_\d{6}(?:_\d+)?\.json$", ) + def test_achievement_path_includes_user_uid_and_timestamp(self): + path = _achievement_path("218216016349") + + self.assertRegex( + path.name, + r"^218216016349_Achievements_\d{8}_\d{6}(?:_\d+)?\.json$", + ) + + def test_achievement_export_has_versioned_metadata_and_records(self): + export = build_achievement_export_json( + [ + AchievementRecord("Battle_30", 10, 639_136_215_846_370_000), + AchievementRecord("Battle_25", 20, 0), + AchievementRecord("Playstation_017", 0, 0), + ], + source="live_capture", + capture_source="npcap", + user_uid="218216016349", + server_id="23003", + ) + + self.assertEqual(export["format"], "nte-achievement-export") + self.assertEqual(export["format_version"], 1) + self.assertEqual(export["capture_source"], "npcap") + self.assertEqual(export["user_uid"], "218216016349") + self.assertEqual(export["account_region"], "EU") + self.assertEqual( + export["scan"], + { + "in_game": { + "total_achievements": 2, + "completed_achievements": 1, + "in_progress_achievements": 1, + "not_started_achievements": 0, + }, + "playstation": { + "total_achievements": 1, + "completed_achievements": 0, + "in_progress_achievements": 0, + "not_started_achievements": 1, + }, + }, + ) + battle_30, battle_25 = export["categories"]["battle"] + self.assertEqual(battle_30["name"], "Death Nova I") + self.assertEqual(battle_30["completed_at"], "2026-05-05 23:46:24") + self.assertEqual(battle_25["name"], "Devil Within II") + self.assertEqual(battle_25["description"], "Trigger Hexed ×50.") + self.assertEqual(battle_25["progress"], 20) + self.assertEqual(battle_25["target"], 50) + self.assertEqual(battle_25["quality"], "high") + self.assertEqual( + battle_25["rewards"], [{"item_id": "Annulith", "amount": 10}] + ) + self.assertEqual( + export["categories"]["playstation"][0]["name"], "Speed Above All" + ) + + def test_unmapped_achievement_still_exports_capture_data(self): + export = build_achievement_export_json( + [AchievementRecord("FutureCategory_999", 7, 0)] + ) + + self.assertEqual( + export["categories"]["futurecategory"], + [ + { + "id": "FutureCategory_999", + "platform": "in_game", + "status": "in_progress", + "progress": 7, + "completed": False, + "completed_at": None, + } + ], + ) + def test_extracts_user_uid_from_record_context(self): payload = ( b"\x00" * 24 diff --git a/tests/test_mapping_update.py b/tests/test_mapping_update.py index 71c59cb..a770148 100644 --- a/tests/test_mapping_update.py +++ b/tests/test_mapping_update.py @@ -36,6 +36,7 @@ def sample_current(): "DiceNormal": {"type": "item", "name": "Fabricated Dice", "rank": "S"}, "OldTicket": {"type": "item", "name": "Retired Ticket", "rank": "B"}, }, + "achievements.json": {}, } @@ -54,11 +55,22 @@ class MappingUpdateTests(unittest.TestCase): self.assertNotIn("fork_retired", result.mappings["arcs.json"]) self.assertNotIn("OldTicket", result.mappings["items.json"]) self.assertNotEqual(result.mappings["characters.json"]["1003"], current["characters.json"]["1003"]) - self.assertEqual(result.report["changes"], {"additions": 4, "updates": 2, "deletions": 3}) + self.assertEqual(result.report["changes"], {"additions": 5, "updates": 2, "deletions": 3}) self.assertIs(result.report["safety"]["authoritative_snapshot"], True) self.assertIs(result.report["safety"]["deletions_allowed"], True) self.assertIs(result.report["safety"]["uid_inputs_touched"], False) self.assertIs(result.report["safety"]["pool_mappings_touched"], False) + self.assertEqual( + result.mappings["achievements.json"]["battle_25"], + { + "name": "Devil Within II", + "description": "Trigger Hexed ×50.", + "category": "fight", + "quality": "high", + "target": 50, + "rewards": [{"item_id": "Annulith", "amount": 10}], + }, + ) def test_items_use_illustrations_as_filter_and_asset_tables_as_authority(self): _current, result = build_sample_update() @@ -71,6 +83,7 @@ class MappingUpdateTests(unittest.TestCase): self.assertNotIn("DIceNormal", result.mappings["items.json"]) self.assertNotIn("UnusedItem", result.mappings["items.json"]) self.assertFalse(any(key.startswith("Characterawaken_") for key in result.mappings["items.json"])) + self.assertFalse(any(key.startswith("Fashion_vehicle_") for key in result.mappings["items.json"])) def test_future_mystery_box_pool_is_discovered_without_hard_coded_event_id(self): assets = load_assets(assets_root=SAMPLE_ASSETS) diff --git a/tests/test_mappings_and_updates.py b/tests/test_mappings_and_updates.py index 4b9cb35..b81b61f 100644 --- a/tests/test_mappings_and_updates.py +++ b/tests/test_mappings_and_updates.py @@ -1,4 +1,5 @@ from tests.support import * # noqa: F401,F403 +from nte_history_exporter.mappings import ACHIEVEMENTS class MappingAndUpdateTests(unittest.TestCase): @@ -79,6 +80,14 @@ class MappingAndUpdateTests(unittest.TestCase): self.assertIn("name", info) self.assertIn(info.get("rank"), ("S", "A")) + self.assertTrue(ACHIEVEMENTS) + battle_25 = ACHIEVEMENTS["battle_25"] + self.assertEqual(battle_25["name"], "Devil Within II") + self.assertEqual(battle_25["target"], 50) + self.assertEqual( + battle_25["rewards"], [{"item_id": "Annulith", "amount": 10}] + ) + self.assertTrue(ITEMS) for item_id, info in ITEMS.items(): with self.subTest(item_id=item_id): diff --git a/tools/update_mappings.py b/tools/update_mappings.py index 9d71a80..df0a001 100644 --- a/tools/update_mappings.py +++ b/tools/update_mappings.py @@ -78,7 +78,10 @@ def main(argv: list[str] | None = None) -> int: f"{changes['deletions']} deletions" ) if args.apply: - print("applied: mappings/arcs.json, mappings/characters.json, mappings/items.json") + print( + "applied: mappings/arcs.json, mappings/characters.json, " + "mappings/items.json, mappings/achievements.json" + ) return 1 if args.check and result.change_count else 0