diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/00-\346\201\242\345\244\215\347\203\255\350\203\275-\350\207\263\345\206\254-\344\270\203\345\244\251\347\245\236\345\203\217.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/00-\346\201\242\345\244\215\347\203\255\350\203\275-\350\207\263\345\206\254-\344\270\203\345\244\251\347\245\236\345\203\217.json" new file mode 100644 index 0000000000..d54250b28a --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/00-\346\201\242\345\244\215\347\203\255\350\203\275-\350\207\263\345\206\254-\344\270\203\345\244\251\347\245\236\345\203\217.json" @@ -0,0 +1,31 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.64.0", + "description": "恢复热能值", + "enable_monster_loot_split": false, + "last_modified_time": 1788353423190, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "00-恢复热能-至冬-七天神像", + "tags": [], + "type": "collect", + "version": "1.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "teleport", + "x": 7842.2702, + "y": 7608.7705 + } + ] +} diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/01-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\345\214\227\345\233\275\351\223\266\350\241\214\351\227\250\345\217\243-6\350\265\236.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/01-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\345\214\227\345\233\275\351\223\266\350\241\214\351\227\250\345\217\243-6\350\265\236.json" new file mode 100644 index 0000000000..f4b7eccf90 --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/01-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\345\214\227\345\233\275\351\223\266\350\241\214\351\227\250\345\217\243-6\350\265\236.json" @@ -0,0 +1,60 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.64.0", + "description": "北国银行门口3人", + "enable_monster_loot_split": false, + "last_modified_time": 1788355062407, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "01-冰造物点赞-至冬-北国银行门口-6赞", + "tags": [ + "点赞=6" + ], + "type": "collect", + "version": "2.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "teleport", + "x": 6583.8818, + "y": 9576.1563 + }, + { + "action": "", + "action_params": "", + "id": 2, + "move_mode": "walk", + "type": "path", + "x": 6578.7275, + "y": 9599.0156 + }, + { + "action": "", + "action_params": "", + "id": 3, + "move_mode": "walk", + "type": "target", + "x": 6561.6884, + "y": 9596.9531 + }, + { + "action": "", + "action_params": "", + "id": 4, + "move_mode": "walk", + "type": "orientation", + "x": 6579.8499, + "y": 9479.9063 + } + ] +} diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/02-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\345\215\260\350\256\260\345\225\206\345\272\227\351\227\250\345\217\243-8\350\265\236.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/02-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\345\215\260\350\256\260\345\225\206\345\272\227\351\227\250\345\217\243-8\350\265\236.json" new file mode 100644 index 0000000000..0573dbe19e --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/02-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\345\215\260\350\256\260\345\225\206\345\272\227\351\227\250\345\217\243-8\350\265\236.json" @@ -0,0 +1,78 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.64.0", + "description": "印记商店门口4人", + "enable_monster_loot_split": false, + "last_modified_time": 1788414454217, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "02-冰造物点赞-至冬-印记商店门口-8赞", + "tags": [ + "点赞=8" + ], + "type": "collect", + "version": "1.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "teleport", + "x": 6583.8818, + "y": 9576.1563 + }, + { + "action": "", + "action_params": "", + "id": 2, + "move_mode": "walk", + "type": "path", + "x": 6581.7969, + "y": 9564.8125 + }, + { + "action": "", + "action_params": "", + "id": 3, + "move_mode": "walk", + "type": "path", + "x": 6587.6406, + "y": 9543.3281 + }, + { + "action": "", + "action_params": "", + "id": 4, + "move_mode": "walk", + "type": "path", + "x": 6555.1563, + "y": 9535.7656 + }, + { + "action": "", + "action_params": "", + "id": 5, + "move_mode": "walk", + "type": "target", + "x": 6550.0007, + "y": 9528.5469 + }, + { + "action": "", + "action_params": "", + "id": 6, + "move_mode": "walk", + "type": "orientation", + "x": 6525.5932, + "y": 9475.2656 + } + ] +} diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/03-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\347\202\211\347\201\260\351\243\237\345\240\202\351\227\250\345\217\243-4\350\265\236.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/03-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\347\202\211\347\201\260\351\243\237\345\240\202\351\227\250\345\217\243-4\350\265\236.json" new file mode 100644 index 0000000000..038c38d685 --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/03-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\347\202\211\347\201\260\351\243\237\345\240\202\351\227\250\345\217\243-4\350\265\236.json" @@ -0,0 +1,60 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.64.0", + "description": "炉灰食堂门口2人", + "enable_monster_loot_split": false, + "last_modified_time": 1788491253964, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "03-冰造物点赞-至冬-炉灰食堂门口-4赞", + "tags": [ + "点赞=4" + ], + "type": "collect", + "version": "3.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "teleport", + "x": 6864.9617, + "y": 9655.6484 + }, + { + "action": "", + "action_params": "", + "id": 2, + "move_mode": "walk", + "type": "path", + "x": 6860.751, + "y": 9644.3906 + }, + { + "action": "", + "action_params": "", + "id": 3, + "move_mode": "walk", + "type": "target", + "x": 6863.1557, + "y": 9635.625 + }, + { + "action": "", + "action_params": "", + "id": 4, + "move_mode": "walk", + "type": "orientation", + "x": 6861.0985, + "y": 9565.1563 + } + ] +} diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/04-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\351\244\220\345\216\205\351\227\250\345\217\243-2\350\265\236.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/04-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\351\244\220\345\216\205\351\227\250\345\217\243-2\350\265\236.json" new file mode 100644 index 0000000000..037b528377 --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/04-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\351\244\220\345\216\205\351\227\250\345\217\243-2\350\265\236.json" @@ -0,0 +1,51 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.64.0", + "description": "餐厅门口1人", + "enable_monster_loot_split": false, + "last_modified_time": 1788491196629, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "04-冰造物点赞-至冬-餐厅门口-2赞", + "tags": [ + "点赞=2" + ], + "type": "collect", + "version": "1.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "teleport", + "x": 6583.8818, + "y": 9576.1563 + }, + { + "action": "", + "action_params": "", + "id": 2, + "move_mode": "walk", + "type": "target", + "x": 6628.7171, + "y": 9574.4375 + }, + { + "action": "", + "action_params": "", + "id": 3, + "move_mode": "walk", + "type": "orientation", + "x": 6651.5853, + "y": 9574.2656 + } + ] +} diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/05-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\351\223\201\345\214\240\351\231\204\350\277\221-2\350\265\236.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/05-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\351\223\201\345\214\240\351\231\204\350\277\221-2\350\265\236.json" new file mode 100644 index 0000000000..8c2a8ab1be --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/05-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\351\223\201\345\214\240\351\231\204\350\277\221-2\350\265\236.json" @@ -0,0 +1,78 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.64.0", + "description": "铁匠", + "enable_monster_loot_split": false, + "last_modified_time": 1788353313560, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "05-冰造物点赞-至冬-铁匠附近-2赞", + "tags": [ + "点赞=2" + ], + "type": "collect", + "version": "2.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "teleport", + "x": 6688.624, + "y": 9624.9688 + }, + { + "action": "", + "action_params": "", + "id": 2, + "move_mode": "walk", + "type": "path", + "x": 6682.7825, + "y": 9607.4375 + }, + { + "action": "", + "action_params": "", + "id": 3, + "move_mode": "walk", + "type": "path", + "x": 6681.9992, + "y": 9592.1406 + }, + { + "action": "", + "action_params": "", + "id": 4, + "move_mode": "walk", + "type": "path", + "x": 6683.2895, + "y": 9585.8672 + }, + { + "action": "", + "action_params": "", + "id": 5, + "move_mode": "walk", + "type": "target", + "x": 6686.9834, + "y": 9580.625 + }, + { + "action": "", + "action_params": "", + "id": 6, + "move_mode": "walk", + "type": "orientation", + "x": 6706.4021, + "y": 9558.4531 + } + ] +} diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/06-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\346\233\231\345\205\211\350\275\246\347\253\231\347\216\251\345\205\267\351\231\204\350\277\221-4\350\265\236.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/06-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\346\233\231\345\205\211\350\275\246\347\253\231\347\216\251\345\205\267\351\231\204\350\277\221-4\350\265\236.json" new file mode 100644 index 0000000000..18dcffd7bb --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/06-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\346\233\231\345\205\211\350\275\246\347\253\231\347\216\251\345\205\267\351\231\204\350\277\221-4\350\265\236.json" @@ -0,0 +1,69 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.64.0", + "description": "曙光车站玩具2人", + "enable_monster_loot_split": false, + "last_modified_time": 1788354846394, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "06-冰造物点赞-至冬-曙光车站玩具附近-4赞", + "tags": [ + "点赞=4" + ], + "type": "collect", + "version": "3.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "teleport", + "x": 7813.7715, + "y": 8520.4678 + }, + { + "action": "", + "action_params": "", + "id": 2, + "move_mode": "walk", + "type": "path", + "x": 7808.5151, + "y": 8513.9688 + }, + { + "action": "", + "action_params": "", + "id": 3, + "move_mode": "walk", + "type": "path", + "x": 7793.3958, + "y": 8517.4063 + }, + { + "action": "", + "action_params": "", + "id": 4, + "move_mode": "walk", + "type": "target", + "x": 7792.365, + "y": 8511.5625 + }, + { + "action": "", + "action_params": "", + "id": 5, + "move_mode": "walk", + "type": "orientation", + "x": 7727.0131, + "y": 8472.375 + } + ] +} diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/07-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\351\244\220\345\216\205-2\350\265\236.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/07-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\351\244\220\345\216\205-2\350\265\236.json" new file mode 100644 index 0000000000..dc846c2631 --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/07-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\351\244\220\345\216\205-2\350\265\236.json" @@ -0,0 +1,69 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.64.0", + "description": "车站餐厅1人", + "enable_monster_loot_split": false, + "last_modified_time": 1788415619283, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "07-冰造物点赞-至冬-车站餐厅-2赞", + "tags": [ + "点赞=2" + ], + "type": "collect", + "version": "1.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "teleport", + "x": 7813.7715, + "y": 8520.4678 + }, + { + "action": "", + "action_params": "", + "id": 2, + "move_mode": "walk", + "type": "path", + "x": 7828.5762, + "y": 8517.2344 + }, + { + "action": "", + "action_params": "", + "id": 3, + "move_mode": "walk", + "type": "path", + "x": 7833.4082, + "y": 8511.2188 + }, + { + "action": "", + "action_params": "", + "id": 4, + "move_mode": "walk", + "type": "target", + "x": 7839.937, + "y": 8497.9844 + }, + { + "action": "", + "action_params": "", + "id": 5, + "move_mode": "walk", + "type": "orientation", + "x": 7799.9284, + "y": 8457.25 + } + ] +} diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/08-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\347\201\253\347\202\254\350\276\271-6\350\265\236.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/08-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\347\201\253\347\202\254\350\276\271-6\350\265\236.json" new file mode 100644 index 0000000000..0543b817a1 --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/08-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\347\201\253\347\202\254\350\276\271-6\350\265\236.json" @@ -0,0 +1,60 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.45.0", + "description": "车站火炬边上3人", + "enable_monster_loot_split": false, + "last_modified_time": 1788489561244, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "08-冰造物点赞-至冬-车站火炬边-6赞", + "tags": [ + "点赞=6" + ], + "type": "collect", + "version": "1.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "path", + "x": 7839.937, + "y": 8497.9844 + }, + { + "action": "", + "action_params": "", + "id": 2, + "move_mode": "walk", + "type": "path", + "x": 7823.9371, + "y": 8461.6328 + }, + { + "action": "", + "action_params": "", + "id": 3, + "move_mode": "jump", + "type": "target", + "x": 7829.6087, + "y": 8455.1016 + }, + { + "action": "", + "action_params": "", + "id": 4, + "move_mode": "walk", + "type": "orientation", + "x": 7875.4996, + "y": 8473.4063 + } + ] +} diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/09-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\345\217\260\345\255\220-2\350\265\236.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/09-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\345\217\260\345\255\220-2\350\265\236.json" new file mode 100644 index 0000000000..1832480b14 --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/09-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\345\217\260\345\255\220-2\350\265\236.json" @@ -0,0 +1,78 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.64.0", + "description": "车站台子1人", + "enable_monster_loot_split": false, + "last_modified_time": 1788415794723, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "09-冰造物点赞-至冬-车站台子-2赞", + "tags": [ + "点赞=2" + ], + "type": "collect", + "version": "1.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "path", + "x": 7822.0635, + "y": 8455.875 + }, + { + "action": "", + "action_params": "", + "id": 2, + "move_mode": "walk", + "type": "path", + "x": 7813.1378, + "y": 8455.875 + }, + { + "action": "", + "action_params": "", + "id": 3, + "move_mode": "walk", + "type": "path", + "x": 7811.7338, + "y": 8469.2696 + }, + { + "action": "", + "action_params": "", + "id": 4, + "move_mode": "walk", + "type": "path", + "x": 7796.6094, + "y": 8470.6563 + }, + { + "action": "", + "action_params": "", + "id": 5, + "move_mode": "walk", + "type": "target", + "x": 7796.5229, + "y": 8474.4375 + }, + { + "action": "", + "action_params": "", + "id": 6, + "move_mode": "walk", + "type": "orientation", + "x": 7789.0467, + "y": 8495.3203 + } + ] +} diff --git "a/repo/js/IceObjectNpcLike/Assets/Pathing/09-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\345\217\260\345\255\220-\345\244\207\347\224\250\350\267\257\347\272\277-2\350\265\236.json" "b/repo/js/IceObjectNpcLike/Assets/Pathing/09-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\345\217\260\345\255\220-\345\244\207\347\224\250\350\267\257\347\272\277-2\350\265\236.json" new file mode 100644 index 0000000000..15495f4798 --- /dev/null +++ "b/repo/js/IceObjectNpcLike/Assets/Pathing/09-\345\206\260\351\200\240\347\211\251\347\202\271\350\265\236-\350\207\263\345\206\254-\350\275\246\347\253\231\345\217\260\345\255\220-\345\244\207\347\224\250\350\267\257\347\272\277-2\350\265\236.json" @@ -0,0 +1,70 @@ +{ + "info": { + "authors": [ + { + "links": "", + "name": "一只低熵体" + } + ], + "bgi_version": "0.64.0", + "description": "Point 08 未启用时,从 Point 07 结束点直达 Point 09 工位的完整备用路线", + "enable_monster_loot_split": false, + "last_modified_time": 1788416516144, + "map_match_method": "SIFT", + "map_name": "Teyvat", + "name": "09-冰造物点赞-至冬-车站台子-备用路线-2赞", + "tags": [ + "备用路线", + "Point09" + ], + "type": "collect", + "version": "1.0" + }, + "positions": [ + { + "action": "", + "action_params": "", + "id": 1, + "move_mode": "walk", + "type": "path", + "x": 7839.937, + "y": 8497.9844 + }, + { + "action": "", + "action_params": "", + "id": 2, + "move_mode": "walk", + "type": "path", + "x": 7811.322, + "y": 8468.8503 + }, + { + "action": "", + "action_params": "", + "id": 3, + "move_mode": "walk", + "type": "path", + "x": 7796.6094, + "y": 8470.6563 + }, + { + "action": "", + "action_params": "", + "id": 4, + "move_mode": "walk", + "type": "target", + "x": 7796.5229, + "y": 8474.4375 + }, + { + "action": "", + "action_params": "", + "id": 5, + "move_mode": "walk", + "type": "orientation", + "x": 7789.0467, + "y": 8495.3203 + } + ] +} diff --git a/repo/js/IceObjectNpcLike/README.md b/repo/js/IceObjectNpcLike/README.md new file mode 100755 index 0000000000..c34b4a4990 --- /dev/null +++ b/repo/js/IceObjectNpcLike/README.md @@ -0,0 +1,89 @@ +# 冰造物 NPC 点赞自动化 + +作者:一只低熵体 +版本:`1.1.0` +全部路线预计总点赞:`36` + +脚本使用 BetterGI 地图追踪前往至冬 NPC 工位,自动完成两次冰造物放置、点赞等待、删除和最终清理。脚本不读取游戏内存,不联网,也不使用第三方程序、KeyMouseScript、OCR 或点赞图像识别。 + +## 使用条件 + +- BetterGI `0.64.0` 或更高版本; +- 简体中文游戏客户端; +- 16:9 游戏画面,至少 `1920×1080`; +- BetterGI 截图方式设为 `BitBlt`; +- 小地图保持“锁定方向”; +- 原神水平、垂直镜头灵敏度均保持默认值 `3`; +- 保持默认亮度并关闭 HDR、显卡滤镜等画面效果; +- 冰造物快捷键为 `T`,相关区域和传送锚点已解锁; +- 运行期间不要操作键盘和鼠标。 + +## 任务前置 + +- **Point 06、07、09**:需要完成三个聚所任务的共同第四幕“雪后曙光”,可用成就“睁开被逸夜合拢的眼睛”确认。参考:[《雪后曙光》教程](https://www.bilibili.com/video/BV172bX6XEPm/)。 +- **Point 08**:除“雪后曙光”外,还需在现实时间第二天完成“新雪来客”。参考:[《新雪来客》教程](https://www.bilibili.com/video/BV1nqbX6fEDS/)。 + +Point 06~09 默认关闭,请根据任务进度启用。 + +## 使用方法 + +1. 在 BetterGI 中订阅或导入脚本。 +2. 右键脚本,打开“修改 JS 自定义配置”。 +3. 按任务进度启用路线;未完成前置任务时保持 Point 06~09 关闭。 +4. 将脚本加入调度器并运行。首次测试建议使用“按 Point 编号”。 + +默认启用 Point 01~05,预计获得 `22` 赞;完成“雪后曙光”并启用 Point 06、07、09 后预计获得 `30` 赞;再启用 Point 08 后,全部路线预计获得 `36` 赞。 + +## 路线一览 + +| 路线 | 地点 | NPC | 预计点赞 | 默认 | 备注 | +| --- | --- | ---: | ---: | --- | --- | +| Point 01 | 北国银行门口 | 3 | 6 | 开启 | 独立路线 | +| Point 02 | 印记商店门口 | 4 | 8 | 开启 | 独立路线 | +| Point 03 | 炉灰食堂门口 | 2 | 4 | 开启 | 独立路线 | +| Point 04 | 餐厅门口 | 1 | 2 | 开启 | 独立路线 | +| Point 05 | 铁匠附近 | 1 | 2 | 开启 | 独立路线 | +| Point 06 | 曙光车站玩具附近 | 2 | 4 | 关闭 | 需完成“雪后曙光” | +| Point 07 | 车站餐厅 | 1 | 2 | 关闭 | 需完成“雪后曙光” | +| Point 08 | 车站火炬边 | 3 | 6 | 关闭 | 接续 Point 07;需完成“新雪来客” | +| Point 09 | 车站台子 | 1 | 2 | 关闭 | 接续路线;需完成“雪后曙光” | + +### 车站连续路线 + +- Point 08 开启时:`Point 07 → Point 08 → Point 09`; +- Point 08 关闭时:自动改走 `Point 07 → Point 09` 完整备用路线; +- Point 07 关闭时:依赖当前位置的 Point 08、09 会自动跳过。 + +连续路线组不会被传送或热能恢复流程拆开。 + +## 运行模式 + +- **按 Point 编号**:默认模式,适合首次测试; +- **高点赞优先**:执行全部已启用路线,并按路线组预计点赞量排序; +- **高收益精简(6赞及以上)**:只执行预计点赞达到 `6` 的路线组。 + +全部路线启用时,高收益精简模式会执行 Point 07~09 连续组(10赞)、Point 02(8赞)和 Point 01(6赞),合计预计 `24` 赞。 + +## 热能与镜头 + +脚本启动时会先返回固定七天神像恢复热能,默认每完成 `4` 个 Point 再恢复一次。若下一个连续组的长度超过剩余额度,会在组开始前提前恢复。 + +每个 Point 到位后,路线末尾的 `orientation` 负责确定水平视角;脚本再将垂直镜头推到上限,随后按自定义配置中的“冰造物放置镜头角度”向下移动,默认值为 `7500`。 + +检测到 `3840×2160` 或更高分辨率时,脚本默认在按 `T` 后额外等待 `800ms`。其他分辨率如果也出现界面动画未完成便点击的问题,可以手动启用相同时序补偿。 + +## 常用设置 + +| 设置 | 默认值 | 建议 | +| --- | ---: | --- | +| 路线运行模式 | 按 Point 编号 | 首次测试保持默认 | +| Point 01~09 独立开关 | 01~05 开启;06~09 关闭 | 按任务进度和需要选择 | +| 冰造物放置镜头角度 | 7500 | 控制最终放置位置 | +| 强制启用 4K 时序补偿 | 关闭 | 非 4K 出现点击过早时开启;4K 会自动启用 | + +## 故障排查与限制 + +- 多条路线同时出现传送点击偏移时,先完整退出并重启 BetterGI; +- 地图追踪反复摇摆时,确认 `BitBlt`、小地图锁定方向和稳定帧率,条件允许时可尝试 `2560×1440`; +- 脚本中途停止或报错后,请人工确认现场是否残留冰造物,并按 `T` 退出造物界面; +- 日志中的预计点赞来自路线标签,脚本不会视觉识别实际点赞、热能数值或删除结果。 diff --git a/repo/js/IceObjectNpcLike/main.js b/repo/js/IceObjectNpcLike/main.js new file mode 100755 index 0000000000..2d831ed682 --- /dev/null +++ b/repo/js/IceObjectNpcLike/main.js @@ -0,0 +1,5 @@ +import { runTask } from "./src/modules/task.js"; + +(async function () { + await runTask(); +})(); diff --git a/repo/js/IceObjectNpcLike/manifest.json b/repo/js/IceObjectNpcLike/manifest.json new file mode 100755 index 0000000000..79b1e6330c --- /dev/null +++ b/repo/js/IceObjectNpcLike/manifest.json @@ -0,0 +1,19 @@ +{ + "manifest_version": 1, + "name": "冰造物 NPC 点赞自动化", + "version": "1.1.0", + "bgi_version": "0.64.0", + "description": "自动前往至冬 NPC 点位完成两次冰造物点赞与最终清理,支持高收益精简、路线开关、多段连续路线、任务分支和定期恢复热能。", + "tags": [ + "冰造物", + "NPC点赞", + "地图追踪" + ], + "authors": [ + { + "name": "一只低熵体" + } + ], + "settings_ui": "settings.json", + "main": "main.js" +} diff --git a/repo/js/IceObjectNpcLike/settings.json b/repo/js/IceObjectNpcLike/settings.json new file mode 100755 index 0000000000..aca2bcfaab --- /dev/null +++ b/repo/js/IceObjectNpcLike/settings.json @@ -0,0 +1,82 @@ +[ + { + "name": "executionOrderMode", + "type": "select", + "label": "路线运行模式(按路线组读取“点赞=总数”标签)", + "options": [ + "按 Point 编号", + "高点赞优先", + "高收益精简(6赞及以上)" + ], + "default": "按 Point 编号" + }, + { + "name": "point01Enabled", + "type": "checkbox", + "label": "启用 Point 01|北国银行门口|预计 6 赞", + "default": true + }, + { + "name": "point02Enabled", + "type": "checkbox", + "label": "启用 Point 02|印记商店门口|预计 8 赞", + "default": true + }, + { + "name": "point03Enabled", + "type": "checkbox", + "label": "启用 Point 03|炉灰食堂门口|预计 4 赞", + "default": true + }, + { + "name": "point04Enabled", + "type": "checkbox", + "label": "启用 Point 04|餐厅门口|预计 2 赞", + "default": true + }, + { + "name": "point05Enabled", + "type": "checkbox", + "label": "启用 Point 05|铁匠附近|预计 2 赞", + "default": true + }, + { + "name": "point06Enabled", + "type": "checkbox", + "label": "启用 Point 06|曙光车站玩具附近|预计 4 赞(需完成“雪后曙光”)", + "default": false + }, + { + "name": "point07Enabled", + "type": "checkbox", + "label": "启用 Point 07|车站餐厅|预计 2 赞(需完成“雪后曙光”)", + "default": false + }, + { + "name": "point08Enabled", + "type": "checkbox", + "label": "启用 Point 08|车站火炬边|预计 6 赞(接续 07;需完成“新雪来客”)", + "default": false + }, + { + "name": "point09Enabled", + "type": "checkbox", + "label": "启用 Point 09|车站台子|预计 2 赞(接续路线;需完成“雪后曙光”)", + "default": false + }, + { + "type": "separator" + }, + { + "name": "defaultCameraPitchFromTop", + "type": "input-text", + "label": "冰造物放置镜头角度(从垂直上限向下移动量,推荐 7500)", + "default": "7500" + }, + { + "name": "forceTDelayCompensationEnabled", + "type": "checkbox", + "label": "强制启用 4K 时序补偿(非 4K 出现点击过早时开启)", + "default": false + } +] diff --git a/repo/js/IceObjectNpcLike/src/constants.js b/repo/js/IceObjectNpcLike/src/constants.js new file mode 100644 index 0000000000..8b456b2a56 --- /dev/null +++ b/repo/js/IceObjectNpcLike/src/constants.js @@ -0,0 +1,38 @@ +export const PATHS = { + restore: "Assets/Pathing/00-恢复热能-至冬-七天神像.json", + pointDirectory: "Assets/Pathing", + point09FromPoint07: "Assets/Pathing/09-冰造物点赞-至冬-车站台子-备用路线-2赞.json" +}; + +export const POINT_FILE_PATTERN = /^(\d{2})-冰造物点赞-(?!.*备用路线).+-\d+赞\.json$/i; +export const LIKE_TAG_PATTERN = /^(?:点赞|likes?)\s*[::==]\s*(\d+)\s*$/i; + +export const ORDER_BY_POINT = "按 Point 编号"; +export const ORDER_BY_LIKES = "高点赞优先"; +export const HIGH_YIELD_ONLY = "高收益精简(6赞及以上)"; +export const HIGH_YIELD_MIN_LIKES = 6; + +// 只改变镜头俯仰,不改变 orientation 已经确定的水平朝向。 +export const CAMERA_PITCH_CLAMP_Y = -10000; + +export const DEFAULT_TIMINGS = { + pathSettleDelay: 1000, + restoreDelay: 1500, + cameraClampPassDelay: 100, + cameraClampSettleDelay: 150, + cameraSettleDelay: 400, + fourKExtraTDelay: 800, + enterCreationDelay: 500, + firstLikeDelay: 800, + selectObjectDelay: 500, + deleteDelay: 400, + secondLikeDelay: 800, + finalDeleteDelay: 400, + exitCreationDelay: 500 +}; + +export const DEFAULT_CAMERA = { + x: 0, + pitchFromTop: 7500, + clampPasses: 4 +}; diff --git a/repo/js/IceObjectNpcLike/src/modules/actions.js b/repo/js/IceObjectNpcLike/src/modules/actions.js new file mode 100644 index 0000000000..eaadc7e4fe --- /dev/null +++ b/repo/js/IceObjectNpcLike/src/modules/actions.js @@ -0,0 +1,101 @@ +import { CAMERA_PITCH_CLAMP_Y, DEFAULT_CAMERA } from "../constants.js"; +import { readDelay, readInteger, readTTransitionDelay } from "./config.js"; +import { runPointPath } from "./pathing.js"; + +async function applyPointCameraOffset(point, xOverrides, pitchOverrides) { + const clampPasses = readInteger( + "cameraClampPasses", + DEFAULT_CAMERA.clampPasses, + 1, + 20 + ); + const clampPassDelay = readDelay("cameraClampPassDelay"); + + log.info( + `[${point.name}] 固定镜头垂直基准:向上推 ${clampPasses} 次,` + + `间隔 ${clampPassDelay}ms(保留 orientation 水平朝向)` + ); + for (let pass = 0; pass < clampPasses; pass += 1) { + moveMouseBy(0, CAMERA_PITCH_CLAMP_Y); + if (pass + 1 < clampPasses) { + await sleep(clampPassDelay); + } + } + await sleep(readDelay("cameraClampSettleDelay")); + + const defaultX = readInteger("defaultCameraOffsetX", DEFAULT_CAMERA.x, -10000, 10000); + const defaultPitch = readInteger( + "defaultCameraPitchFromTop", + DEFAULT_CAMERA.pitchFromTop, + 0, + 10000 + ); + const x = Object.prototype.hasOwnProperty.call(xOverrides, point.number) + ? xOverrides[point.number] + : defaultX; + const pitchFromTop = Object.prototype.hasOwnProperty.call(pitchOverrides, point.number) + ? pitchOverrides[point.number] + : defaultPitch; + + log.info(`[${point.name}] 应用镜头标定:水平 x=${x},从垂直上限向下 y=${pitchFromTop}`); + if (x !== 0) { + moveMouseBy(x, 0); + } + if (pitchFromTop !== 0) { + moveMouseBy(0, pitchFromTop); + } + await sleep(readDelay("cameraSettleDelay")); +} + +async function exitCreationMode(point) { + log.info(`[${point.name}] 按 T 退出冰造物界面`); + keyPress("VK_T"); + await sleep(readDelay("exitCreationDelay")); + log.info(`[${point.name}] 已回到正常游戏状态`); +} + +async function doIceObjectCycle(point) { + const label = point.name; + + log.info(`[${label}] 第一次放置:进入造物模式`); + keyPress("VK_T"); + await sleep(readTTransitionDelay("enterCreationDelay")); + + leftButtonClick(); + log.info(`[${label}] 第一次放置完成,等待 NPC 点赞`); + await sleep(readDelay("firstLikeDelay")); + log.info(`[${label}] 第一次点赞安全等待完成`); + + log.info(`[${label}] 删除第一次造物`); + keyPress("VK_T"); + await sleep(readTTransitionDelay("selectObjectDelay")); + + leftButtonClick(); + log.info(`[${label}] 第一次造物已删除`); + await sleep(readDelay("deleteDelay")); + + log.info(`[${label}] 第二次放置`); + leftButtonClick(); + await sleep(readDelay("secondLikeDelay")); + log.info(`[${label}] 第二次点赞安全等待完成`); + + log.info(`[${label}] 最终删除第二次造物`); + keyPress("VK_T"); + await sleep(readTTransitionDelay("selectObjectDelay")); + + leftButtonClick(); + await sleep(readDelay("finalDeleteDelay")); + log.info(`[${label}] 最终删除动作完成(未进行视觉校验)`); + + await exitCreationMode(point); + log.info(`[${label}] 完成`); +} + +export async function runPoint(point, xOverrides, pitchOverrides) { + await runPointPath(point); + log.info(`[${point.name}] 已到达固定工位,orientation 完成`); + + await sleep(readDelay("pathSettleDelay")); + await applyPointCameraOffset(point, xOverrides, pitchOverrides); + await doIceObjectCycle(point); +} diff --git a/repo/js/IceObjectNpcLike/src/modules/config.js b/repo/js/IceObjectNpcLike/src/modules/config.js new file mode 100644 index 0000000000..5b37304ed1 --- /dev/null +++ b/repo/js/IceObjectNpcLike/src/modules/config.js @@ -0,0 +1,139 @@ +import { DEFAULT_TIMINGS } from "../constants.js"; +import { errorText } from "../utils/common.js"; + +let runtimeTDelayExtra = 0; + +export function readCheckbox(name, fallback) { + const raw = settings[name]; + if (raw === undefined || raw === null || raw === "") { + return fallback; + } + return raw === true || String(raw).toLowerCase() === "true"; +} + +export function readInteger(name, fallback, min, max) { + const raw = settings[name]; + if (raw === undefined || raw === null || raw === "") { + return fallback; + } + + const value = Number(raw); + if (!Number.isFinite(value) || value < min || value > max) { + log.warn(`[Config] ${name}=${raw} 无效,使用默认值 ${fallback}`); + return fallback; + } + + return Math.round(value); +} + +export function readSelect(name, fallback, allowedValues) { + const raw = String(settings[name] || fallback).trim(); + if (allowedValues.includes(raw)) { + return raw; + } + log.warn(`[Config] ${name}=${raw} 无效,使用默认值 ${fallback}`); + return fallback; +} + +export function readDelay(name) { + return readInteger(name, DEFAULT_TIMINGS[name], 0, 600000); +} + +function readGameResolution() { + try { + const metrics = getGameMetrics(); + if (metrics && metrics.length >= 2) { + const width = Math.round(Number(metrics[0])); + const height = Math.round(Number(metrics[1])); + if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { + return { width, height }; + } + } + } catch (error) { + log.warn(`[Environment] 无法读取游戏窗口尺寸:${errorText(error)}`); + } + + return null; +} + +export function configureRuntimeTimings() { + const forceCompensation = readCheckbox("forceTDelayCompensationEnabled", false); + const configuredExtraDelay = readDelay("fourKExtraTDelay"); + const resolution = readGameResolution(); + if (!resolution) { + runtimeTDelayExtra = forceCompensation ? configuredExtraDelay : 0; + if (forceCompensation) { + log.warn( + `[Environment] 未能确认游戏分辨率,已按手动设置启用 T 后额外等待 ${runtimeTDelayExtra}ms` + ); + } else { + log.warn("[Environment] 未能确认游戏分辨率,不启用 T 后额外等待"); + } + return; + } + + log.info(`[Environment] 游戏窗口分辨率:${resolution.width}x${resolution.height}`); + + const isFourK = resolution.width >= 3840 && resolution.height >= 2160; + runtimeTDelayExtra = isFourK || forceCompensation ? configuredExtraDelay : 0; + + if (isFourK && runtimeTDelayExtra > 0) { + log.info(`[Environment] 已启用 4K 时序补偿:每次 T 后、左键前额外等待 ${runtimeTDelayExtra}ms`); + } else if (isFourK) { + log.info("[Environment] 检测到 4K,4K 时序补偿已设为 0ms"); + } else if (forceCompensation && runtimeTDelayExtra > 0) { + log.info(`[Environment] 当前不是 4K,已按手动设置启用 T 后额外等待 ${runtimeTDelayExtra}ms`); + } else if (forceCompensation) { + log.info("[Environment] 当前不是 4K,手动时序补偿已开启,但额外等待设为 0ms"); + } else { + log.info("[Environment] 当前不是 4K,且未手动启用时序补偿"); + } +} + +export function readTTransitionDelay(name) { + return readDelay(name) + runtimeTDelayExtra; +} + +export function parsePointIntegerOverrides(settingName, min, max) { + const result = {}; + const raw = String(settings[settingName] || "").trim(); + if (!raw) { + return result; + } + + const entries = raw.split(/[,,;;]+/); + for (const entry of entries) { + const trimmed = entry.trim(); + if (!trimmed) { + continue; + } + + const match = /^(\d+)\s*=\s*(-?\d+)$/.exec(trimmed); + if (!match) { + log.warn(`[Config] ${settingName} 中的“${trimmed}”格式无效,已忽略`); + continue; + } + + const pointNumber = Number(match[1]); + const value = Number(match[2]); + if (pointNumber < 1 || value < min || value > max) { + log.warn(`[Config] ${settingName} 中的“${trimmed}”超出范围,已忽略`); + continue; + } + + result[pointNumber] = Math.round(value); + } + + return result; +} + +export function normalizeVirtualKey(rawKey, fallback) { + const value = String(rawKey || fallback).trim().toUpperCase(); + if (value.startsWith("VK_")) { + return value; + } + if (value.length === 1) { + return `VK_${value}`; + } + return value; +} diff --git a/repo/js/IceObjectNpcLike/src/modules/pathing.js b/repo/js/IceObjectNpcLike/src/modules/pathing.js new file mode 100644 index 0000000000..9baa8a4620 --- /dev/null +++ b/repo/js/IceObjectNpcLike/src/modules/pathing.js @@ -0,0 +1,232 @@ +import { LIKE_TAG_PATTERN, PATHS, POINT_FILE_PATTERN } from "../constants.js"; +import { readCheckbox, readDelay, normalizeVirtualKey } from "./config.js"; +import { baseName, errorText, fileStem, formatPointNumber } from "../utils/common.js"; + +function loadPathData(path) { + let content; + try { + content = file.readTextSync(path); + } catch (error) { + throw new Error(`无法读取路径文件 ${path}:${errorText(error)}`); + } + + let data; + try { + data = JSON.parse(content.replace(/^\uFEFF/, "")); + } catch (error) { + throw new Error(`路径文件不是有效 JSON:${path}:${errorText(error)}`); + } + + if (!data || !Array.isArray(data.positions)) { + throw new Error(`路径文件缺少 positions 数组:${path}`); + } + if (data.positions.length === 0) { + throw new Error(`路径文件 positions 为空:${path}`); + } + + return data; +} + +function validatePathName(path, data) { + const declaredName = data.info && String(data.info.name || "").trim(); + const expectedName = fileStem(path); + if (declaredName !== expectedName) { + throw new Error(`路径文件名与 info.name 不一致:${baseName(path)} / ${declaredName || "未填写"}`); + } +} + +function parseLikeCount(data, point) { + const tags = data.info && Array.isArray(data.info.tags) ? data.info.tags : []; + const matchedCounts = []; + + for (const rawTag of tags) { + const tag = String(rawTag).trim(); + const match = LIKE_TAG_PATTERN.exec(tag); + if (match) { + matchedCounts.push(Number(match[1])); + } else if (/^(?:点赞|likes?)/i.test(tag)) { + log.warn(`[${point.name}] 无法识别点赞标签“${tag}”,正确格式示例:点赞=6`); + } + } + + if (matchedCounts.length > 1) { + throw new Error(`[${point.name}] 存在多个点赞数量标签,请只保留一个“点赞=总数”标签`); + } + if (matchedCounts.length === 0) { + return null; + } + + const count = matchedCounts[0]; + if (!Number.isSafeInteger(count) || count < 0 || count > 9999) { + throw new Error(`[${point.name}] 点赞数量超出允许范围 0~9999`); + } + return count; +} + +export function discoverPoints() { + let paths; + try { + paths = Array.from(file.readPathSync(PATHS.pointDirectory)); + } catch (error) { + throw new Error(`无法读取点位目录 ${PATHS.pointDirectory}:${errorText(error)}`); + } + + const points = []; + const seenNumbers = {}; + + for (const path of paths) { + if (file.isFolder(path)) { + continue; + } + + const match = POINT_FILE_PATTERN.exec(baseName(path)); + if (!match) { + continue; + } + + const number = Number(match[1]); + if (number < 1) { + continue; + } + + if (seenNumbers[number]) { + throw new Error(`Point ${formatPointNumber(number)} 存在重复文件:${seenNumbers[number]} 与 ${path}`); + } + seenNumbers[number] = path; + + points.push({ + number, + name: `Point ${formatPointNumber(number)}`, + path: String(path).replace(/\\/g, "/"), + continueFromPrevious: false, + alternatePathReason: null, + enabled: true, + hasFinalOrientation: false, + requiresRestoreStart: false, + likeCount: null + }); + } + + points.sort((a, b) => a.number - b.number); + + if (points.length === 0) { + throw new Error(`未在 ${PATHS.pointDirectory} 中找到符合命名规范的 Point 路线`); + } + + return points; +} + +function validatePoint09AlternatePath() { + const data = loadPathData(PATHS.point09FromPoint07); + validatePathName(PATHS.point09FromPoint07, data); + + const positions = data.positions; + const firstType = String(positions[0].type || "").toLowerCase(); + const lastType = String(positions[positions.length - 1].type || "").toLowerCase(); + + if (firstType === "teleport") { + throw new Error(`[Point 09] 备用路线不能以 teleport 开始:${PATHS.point09FromPoint07}`); + } + if (lastType !== "orientation") { + throw new Error(`[Point 09] 备用路线必须以 orientation 结束:${PATHS.point09FromPoint07}`); + } + + const matchMethod = data.info && data.info.map_match_method; + if (matchMethod && String(matchMethod).toUpperCase() !== "SIFT") { + log.warn(`[Point 09] 备用路线 map_match_method=${matchMethod},至冬区域建议使用 SIFT`); + } + + log.info("[System] Point 09 绕过 Point 08 的备用路线预检通过"); +} + +export function preflight(points) { + const restoreData = loadPathData(PATHS.restore); + validatePathName(PATHS.restore, restoreData); + + for (let index = 0; index < points.length; index += 1) { + const point = points[index]; + const data = loadPathData(point.path); + validatePathName(point.path, data); + + const positions = data.positions; + const firstPosition = positions[0]; + const lastPosition = positions[positions.length - 1]; + const firstType = String(firstPosition.type || "").toLowerCase(); + const lastType = String(lastPosition.type || "").toLowerCase(); + const previousPoint = index > 0 ? points[index - 1] : null; + + // 规范化文件名不再使用 _continue;是否接续由路线起点类型和连续编号判断。 + point.continueFromPrevious = firstType !== "teleport" && + previousPoint !== null && point.number === previousPoint.number + 1; + + if (point.continueFromPrevious && previousPoint.number + 1 !== point.number) { + throw new Error(`[${point.name}] 接续路线必须紧跟上一个 Point`); + } + + point.hasFinalOrientation = lastType === "orientation"; + if (!point.hasFinalOrientation) { + throw new Error(`[${point.name}] 路线末尾必须是 orientation,才能固定冰造物的水平放置方向`); + } + + point.requiresRestoreStart = !point.continueFromPrevious && firstType !== "teleport"; + point.likeCount = parseLikeCount(data, point); + point.enabled = readCheckbox(`point${formatPointNumber(point.number)}Enabled`, true); + + const matchMethod = data.info && data.info.map_match_method; + if (matchMethod && String(matchMethod).toUpperCase() !== "SIFT") { + log.warn(`[${point.name}] map_match_method=${matchMethod},至冬区域建议使用 SIFT`); + } + + if (point.likeCount === null) { + log.warn(`[${point.name}] 未填写“点赞=总数”标签,高点赞排序时将放在已标注路线之后`); + } else { + log.info(`[${point.name}] 标签记录预计总点赞:${point.likeCount}`); + } + } + + const hasStationChain = [7, 8, 9].every(number => + points.some(point => point.number === number) + ); + if (hasStationChain) { + validatePoint09AlternatePath(); + } + + log.info(`[System] 路径预检通过,共发现 ${points.length} 个 Point`); +} + +export async function runPathFile(label, path) { + log.info(`[${label}] 开始地图追踪:${path}`); + try { + await pathingScript.runFile(path); + } catch (error) { + throw new Error(`[${label}] 地图追踪失败:${errorText(error)}`); + } + log.info(`[${label}] 地图追踪完成`); +} + +export async function runPointPath(point) { + if (point.alternatePathReason) { + log.info(`[${point.name}] 使用备用路线:${point.alternatePathReason}`); + } + log.info(`[${point.name}] 开始地图追踪:${point.path}`); + try { + await pathingScript.runFile(point.path); + } catch (error) { + throw new Error(`[${point.name}] 地图追踪失败:${errorText(error)}`); + } + log.info(`[${point.name}] 地图追踪完成,orientation 已执行`); +} + +export async function restoreHeat(reason) { + log.info(`[Heat] 开始恢复热能:${reason}`); + await runPathFile("Heat", PATHS.restore); + + if (readCheckbox("restoreInteractionEnabled", false)) { + const interactKey = normalizeVirtualKey(settings.restoreInteractionKey, "VK_F"); + log.info(`[Heat] 执行固定交互键:${interactKey}`); + keyPress(interactKey); + } + + await sleep(readDelay("restoreDelay")); + log.info("[Heat] 恢复完成(未进行热能数值识别)"); +} diff --git a/repo/js/IceObjectNpcLike/src/modules/planner.js b/repo/js/IceObjectNpcLike/src/modules/planner.js new file mode 100644 index 0000000000..e62de75090 --- /dev/null +++ b/repo/js/IceObjectNpcLike/src/modules/planner.js @@ -0,0 +1,209 @@ +import { + HIGH_YIELD_MIN_LIKES, + HIGH_YIELD_ONLY, + ORDER_BY_LIKES +} from "../constants.js"; + +export function buildRouteGroups(points) { + const groups = []; + + for (const point of points) { + if (!point.continueFromPrevious) { + groups.push({ + points: [point], + firstNumber: point.number, + requiresRestoreStart: point.requiresRestoreStart, + likeCount: 0, + hasCompleteLikeData: true + }); + } else { + groups[groups.length - 1].points.push(point); + } + } + + return groups; +} + +function createSelectedGroup(sourceGroup, selectedPoints) { + if (selectedPoints.length === 0) { + return null; + } + + const selectedGroup = { + points: selectedPoints, + firstNumber: selectedPoints[0].number, + requiresRestoreStart: sourceGroup.requiresRestoreStart, + likeCount: 0, + hasCompleteLikeData: true + }; + + for (const point of selectedPoints) { + if (point.likeCount === null) { + selectedGroup.hasCompleteLikeData = false; + } else { + selectedGroup.likeCount += point.likeCount; + } + } + + return selectedGroup; +} + +function selectStationRouteGroup(group, point09AlternatePath) { + const point07 = group.points.find(point => point.number === 7); + const point08 = group.points.find(point => point.number === 8); + const point09 = group.points.find(point => point.number === 9); + + if (!point07 || !point08 || !point09) { + return null; + } + + if (!point07.enabled) { + log.info(`[${point07.name}] 已在 JS 自定义设置中关闭`); + if (point08.enabled) { + log.warn(`[${point08.name}] 依赖 ${point07.name} 的当前位置,已自动跳过`); + } + if (point09.enabled) { + log.warn(`[${point09.name}] 依赖 ${point07.name} 的当前位置,已自动跳过`); + } + return { handled: true, group: null }; + } + + const selectedPoints = [point07]; + + if (point08.enabled) { + selectedPoints.push(point08); + } else { + log.info(`[${point08.name}] 已关闭,将在需要时使用备用路线绕过`); + } + + if (point09.enabled) { + if (point08.enabled) { + selectedPoints.push(point09); + } else { + selectedPoints.push({ + ...point09, + path: point09AlternatePath, + alternatePathReason: "Point 08 已关闭,从 Point 07 直接前往 Point 09" + }); + log.info("[RouteGroup] Point 07 → Point 09 将使用备用路线,跳过 Point 08"); + } + } else { + log.info(`[${point09.name}] 已在 JS 自定义设置中关闭`); + } + + return { + handled: true, + group: createSelectedGroup(group, selectedPoints) + }; +} + +export function applyRouteSwitches(groups, point09AlternatePath) { + const selectedGroups = []; + + for (const group of groups) { + const stationSelection = selectStationRouteGroup(group, point09AlternatePath); + if (stationSelection) { + if (stationSelection.group) { + selectedGroups.push(stationSelection.group); + } + continue; + } + + const selectedPoints = []; + let dependencyBrokenBy = null; + + for (const point of group.points) { + if (dependencyBrokenBy) { + log.warn(`[${point.name}] 依赖已跳过的 ${dependencyBrokenBy.name},连续路线已自动跳过`); + continue; + } + + if (!point.enabled) { + log.info(`[${point.name}] 已在 JS 自定义设置中关闭`); + dependencyBrokenBy = point; + continue; + } + + selectedPoints.push(point); + } + + const selectedGroup = createSelectedGroup(group, selectedPoints); + if (selectedGroup) { + selectedGroups.push(selectedGroup); + } + } + + return selectedGroups; +} + +export function routeGroupName(group) { + return group.points.map(point => point.name).join(" → "); +} + +export function orderRouteGroups(groups, orderMode) { + let ordered = groups.slice(); + + if (orderMode === HIGH_YIELD_ONLY) { + ordered = ordered.filter(group => { + if (!group.hasCompleteLikeData) { + log.warn( + `[RouteGroup] “${routeGroupName(group)}”缺少完整点赞标签,` + + "无法判断是否达到高收益门槛,已跳过" + ); + return false; + } + if (group.likeCount < HIGH_YIELD_MIN_LIKES) { + log.info( + `[RouteGroup] “${routeGroupName(group)}”预计 ${group.likeCount} 赞,` + + `低于高收益门槛 ${HIGH_YIELD_MIN_LIKES} 赞,已跳过` + ); + return false; + } + return true; + }); + } + + if (orderMode === ORDER_BY_LIKES || orderMode === HIGH_YIELD_ONLY) { + ordered.sort((a, b) => { + if (a.hasCompleteLikeData !== b.hasCompleteLikeData) { + return a.hasCompleteLikeData ? -1 : 1; + } + if (a.hasCompleteLikeData && a.likeCount !== b.likeCount) { + return b.likeCount - a.likeCount; + } + return a.firstNumber - b.firstNumber; + }); + } else { + ordered.sort((a, b) => a.firstNumber - b.firstNumber); + } + + return ordered; +} + +export function validateRouteGroups(groups, restoreEveryPoints) { + for (const group of groups) { + if (group.points.length > 6) { + throw new Error( + `连续路线组“${routeGroupName(group)}”包含 ${group.points.length} 个 Point,` + + "超过已确认的60点热能容量;请拆分为可以重新传送的独立路线" + ); + } + if (group.points.length > restoreEveryPoints) { + log.warn( + `[RouteGroup] “${routeGroupName(group)}”长度超过恢复间隔 ${restoreEveryPoints},` + + "为保持连续位置,将在组开始前恢复并完整执行该组" + ); + } + } +} + +export function logExecutionPlan(groups, orderMode, restoreEveryPoints) { + const plan = groups.map(group => { + const likes = group.hasCompleteLikeData ? `${group.likeCount}赞` : "点赞未完整标注"; + return `${routeGroupName(group)}(${likes})`; + }).join(" | "); + + log.info(`[Config] 路线运行模式:${orderMode}`); + log.info(`[Config] 每 ${restoreEveryPoints} 个成功 Point 恢复一次热能`); + log.info(`[Plan] ${plan}`); +} diff --git a/repo/js/IceObjectNpcLike/src/modules/task.js b/repo/js/IceObjectNpcLike/src/modules/task.js new file mode 100644 index 0000000000..c70a5c7627 --- /dev/null +++ b/repo/js/IceObjectNpcLike/src/modules/task.js @@ -0,0 +1,111 @@ +import { + HIGH_YIELD_ONLY, + ORDER_BY_LIKES, + ORDER_BY_POINT, + PATHS +} from "../constants.js"; +import { runPoint } from "./actions.js"; +import { + configureRuntimeTimings, + parsePointIntegerOverrides, + readInteger, + readSelect +} from "./config.js"; +import { discoverPoints, preflight, restoreHeat } from "./pathing.js"; +import { + applyRouteSwitches, + buildRouteGroups, + logExecutionPlan, + orderRouteGroups, + routeGroupName, + validateRouteGroups +} from "./planner.js"; +import { errorText } from "../utils/common.js"; + +export async function runTask() { + try { + log.info("[System] 冰造物 NPC 点赞自动化开始"); + configureRuntimeTimings(); + + const points = discoverPoints(); + preflight(points); + + const orderMode = readSelect( + "executionOrderMode", + ORDER_BY_POINT, + [ORDER_BY_POINT, ORDER_BY_LIKES, HIGH_YIELD_ONLY] + ); + const restoreEveryPoints = readInteger("restoreEveryPoints", 4, 1, 6); + const xOverrides = parsePointIntegerOverrides("cameraOffsetXOverrides", -10000, 10000); + const pitchOverrides = parsePointIntegerOverrides("cameraPitchFromTopOverrides", 0, 10000); + + const allGroups = buildRouteGroups(points); + const selectedGroups = applyRouteSwitches(allGroups, PATHS.point09FromPoint07); + const orderedGroups = orderRouteGroups(selectedGroups, orderMode); + + if (orderedGroups.length === 0) { + log.warn("[System] 没有启用任何可执行路线,脚本结束"); + return; + } + + validateRouteGroups(orderedGroups, restoreEveryPoints); + logExecutionPlan(orderedGroups, orderMode, restoreEveryPoints); + + await restoreHeat("脚本启动"); + + let completedSinceRestore = 0; + let completedPoints = 0; + let expectedLikes = 0; + let pointsWithoutLikeTag = 0; + + for (const group of orderedGroups) { + const groupSize = group.points.length; + const insufficientHeatQuota = completedSinceRestore > 0 && + completedSinceRestore + groupSize > restoreEveryPoints; + const mustReturnToRestoreStart = group.requiresRestoreStart && completedSinceRestore > 0; + + if (insufficientHeatQuota || mustReturnToRestoreStart) { + const reason = mustReturnToRestoreStart + ? `${routeGroupName(group)} 需要从恢复点开始` + : `下一个连续组有 ${groupSize} 个 Point,当前剩余额度不足`; + await restoreHeat(reason); + completedSinceRestore = 0; + } + + if (group.points.length > 1) { + log.info(`[RouteGroup] 开始连续执行:${routeGroupName(group)}`); + } + + for (const point of group.points) { + if (point.continueFromPrevious) { + log.info(`[${point.name}] 连续路线:从上一个 Point 的当前位置继续`); + } + + await runPoint(point, xOverrides, pitchOverrides); + completedSinceRestore += 1; + completedPoints += 1; + + if (point.likeCount === null) { + pointsWithoutLikeTag += 1; + } else { + expectedLikes += point.likeCount; + log.info(`[${point.name}] 标签预计获得 ${point.likeCount} 赞`); + } + + log.info(`[Heat] 本轮已完成 ${completedSinceRestore}/${restoreEveryPoints} 个 Point`); + } + } + + if (pointsWithoutLikeTag === 0) { + log.info(`[System] 全部完成:执行 ${completedPoints} 个 Point,标签预计总计 ${expectedLikes} 赞`); + } else { + log.info( + `[System] 全部完成:执行 ${completedPoints} 个 Point,已标注路线预计 ${expectedLikes} 赞,` + + `${pointsWithoutLikeTag} 个 Point 未填写点赞标签` + ); + } + } catch (error) { + log.error(`[System] 脚本终止:${errorText(error)}`); + throw error; + } +} diff --git a/repo/js/IceObjectNpcLike/src/utils/common.js b/repo/js/IceObjectNpcLike/src/utils/common.js new file mode 100644 index 0000000000..b5d0a73f61 --- /dev/null +++ b/repo/js/IceObjectNpcLike/src/utils/common.js @@ -0,0 +1,19 @@ +export function errorText(error) { + if (error && error.message) { + return error.message; + } + return String(error); +} + +export function formatPointNumber(number) { + return String(number).padStart(2, "0"); +} + +export function baseName(path) { + const normalized = String(path).replace(/\\/g, "/"); + return normalized.slice(normalized.lastIndexOf("/") + 1); +} + +export function fileStem(path) { + return baseName(path).replace(/\.json$/i, ""); +}