tarinai/js/achievements.js
2026-07-16 22:12:03 +09:00

1978 lines
103 KiB
JavaScript

"use strict";
(function initAchievementSystem(global) {
const STORAGE_KEY = "tarinai_achievements_v2";
const PLAYER_KEY = "tarinai_achievement_player_v1";
const API_URL = String(global.TARINAI_ACHIEVEMENT_API || "achievement_api.php");
const GAME_VERSION = String(global.TARINAI_VERSION || "39.16.74");
const DEBUG_MODE = ["1", "true"].includes(new URLSearchParams(global.location?.search || "").get("debug"));
const PLAYER_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const ABSOLUTE_ZERO_C = -273.15;
const CONTINUOUS_PLAY_TARGET_MS = 60 * 60 * 1000;
const CONTINUOUS_PLAY_24H_TARGET_MS = 24 * 60 * 60 * 1000;
const HELD_TARGET_MS = 30 * 1000;
const COMPLETIONIST_ID = "true_tarinai_observer";
const GENERATION_TARGET = 10;
const CHAOS_FIGHT_TARGET = 666;
const SNIPER_SHOT_TARGET = 333;
const FERTILITY_BIRTH_TARGET = 721;
const DAILY_PLAY_TARGET = 7;
const WIRE_SHOCK_TARGET = 7;
const DAY_MS = 24 * 60 * 60 * 1000;
const ROBOT_CLEAN_TARGET = 200;
const ROBOT_ALL_TARGET_MASK = 0x3f;
const SYNC_DEBOUNCE_MS = 450;
const FULL_SYNC_INTERVAL_MS = 5 * 60 * 1000;
const SPELL_STATE_VERSION = 5;
const MEDICINE_LEDGER_TYPES = Object.freeze(["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"]);
const DIRECT_TREATMENT_TYPES = new Set(["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"]);
const PRE_UNLOCK_DESCRIPTION_IDS = new Set([
"natural_zunchi_slave",
"natural_tarinai_king",
"revolution",
"laxative_starvation",
"natural_zunchi_slave_5_generations",
"natural_tarinai_king_3_generations",
"first_birth",
"lifespan_completed",
"birth_50_in_60_seconds",
"death_50_in_10_seconds",
"all_non_sleep_diseased_25",
"colony_happy",
"safe_colony_25_5_minutes",
"ants_alive_25",
"great_mother_1000_births",
"continuous_play_1_hour",
"low_fps_single_digit",
"continuous_play_24_hours",
"robot_cleaner_100",
"mercury_lifespan",
"enemy_enemy_friend",
"eternal_history_generation_10",
"town_doctor_50",
"chaos_seeker_666_fights",
"fertility_seeker_721_love_births",
"daily_play_7_days",
]);
const ACHIEVEMENT_CATEGORIES = Object.freeze([
Object.freeze({
id: "ecology",
title: "\u751f\u614b",
ids: Object.freeze([
"first_birth", "natural_zunchi_slave", "natural_tarinai_king",
"natural_zunchi_slave_5_generations", "natural_tarinai_king_3_generations",
"ant_nest_without_tarinai",
"birth_50_in_60_seconds", "great_mother_1000_births", "lifespan_completed",
"all_non_sleep_diseased_25", "colony_happy", "direct_feed_33",
"safe_colony_25_5_minutes", "ants_alive_25", "overprotective",
"self_sufficient", "rain_shelter_all", "eternal_history_generation_10", "town_doctor_50",
"fertility_seeker_721_love_births",
]),
}),
Object.freeze({
id: "experiment",
title: "\u5b9f\u9a13",
ids: Object.freeze([
"self_zunchi_death", "death_50_in_10_seconds", "soccer_ball_death",
"fight_pair_danger_kill", "ignite_during_birth_ritual", "laxative_starvation",
"ants_killed_100", "below_absolute_zero_item", "sauna_cold_plunge",
"medicine_ledger_all", "mercury_lifespan", "enemy_enemy_friend",
"revolution", "fuel_to_fire", "king_full_satisfaction", "slave_zero_satisfaction",
"chaos_seeker_666_fights", "sticky_bomb_15_passes", "wire_shock_7_tarinai",
]),
}),
Object.freeze({
id: "construction",
title: "\u5efa\u7bc9",
ids: Object.freeze([
"placed_objects_100", "mechanized_industry", "information_industry", "robot_cleaner_100",
"minimalist_happy", "unplanned_city_30", "clean_freak_robot_only", "megalopolis",
"sandbox_five_toilets", "park_ground_changed", "ground_change_4_in_1_second",
]),
}),
Object.freeze({
id: "operation",
title: "\u305d\u306e\u4ed6",
ids: Object.freeze([
"idle_observer_5_minutes", "secret_collection_9_slots", "pause_spam_4_in_1_second",
"undo_mass_revival", "held_30_seconds", "undo_20", "redo_20",
"poke_plushie_fling", "sniper_333_shots", "low_fps_single_digit",
"daily_play_7_days", "continuous_play_24_hours", "continuous_play_1_hour", "true_tarinai_observer",
]),
}),
]);
const DEFINITIONS = Object.freeze([
Object.freeze({ id: "first_birth", title: "\u306f\u3058\u3081\u3066\u306e\u7e41\u6b96", description: "\u305f\u308a\u306a\u3044\u306e\u7e41\u6b96\u30921\u56de\u6210\u529f\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "natural_zunchi_slave", title: "\u305a\u3093\u3061\u3069\u308c\u3044", description: "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u51fa\u73fe\u3059\u308b\u3002" }),
Object.freeze({ id: "natural_tarinai_king", title: "\u305f\u308a\u306a\u3044\u738b", description: "\u305f\u308a\u306a\u3044\u738b\u304c\u51fa\u73fe\u3059\u308b\u3002" }),
Object.freeze({ id: "self_zunchi_death", title: "\u3042\u3041\uff3e\uff5e \u305f\u307e\u3089\u306d\u3047\u305c", description: "\u304a\u3057\u308a\u92f2\u3092\u629c\u3044\u305f\u6642\u3001\u81ea\u5206\u306e\u305a\u3093\u3061\u3067\u81ea\u7206\u3059\u308b\u3002" }),
Object.freeze({ id: "natural_zunchi_slave_5_generations", title: "\u672b\u4ee3\u307e\u3067\u306e\u6065", description: "5\u4ee3\u9023\u7d9a\u3067\u305a\u3093\u3061\u3069\u308c\u3044\uff08\u9396\u306a\u3057\uff09" }),
Object.freeze({ id: "natural_tarinai_king_3_generations", title: "\u83ef\u9e97\u306a\u308b\u738b\u65cf", description: "3\u4ee3\u9023\u7d9a\u3067\u305f\u308a\u306a\u3044\u738b\uff08\u738b\u51a0\u306a\u3057\uff09" }),
Object.freeze({ id: "death_50_in_10_seconds", title: "6\u756a\u76ee\u306e\u5927\u91cf\u7d76\u6ec5", description: "10\u79d2\u4ee5\u5185\u306b50\u4f53\u306e\u305f\u308a\u306a\u3044\u304c\u6b7b\u4ea1\u3059\u308b\u3002" }),
Object.freeze({ id: "birth_50_in_60_seconds", title: "\u30d9\u30d3\u30fc\u30d6\u30fc\u30e0", description: "1\u5206\u4ee5\u5185\u306b30\u4f53\u306e\u305f\u308a\u306a\u3044\u304c\u8a95\u751f\u3059\u308b\u3002" }),
Object.freeze({ id: "great_mother_1000_births", title: "\u5927\u3044\u306a\u308b\u6bcd", description: "\u305f\u308a\u306a\u3044\u3092\u7e41\u6b96\u3067\u7d2f\u8a083333\u4f53\u8a95\u751f\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "lifespan_completed", title: "\u5929\u5bff", description: "\u305f\u308a\u306a\u30441\u4f53\u304c\u5929\u5bff\u3092\u5168\u3046\u3059\u308b\u3002" }),
Object.freeze({ id: "soccer_ball_death", title: "\u8c46\u91ce\u3001\u6bba\u30c3\u30ab\u30fc\u3084\u308d\u3046\u305c\uff01", description: "\u30b5\u30c3\u30ab\u30fc\u30dc\u30fc\u30eb\u3068\u306e\u885d\u7a81\u3067\u3001\u305f\u308a\u306a\u30441\u4f53\u304c\u6b7b\u4ea1\u3059\u308b\u3002" }),
Object.freeze({ id: "fight_pair_danger_kill", title: "\u55a7\u5629\u4e21\u6210\u6557", description: "\u55a7\u5629\u4e2d\u306e\u305f\u308a\u306a\u3044\u3092\u3001\u4e21\u65b9\u3068\u3082\u5371\u967a\u30ab\u30c6\u30b4\u30ea\u306e\u30a2\u30a4\u30c6\u30e0\u3067\u5012\u3059\u3002" }),
Object.freeze({ id: "all_non_sleep_diseased_25", title: "\u7dca\u6025\u4e8b\u614b\u5ba3\u8a00", description: "25\u4f53\u4ee5\u4e0a\u304c\u5b58\u547d\u3057\u3001\u30d5\u30a3\u30fc\u30eb\u30c9\u4e0a\u306e\u5168\u54e1\u304c\u306d\u3080\u308a\u75c5\u4ee5\u5916\u306e\u75c5\u6c17\u306b\u304b\u304b\u308b\u3002" }),
Object.freeze({ id: "colony_happy", title: "\u7ba1\u7406\u3055\u308c\u305f\u697d\u5712", description: "\u30b3\u30ed\u30cb\u30fc\u72b6\u614b\u3092\u3057\u3070\u3089\u304f\u300c\u5e78\u798f\u300d\u306b\u3059\u308b\u3002" }),
Object.freeze({ id: "direct_feed_33", title: "\u990c\u3084\u308a\u3058\u3044\u3055\u3093", description: "\u624b\u304b\u3089\u76f4\u63a533\u56de\u990c\u3092\u4e0e\u3048\u308b\u3002" }),
Object.freeze({ id: "ignite_during_birth_ritual", title: "\u71c3\u3048\u4e0a\u304c\u308b\u604b", description: "\u305f\u308a\u306a\u3044\u304c\u30d8\u30b3\u30d8\u30b3\u4e2d\u306b\u7740\u706b\u3059\u308b\u3002" }),
Object.freeze({ id: "laxative_starvation", title: "\u5185\u81d3\u5168\u90e8\u51fa\u305f", description: "\u4e0b\u5264\u306e\u52b9\u679c\u4e2d\u306b\u9913\u6b7b\u3059\u308b\u3002" }),
Object.freeze({ id: "idle_observer_5_minutes", title: "\u76e3\u8996\u30ab\u30e1\u30e9", description: "5\u5206\u9593\u753b\u9762\u3092\u898b\u308b\u3060\u3051" }),
Object.freeze({ id: "placed_objects_100", title: "\u7269\u3067\u3044\u3063\u3071\u3044", description: "\u914d\u7f6e\u7269\u3092\u7d2f\u8a08100\u500b\u7f6e\u304f\u3002" }),
Object.freeze({ id: "mechanized_industry", title: "\u6a5f\u68b0\u5316\u7523\u696d", description: "\u691c\u77e5\u5668\u306e\u4fe1\u53f7\u3067\u63a5\u7d9a\u5148\u3092\u4f5c\u52d5\u3055\u305b\u3001\u30ed\u30fc\u30d7\u30fb\u68d2\u30fb\u3070\u306d\u3092\u305d\u308c\u305e\u308c1\u672c\u4ee5\u4e0a\u8a2d\u7f6e\u3059\u308b\u3002" }),
Object.freeze({ id: "safe_colony_25_5_minutes", title: "\u5b89\u5168\u7b2c\u4e00", description: "25\u4f53\u4ee5\u4e0a\u3044\u308b\u72b6\u614b\u3092\u30b2\u30fc\u30e0\u51855\u5206\u9593\u7dad\u6301\u3057\u3001\u305d\u306e\u9593\u5bff\u547d\u4ee5\u5916\u3067\u6b7b\u4ea1\u8005\u3092\u51fa\u3055\u306a\u3044\u3002" }),
Object.freeze({ id: "ants_alive_25", title: "\u30a2\u30ea\u306e\u60d1\u661f", description: "\u751f\u5b58\u3057\u3066\u3044\u308b\u30a2\u30ea\u304c25\u4f53\u306b\u5230\u9054\u3059\u308b\u3002" }),
Object.freeze({ id: "ants_killed_100", title: "\u5bb3\u866b\u99c6\u9664", description: "\u30a2\u30ea\u3092\u7d2f\u8a08100\u4f53\u5012\u3059\u3002" }),
Object.freeze({ id: "secret_collection_9_slots", title: "\u79d8\u5bc6\u306e\u30b3\u30ec\u30af\u30b7\u30e7\u30f3", description: "\u30bb\u30fc\u30d6\u30b9\u30ed\u30c3\u30c89\u3064\u3092\u3059\u3079\u3066\u57cb\u3081\u308b\u3002" }),
Object.freeze({ id: "pause_spam_4_in_1_second", title: "\u4f55\u3084\u3063\u3066\u308b\u306e\uff1f", description: "\u4f55\u3082\u306a\u3044\u90e8\u5206\u30921\u79d2\u9593\u306b4\u56de\u9023\u6253\u3059\u308b\u3002" }),
Object.freeze({ id: "continuous_play_1_hour", title: "\u3081\u3063\u3061\u3083\u305f\u308a\u306a\u3044\u89b3\u5bdf", description: "1\u6642\u9593\u9023\u7d9a\u30d7\u30ec\u30a4\u3059\u308b\u3002" }),
Object.freeze({ id: "below_absolute_zero_item", title: "\u7269\u7406\u6cd5\u5247\u3092\u4e0b\u5074\u306b\u8d85\u8d8a", description: "\u6e29\u5ea6\u8a08\u7b97\u3055\u308c\u308b\u30a2\u30a4\u30c6\u30e0\u30921\u3064\u7d76\u5bfe\u96f6\u5ea6\u3092\u4e0b\u56de\u308b\u6e29\u5ea6\u306b\u3059\u308b\u3002" }),
Object.freeze({ id: "undo_mass_revival", title: "\u898b\u306a\u304b\u3063\u305f\u3053\u3068\u306b\u3057\u3088\u3046", description: "\u4e00\u5ea6\u306eUndo\u306710\u4f53\u4ee5\u4e0a\u306e\u6b7b\u4ea1\u3092\u306a\u304b\u3063\u305f\u3053\u3068\u306b\u3059\u308b\u3002" }),
Object.freeze({ id: "robot_cleaner_100", title: "\u6e05\u6383\u696d\u8005", description: "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u305a\u3093\u3061\u30fb\u6b7b\u9ab8\u30fb\u6c5a\u308c\u3092\u7d2f\u8a08200\u500b\u51e6\u7406\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "held_30_seconds", title: "\u7a7a\u4e2d\u751f\u6d3b", description: "\u540c\u3058\u305f\u308a\u306a\u3044\u309230\u79d2\u9593\u3064\u307e\u307f\u7d9a\u3051\u308b\u3002" }),
Object.freeze({ id: "minimalist_happy", title: "\u30df\u30cb\u30de\u30ea\u30b9\u30c8", description: "\u30d7\u30ec\u30a4\u958b\u59cb6\u65e5\u76ee\u4ee5\u964d\u3001\u914d\u7f6e\u7269\u309210\u500b\u4ee5\u4e0b\u306b\u6291\u3048\u3066\u30b3\u30ed\u30cb\u30fc\u72b6\u614b\u3092\u300c\u5e78\u798f\u300d\u306b\u3059\u308b\u3002" }),
Object.freeze({ id: "overprotective", title: "\u904e\u4fdd\u8b77", description: "\u540c\u3058\u305f\u308a\u306a\u3044\u306b\u624b\u304b\u308910\u56de\u990c\u3092\u4e0e\u3048\u30013\u56de\u4ee5\u4e0a\u85ac\u3067\u6cbb\u7642\u3059\u308b\u3002" }),
Object.freeze({ id: "self_sufficient", title: "\u81ea\u7d66\u81ea\u8db3", description: "\u8907\u88fd\u6a5f\u304c\u306a\u3044\u72b6\u614b\u3067\u300120\u4f53\u4ee5\u4e0a\u3092\u76f4\u63a5\u7d66\u990c\u305b\u305a10\u5206\u9593\u7dad\u6301\u3059\u308b\u3002" }),
Object.freeze({ id: "unplanned_city_30", title: "\u7121\u8a08\u753b\u90fd\u5e02", description: "\u914d\u7f6e\u5f8c30\u79d2\u4ee5\u5185\u306b\u524a\u9664\u3059\u308b\u64cd\u4f5c\u3092\u7d2f\u8a0830\u56de\u884c\u3046\u3002" }),
Object.freeze({ id: "sauna_cold_plunge", title: "\u30b5\u30a6\u30ca\u6c34\u98a8\u5442", description: "\u6691\u3059\u304e\u308b\u72b6\u614b\u306b\u306a\u3063\u305f\u305f\u308a\u306a\u3044\u3092\u300115\u79d2\u4ee5\u5185\u306b\u5bd2\u3059\u304e\u308b\u72b6\u614b\u306b\u3059\u308b\u3002" }),
Object.freeze({ id: "rain_shelter_all", title: "\u96e8\u5bbf\u308a\u5b8c\u4e86", description: "\u96e8\u5929\u6642\u300120\u4f53\u4ee5\u4e0a\u306e\u5168\u54e1\u3092\u30d1\u30e9\u30bd\u30eb\u30fb\u5de3\u7bb1\u30fb\u571f\u7ba1\u3067\u96e8\u5bbf\u308a\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "medicine_ledger_all", title: "\u304a\u304f\u3059\u308a\u624b\u5e33\u5168\u57cb\u3081", description: "\u5168\u7a2e\u985e\u306e\u85ac\u3092\u5c11\u306a\u304f\u3068\u30821\u56de\u4f7f\u7528\u3059\u308b\u3002" }),
Object.freeze({ id: "mercury_lifespan", title: "\u4e0d\u8001\u4e0d\u6b7b\u307e\u3067\u3042\u3068\u4e00\u6b69", description: "\u6c34\u9280\u3092\u4f7f\u7528\u3057\u305f\u305f\u308a\u306a\u3044\u304c\u5929\u5bff\u3092\u5168\u3046\u3059\u308b\u3002" }),
Object.freeze({ id: "enemy_enemy_friend", title: "\u6575\u306e\u6575\u306f\u5473\u65b9", description: "\u305f\u308a\u306a\u304430\u4f53\u4ee5\u4e0a\u3092\u7121\u50b7\u3067\u4fdd\u3061\u306a\u304c\u3089\u3001\u5371\u967a\u30a2\u30a4\u30c6\u30e0\u3067\u30a2\u30ea\u309210\u4f53\u5012\u3059\u3002" }),
Object.freeze({ id: "revolution", title: "\u9769\u547d", description: "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u3001\u305f\u308a\u306a\u3044\u738b\u3092\u55a7\u5629\u3067\u6b7b\u4ea1\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "fuel_to_fire", title: "\u706b\u306b\u6cb9", description: "\u3051\u3093\u304b\u9905\u3092\u4e0e\u3048\u305f\u305f\u308a\u306a\u3044\u304c30\u79d2\u4ee5\u5185\u306b\u55a7\u5629\u3092\u59cb\u3081\u308b\u3002" }),
Object.freeze({ id: "undo_20", title: "\u3084\u3063\u3071\u306a\u3057", description: "Undo\u3092\u7d2f\u8a0820\u56de\u6210\u529f\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "redo_20", title: "\u3084\u3063\u3071\u3042\u308a", description: "Redo\u3092\u7d2f\u8a0820\u56de\u6210\u529f\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "eternal_history_generation_10", title: "\u60a0\u4e45\u306e\u6b74\u53f2", description: "\u4e16\u4ee3\u304c10\u306b\u5230\u9054\u3059\u308b\u3002" }),
Object.freeze({ id: "king_full_satisfaction", title: "\u4e94\u4f53\u6e80\u8db3", description: "\u305f\u308a\u306a\u3044\u738b1\u500b\u4f53\u306b\u30d7\u30ed\u30c6\u30a4\u30f3\u3068\u5de8\u5927\u85ac\u306e\u52b9\u679c\u3092\u540c\u6642\u306b\u4ed8\u4e0e\u3059\u308b\u3002" }),
Object.freeze({ id: "slave_zero_satisfaction", title: "\u96f6\u4f53\u6e80\u8db3", description: "\u305a\u3093\u3061\u3069\u308c\u30441\u500b\u4f53\u306b\u30f3\u30a4\u30c6\u30ed\u30d7\u3068\u77ee\u5c0f\u85ac\u306e\u52b9\u679c\u3092\u540c\u6642\u306b\u4ed8\u4e0e\u3059\u308b\u3002" }),
Object.freeze({ id: "town_doctor_50", title: "\u753a\u306e\u304a\u533b\u8005\u3055\u3093", description: "\u6551\u6025\u85ac\u3092\u7d2f\u8a0850\u56de\u4f7f\u7528\u3057\u3001\u4f53\u529b\u3092\u56de\u5fa9\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "poke_plushie_fling", title: "\u5272\u3068\u3075\u308f\u3075\u308f", description: "\u306c\u3044\u3050\u308b\u307f\u3092\u300c\u3064\u3064\u304f\u300d\u3067\u5f3e\u304d\u98db\u3070\u3059\u3002" }),
Object.freeze({ id: "chaos_seeker_666_fights", title: "\u6df7\u6c8c\u30f2\u5e0c\u6c42\u30b9\u30eb\u8005", description: "\u3051\u3093\u304b\u9905\u306e\u5f71\u97ff\u4e0b\u3067\u55a7\u5629\u3092\u7d2f\u8a08666\u56de\u767a\u751f\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "clean_freak_robot_only", title: "\u6f54\u7656\u75c7", description: "\u30ed\u30dc\u30c3\u30c8\u6383\u9664\u6a5f\u306e\u8a2d\u5b9a\u5168\u9805\u76ee\u306b\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u3066\u7a3c\u50cd\u3055\u305b\u3001\u30d5\u30a3\u30fc\u30eb\u30c9\u4e0a\u306b\u30ed\u30dc\u30c3\u30c8\u6383\u9664\u6a5f\u4ee5\u5916\u3092\u5b58\u5728\u3055\u305b\u306a\u3044\u3002" }),
Object.freeze({ id: "true_tarinai_observer", title: "\u771f\u30fb\u305f\u308a\u306a\u3044\u89b3\u5bdf", description: "\u3053\u308c\u4ee5\u5916\u306e\u5168\u3066\u306e\u5b9f\u7e3e\u3092\u9054\u6210\u3059\u308b\u3002" }),
Object.freeze({ id: "sniper_333_shots", title: "\u72d9\u6483\u624b", description: "\u7d2f\u8a08333\u767a\u6483\u3064\u3002" }),
Object.freeze({ id: "megalopolis", title: "\u30e1\u30ac\u30ed\u30dd\u30ea\u30b9", description: "\u8907\u88fd\u6a5f\u309210\u500b\u4ee5\u4e0a\u3001\u5e72\u8349\u5bdd\u5e8a\u30fb\u5de3\u7bb1\u30fb\u571f\u7ba1\u3092\u5408\u8a0815\u500b\u4ee5\u4e0a\u8a2d\u7f6e\u3057\u3001\u305f\u308a\u306a\u3044\u3092100\u4f53\u4ee5\u4e0a\u751f\u5b58\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "fertility_seeker_721_love_births", title: "\u8c4a\u7a63\u30f2\u5e0c\u6c42\u30b9\u30eb\u8005", description: "\u3078\u3053\u9905\u306e\u5f71\u97ff\u4e0b\u3067\u7e41\u6b96\u3092721\u56de\u6210\u529f\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "low_fps_single_digit", title: "\u30b9\u30da\u30c3\u30af\u304c\u305f\u308a\u306a\u3044", description: "fps\u30921\u6841\u306b\u3059\u308b\u3002" }),
Object.freeze({ id: "continuous_play_24_hours", title: "\u5bdd\u308d", description: "24\u6642\u9593\u9023\u7d9a\u3067\u30d7\u30ec\u30a4\u3059\u308b\u3002" }),
Object.freeze({ id: "sandbox_five_toilets", title: "\u7802\u5834", description: "\u30c8\u30a4\u30ec\u306e\u7802\u3092\u30d5\u30a3\u30fc\u30eb\u30c9\u306b5\u500b\u8a2d\u7f6e\u3059\u308b\u3002" }),
Object.freeze({ id: "park_ground_changed", title: "\u305d\u3053\u516c\u5171\u7a7a\u9593\u3060\u3088\uff1f", description: "\u516c\u5712\u306e\u5730\u9762\u3092\u52dd\u624b\u306b\u5909\u66f4\u3059\u308b\u3002" }),
Object.freeze({ id: "ground_change_4_in_1_second", title: "\u7834\u683c\u306e\u5de5\u4e8b\u8cbb\u7528", description: "\u5730\u9762\u3092\u5909\u3048\u3059\u304e\u3066\u7834\u7523\u3059\u308b\u3002" }),
Object.freeze({ id: "ant_nest_without_tarinai", title: "\u30a2\u30ea\u89b3\u5bdf", description: "\u30a2\u30ea\u306e\u5de3\u304c\u3042\u308b\u304c\u3001\u305f\u308a\u306a\u3044\u304c\u3044\u306a\u3044\u3002" }),
Object.freeze({ id: "sticky_bomb_15_passes", title: "\u547d\u306e\u30d0\u30c8\u30f3", description: "\u7c98\u7740\u30dc\u30e0\u3092\u7206\u767a\u307e\u3067\u306b15\u56de\u4ee5\u4e0a\u30d1\u30b9\u3059\u308b\u3002" }),
Object.freeze({ id: "daily_play_7_days", title: "\u6bce\u65e5\u305f\u308a\u306a\u3044\u89b3\u5bdf", description: "7\u65e5\u9023\u7d9a\u3067\u30d7\u30ec\u30a4\u3059\u308b\u3002" }),
Object.freeze({ id: "wire_shock_7_tarinai", title: "\u62b5\u6297\u5668\u3058\u3083\u306a\u3044", description: "\u96fb\u6d41\u304c\u6d41\u308c\u3066\u3044\u308b1\u672c\u306e\u96fb\u7dda\u30677\u4f53\u306e\u305f\u308a\u306a\u3044\u3092\u611f\u96fb\u3055\u305b\u308b\u3002" }),
Object.freeze({ id: "information_industry", title: "\u60c5\u5831\u5316\u7523\u696d", description: "\u57fa\u76e4\u540c\u58eb\u3092\u96fb\u7dda\u985e\u3067\u3064\u306a\u304e\u3001\u4fe1\u53f7\u3092\u6d41\u3059\u3002" }),
]);
const definitionById = new Map(DEFINITIONS.map(def => [def.id, def]));
const CATEGORY_DEFINITIONS = new Map(ACHIEVEMENT_CATEGORIES.map(category => [
category.id,
Object.freeze(category.ids.map(id => definitionById.get(id)).filter(Boolean)),
]));
const dom = {
button: document.getElementById("achievementsBtn"),
buttonCount: document.getElementById("achievementButtonCount"),
dialog: document.getElementById("achievementsDialog"),
dialogCount: document.getElementById("achievementDialogCount"),
closeButton: document.getElementById("achievementsCloseBtn"),
closeIconButton: document.getElementById("achievementsCloseIconBtn"),
refreshButton: document.getElementById("achievementRefreshBtn"),
resetButton: document.getElementById("achievementResetBtn"),
unlockAllButton: document.getElementById("achievementUnlockAllBtn"),
list: document.getElementById("achievementList"),
sharedStatus: document.getElementById("achievementSharedStatus"),
toast: document.getElementById("achievementToast"),
toastTitle: document.getElementById("achievementToastTitle"),
pauseButton: document.getElementById("pauseBtn"),
};
let state = loadState();
let shared = { connected: false, loading: false, totalPlayers: 0, achievements: {} };
let requestSerial = 0;
let runtimeByWorld = new WeakMap();
let dangerFightDeathsByWorld = new WeakMap();
let wireShockTrackers = new WeakMap();
let syncQueue = Promise.resolve();
let toastQueue = [];
let toastActive = false;
let emptyClickTimes = [];
let groundChangeTimes = [];
let continuousPlayStartedAt = Date.now();
let continuousPlayLastHeartbeatAt = continuousPlayStartedAt;
let lastListSignature = "";
let scheduledSyncTimer = 0;
function blankState() {
return { unlocked: {}, pending: [], resetPending: false, debugUnlocked: [], progress: { placementCount: 0, linkTypes: [], signalActivated: false, antKills: 0, birthCount: 0, quickDeleteCount: 0, undoCount: 0, redoCount: 0, medicineTypes: [], firstAidHeals: 0, fightMochiFightCount: 0, shotCount: 0, loveMochiBirthCount: 0, dailyPlayStreak: 0, dailyPlayLastDay: 0 } };
}
function loadState() {
try {
const parsed = JSON.parse(global.localStorage?.getItem(STORAGE_KEY) || "null");
if (!parsed || typeof parsed !== "object") return blankState();
const unlockedSource = parsed.unlocked && typeof parsed.unlocked === "object" ? parsed.unlocked : {};
const unlocked = {};
for (const [id, timestamp] of Object.entries(unlockedSource)) {
if (definitionById.has(id) && Number(timestamp) > 0) unlocked[id] = Number(timestamp);
}
const legacyMechanizedAt = Math.max(
Number(unlockedSource.signal_automation_first || 0) || 0,
Number(unlockedSource.link_craftsman || 0) || 0
);
if (!unlocked.mechanized_industry
&& Number(unlockedSource.signal_automation_first || 0) > 0
&& Number(unlockedSource.link_craftsman || 0) > 0) {
unlocked.mechanized_industry = legacyMechanizedAt || Date.now();
}
const debugUnlockedSource = Array.isArray(parsed.debugUnlocked) ? parsed.debugUnlocked : [];
const debugUnlocked = [...new Set(debugUnlockedSource.filter(id => definitionById.has(id)))];
if (!DEBUG_MODE) {
for (const id of debugUnlocked) delete unlocked[id];
}
const pendingSource = Array.isArray(parsed.pending) ? parsed.pending : [];
const pending = pendingSource.filter(id => definitionById.has(id) && !debugUnlocked.includes(id));
if (unlocked.mechanized_industry && !pending.includes("mechanized_industry")
&& (pendingSource.includes("signal_automation_first") || pendingSource.includes("link_craftsman"))) {
pending.push("mechanized_industry");
}
const progressSource = parsed.progress && typeof parsed.progress === "object" ? parsed.progress : {};
const legacyLinkTypes = Number(unlockedSource.link_craftsman || 0) > 0 ? ["rope", "rod", "spring"] : [];
const progress = {
placementCount: Math.max(0, Math.floor(Number(progressSource.placementCount || 0) || 0)),
linkTypes: [...new Set([...(Array.isArray(progressSource.linkTypes) ? progressSource.linkTypes : []), ...legacyLinkTypes].filter(type => ["rope", "rod", "spring"].includes(type)))],
signalActivated: Boolean(progressSource.signalActivated || unlockedSource.signal_automation_first),
antKills: Math.max(0, Math.floor(Number(progressSource.antKills || 0) || 0)),
birthCount: Math.max(0, Math.floor(Number(progressSource.birthCount || 0) || 0)),
quickDeleteCount: Math.max(0, Math.floor(Number(progressSource.quickDeleteCount || 0) || 0)),
undoCount: Math.max(0, Math.floor(Number(progressSource.undoCount || 0) || 0)),
redoCount: Math.max(0, Math.floor(Number(progressSource.redoCount || 0) || 0)),
medicineTypes: [...new Set((Array.isArray(progressSource.medicineTypes) ? progressSource.medicineTypes : []).filter(type => MEDICINE_LEDGER_TYPES.includes(type)))],
firstAidHeals: Math.max(0, Math.floor(Number(progressSource.firstAidHeals || 0) || 0)),
fightMochiFightCount: Math.max(0, Math.floor(Number(progressSource.fightMochiFightCount || 0) || 0)),
shotCount: Math.max(0, Math.floor(Number(progressSource.shotCount || 0) || 0)),
loveMochiBirthCount: Math.max(0, Math.floor(Number(progressSource.loveMochiBirthCount || 0) || 0)),
dailyPlayStreak: Math.min(DAILY_PLAY_TARGET, Math.max(0, Math.floor(Number(progressSource.dailyPlayStreak || 0) || 0))),
dailyPlayLastDay: Math.max(0, Math.floor(Number(progressSource.dailyPlayLastDay || 0) || 0)),
};
const missingCompletionRequirement = DEFINITIONS.some(definition => definition.id !== COMPLETIONIST_ID && !unlocked[definition.id]);
if (unlocked[COMPLETIONIST_ID] && missingCompletionRequirement) {
delete unlocked[COMPLETIONIST_ID];
const pendingIndex = pending.indexOf(COMPLETIONIST_ID);
if (pendingIndex >= 0) pending.splice(pendingIndex, 1);
}
return { unlocked, pending: [...new Set(pending)], resetPending: Boolean(parsed.resetPending), debugUnlocked: DEBUG_MODE ? debugUnlocked : [], progress };
} catch (_) {
return blankState();
}
}
function saveState() {
try { global.localStorage?.setItem(STORAGE_KEY, JSON.stringify(state)); }
catch (_) {}
}
function createPlayerId() {
if (global.crypto?.randomUUID) return global.crypto.randomUUID();
const hex = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
return hex.replace(/[xy]/g, char => {
const value = Math.floor(Math.random() * 16);
return (char === "x" ? value : ((value & 3) | 8)).toString(16);
});
}
function playerId() {
try {
const stored = String(global.localStorage?.getItem(PLAYER_KEY) || "");
if (PLAYER_ID_PATTERN.test(stored)) return stored;
const next = createPlayerId();
global.localStorage?.setItem(PLAYER_KEY, next);
return next;
} catch (_) {
if (!playerId.memory) playerId.memory = createPlayerId();
return playerId.memory;
}
}
function unlockedCount() {
return DEFINITIONS.reduce((sum, def) => sum + (state.unlocked[def.id] ? 1 : 0), 0);
}
function formatDate(timestamp) {
const value = Number(timestamp) || 0;
if (!value) return "";
try {
return new Intl.DateTimeFormat("ja-JP", {
year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit",
}).format(new Date(value));
} catch (_) {
return new Date(value).toLocaleString();
}
}
function percentageValue(id) {
const item = shared.achievements?.[id];
const value = Number(item?.percentage);
return shared.connected && Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0;
}
function percentageText(id) {
return shared.connected ? `${percentageValue(id).toFixed(1)}%` : "--";
}
function achieverCountValue(id) {
const value = Number(shared.achievements?.[id]?.unlockedPlayers);
return shared.connected && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : null;
}
function achieverCountText(id) {
const value = achieverCountValue(id);
return value === null ? "--" : `${value}\u4eba`;
}
function sharedAchievementText(id) {
return `\u5168\u4f53\u9054\u6210\u7387 ${percentageText(id)}\uff08\u9054\u6210\u8005 ${achieverCountText(id)}\uff09`;
}
function continuousPlayElapsedMs(now = Date.now()) {
if (state.unlocked.continuous_play_24_hours) return CONTINUOUS_PLAY_24H_TARGET_MS;
const timestamp = Number(now);
if (!continuousPlayStartedAt || !Number.isFinite(timestamp)) return 0;
return Math.max(0, timestamp - continuousPlayStartedAt);
}
function formatElapsed(ms = 0) {
const totalSeconds = Math.max(0, Math.floor(Number(ms || 0) / 1000));
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
function localProgressText(id) {
if (id === "great_mother_1000_births") {
const target = 3333;
const count = state.unlocked[id] ? target : Math.min(target, Math.max(0, Math.floor(Number(state.progress?.birthCount || 0) || 0)));
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(count / target * 100).toFixed(1)}%\uff08${count} / ${target}\u4f53\uff09`;
}
if (id === "town_doctor_50") {
const target = 50;
const count = state.unlocked[id] ? target : Math.min(target, Math.max(0, Math.floor(Number(state.progress?.firstAidHeals || 0) || 0)));
return `\u73fe\u5728 ${count} / ${target}\u56de`;
}
if (id === "chaos_seeker_666_fights") {
const target = CHAOS_FIGHT_TARGET;
const count = state.unlocked[id] ? target : Math.min(target, Math.max(0, Math.floor(Number(state.progress?.fightMochiFightCount || 0) || 0)));
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(count / target * 100).toFixed(1)}%\uff08${count} / ${target}\u56de\uff09`;
}
if (id === "fertility_seeker_721_love_births") {
const target = FERTILITY_BIRTH_TARGET;
const count = state.unlocked[id] ? target : Math.min(target, Math.max(0, Math.floor(Number(state.progress?.loveMochiBirthCount || 0) || 0)));
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(count / target * 100).toFixed(1)}%\uff08${count} / ${target}\u56de\uff09`;
}
if (id === "daily_play_7_days") {
const count = state.unlocked[id] ? DAILY_PLAY_TARGET : Math.min(DAILY_PLAY_TARGET, Math.max(0, Math.floor(Number(state.progress?.dailyPlayStreak || 0) || 0)));
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(count / DAILY_PLAY_TARGET * 100).toFixed(1)}%\uff08${count} / ${DAILY_PLAY_TARGET}\u65e5\uff09`;
}
if (id === "continuous_play_1_hour") {
const elapsed = Math.min(CONTINUOUS_PLAY_TARGET_MS, continuousPlayElapsedMs());
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(elapsed / CONTINUOUS_PLAY_TARGET_MS * 100).toFixed(1)}%\uff08${formatElapsed(elapsed)} / 60:00\uff09`;
}
if (id === "continuous_play_24_hours") {
const elapsed = Math.min(CONTINUOUS_PLAY_24H_TARGET_MS, continuousPlayElapsedMs());
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(elapsed / CONTINUOUS_PLAY_24H_TARGET_MS * 100).toFixed(1)}%\uff08${formatElapsed(elapsed)} / 1440:00\uff09`;
}
if (id === "enemy_enemy_friend") {
const world = global.world || null;
const count = state.unlocked[id] ? 10 : Math.min(10, Math.max(0, Math.floor(Number(world?.achievementEnemyAntKills || 0) || 0)));
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(count * 10).toFixed(1)}%\uff08${count} / 10\u4f53\uff09`;
}
return "";
}
function lockedDescriptionText(definition) {
return PRE_UNLOCK_DESCRIPTION_IDS.has(definition.id) ? definition.description : "\uff1f\uff1f\uff1f";
}
function achievementListSignature() {
const unlocks = DEFINITIONS.map(definition => Number(state.unlocked[definition.id] || 0) || 0).join(",");
const rates = shared.connected
? DEFINITIONS.map(definition => `${percentageValue(definition.id).toFixed(1)}:${achieverCountValue(definition.id) ?? "-"}`).join(",")
: shared.loading ? "loading" : "offline";
const local = [
Math.min(3333, Math.max(0, Number(state.progress?.birthCount || 0) || 0)),
Math.floor(continuousPlayElapsedMs() / 1000),
Math.max(0, Math.floor(Number(global.__tarinaiFps || 0) || 0)),
Math.min(10, Math.max(0, Number(global.world?.achievementEnemyAntKills || 0) || 0)),
Math.min(50, Math.max(0, Number(state.progress?.firstAidHeals || 0) || 0)),
Math.min(CHAOS_FIGHT_TARGET, Math.max(0, Number(state.progress?.fightMochiFightCount || 0) || 0)),
Math.min(FERTILITY_BIRTH_TARGET, Math.max(0, Number(state.progress?.loveMochiBirthCount || 0) || 0)),
Math.min(DAILY_PLAY_TARGET, Math.max(0, Number(state.progress?.dailyPlayStreak || 0) || 0)),
Math.max(0, Number(state.progress?.dailyPlayLastDay || 0) || 0),
].join(":");
return `${unlocks}|${rates}|${local}`;
}
function createAchievementCard(definition) {
const unlockedAt = Number(state.unlocked[definition.id] || 0) || 0;
const rate = percentageValue(definition.id);
const card = document.createElement("article");
card.className = `achievement-entry ${unlockedAt ? "unlocked" : "locked"}`;
card.dataset.achievementId = definition.id;
card.style.setProperty("--achievement-rate", `${rate}%`);
const icon = document.createElement("div");
icon.className = "achievement-entry-icon";
icon.textContent = unlockedAt ? "\u2605" : "\u2606";
const copy = document.createElement("div");
copy.className = "achievement-entry-copy";
const heading = document.createElement("div");
heading.className = "achievement-entry-heading";
const title = document.createElement("strong");
title.textContent = definition.title;
const description = document.createElement("span");
description.className = "achievement-entry-description";
description.textContent = unlockedAt ? definition.description : lockedDescriptionText(definition);
heading.append(title, description);
const progressText = !unlockedAt && PRE_UNLOCK_DESCRIPTION_IDS.has(definition.id) ? localProgressText(definition.id) : "";
if (progressText) {
const progress = document.createElement("div");
progress.className = "achievement-entry-progress";
progress.textContent = progressText;
copy.append(heading, progress);
} else {
copy.append(heading);
}
const meta = document.createElement("div");
meta.className = "achievement-entry-meta";
const status = document.createElement("span");
status.textContent = unlockedAt ? `\u9054\u6210\u6e08\u307f ${formatDate(unlockedAt)}` : "";
const percentage = document.createElement("span");
percentage.className = "achievement-entry-percentage";
percentage.textContent = sharedAchievementText(definition.id);
meta.append(status, percentage);
copy.append(meta);
card.append(icon, copy);
card.setAttribute("aria-label", `${definition.title}\u3002${description.textContent}\u3002${sharedAchievementText(definition.id)}`);
return card;
}
function createAchievementGroup({ id, title, definitions, countText = "", open = false, emptyText = "" }) {
const group = document.createElement("details");
group.className = "achievement-group";
group.dataset.achievementGroup = id;
group.dataset.achievementCount = String(definitions.length);
group.open = Boolean(open);
const summary = document.createElement("summary");
summary.className = "achievement-group-summary";
const label = document.createElement("strong");
label.textContent = title;
const count = document.createElement("span");
count.className = "achievement-group-count";
count.textContent = countText;
summary.append(label, count);
const body = document.createElement("div");
body.className = "achievement-group-body";
if (definitions.length) body.append(...definitions.map(createAchievementCard));
else {
const empty = document.createElement("p");
empty.className = "achievement-group-empty";
empty.textContent = emptyText || "\u8a72\u5f53\u3059\u308b\u5b9f\u7e3e\u306f\u3042\u308a\u307e\u305b\u3093\u3002";
body.append(empty);
}
group.append(summary, body);
return group;
}
function render(options = {}) {
const count = unlockedCount();
if (dom.buttonCount) dom.buttonCount.textContent = `${count}/${DEFINITIONS.length}`;
if (dom.dialogCount) dom.dialogCount.textContent = `${count} / ${DEFINITIONS.length}`;
dom.button?.classList.toggle("complete", count === DEFINITIONS.length);
if (dom.sharedStatus) {
if (shared.loading) dom.sharedStatus.textContent = "\u5171\u6709\u9054\u6210\u7387\u3092\u78ba\u8a8d\u4e2d\u2026";
else if (shared.connected) dom.sharedStatus.textContent = `\u5171\u6709\u96c6\u8a08: ${shared.totalPlayers}\u4eba\u306e\u30d7\u30ec\u30a4\u30e4\u30fc`;
else dom.sharedStatus.textContent = "\u5171\u6709\u9054\u6210\u7387: \u672a\u63a5\u7d9a\uff08\u30ed\u30fc\u30ab\u30eb\u5b9f\u7e3e\u306f\u5229\u7528\u53ef\u80fd\uff09";
}
if (!dom.list || dom.dialog?.classList.contains("hidden")) return;
const signature = achievementListSignature();
if (!options.forceList && signature === lastListSignature) return;
lastListSignature = signature;
const previousGroups = Array.from(dom.list.children || []);
const hadPreviousGroups = previousGroups.some(group => group?.dataset?.achievementGroup);
const openGroupIds = new Set(previousGroups.filter(group => group?.open).map(group => group.dataset.achievementGroup));
const previousUnlockedCounts = new Map(previousGroups.map(group => [
group?.dataset?.achievementGroup,
Math.max(0, Number(group?.dataset?.achievementUnlockedCount || 0) || 0),
]));
const groups = [];
for (const category of ACHIEVEMENT_CATEGORIES) {
const categoryDefinitions = CATEGORY_DEFINITIONS.get(category.id) || [];
const unlockedDefinitions = categoryDefinitions
.filter(definition => state.unlocked[definition.id])
.sort((a, b) => Number(state.unlocked[b.id] || 0) - Number(state.unlocked[a.id] || 0));
const lockedDefinitions = categoryDefinitions.filter(definition => !state.unlocked[definition.id]);
const orderedDefinitions = [...unlockedDefinitions, ...lockedDefinitions];
const previousUnlockedCount = previousUnlockedCounts.get(category.id) || 0;
const group = createAchievementGroup({
id: category.id,
title: category.title,
definitions: orderedDefinitions,
countText: `${unlockedDefinitions.length} / ${categoryDefinitions.length}`,
open: hadPreviousGroups
? (openGroupIds.has(category.id) || unlockedDefinitions.length > previousUnlockedCount)
: category.id === "ecology",
});
group.dataset.achievementUnlockedCount = String(unlockedDefinitions.length);
groups.push(group);
}
dom.list.replaceChildren(...groups);
}
function renderIfDialogOpen() {
if (!dom.dialog?.classList.contains("hidden")) render();
}
function resetUnlockToastVisual() {
if (!dom.toast) return;
dom.toast.classList.remove("absorbing");
dom.toast.style.removeProperty("--achievement-toast-dx");
dom.toast.style.removeProperty("--achievement-toast-dy");
}
function finishUnlockToast() {
dom.toast?.classList.add("hidden");
resetUnlockToastVisual();
dom.button?.classList.remove("achievement-absorb-target");
toastActive = false;
global.setTimeout(runNextUnlockToast, 80);
}
function absorbUnlockToast() {
if (!dom.toast || dom.toast.classList.contains("hidden")) {
finishUnlockToast();
return;
}
const toastRect = dom.toast.getBoundingClientRect();
const targetRect = dom.button?.getBoundingClientRect?.();
if (!targetRect || !toastRect.width || !toastRect.height) {
finishUnlockToast();
return;
}
const dx = targetRect.left + targetRect.width / 2 - (toastRect.left + toastRect.width / 2);
const dy = targetRect.top + targetRect.height / 2 - (toastRect.top + toastRect.height / 2);
dom.toast.style.setProperty("--achievement-toast-dx", `${dx}px`);
dom.toast.style.setProperty("--achievement-toast-dy", `${dy}px`);
dom.toast.classList.add("absorbing");
dom.button?.classList.add("achievement-absorb-target");
clearTimeout(showUnlockToast.hideTimer);
showUnlockToast.hideTimer = global.setTimeout(finishUnlockToast, 760);
}
function runNextUnlockToast() {
if (toastActive || !toastQueue.length) return;
const definition = toastQueue.shift();
if (!dom.toast || !dom.toastTitle) {
global.showToast?.(`\u5b9f\u7e3e\u89e3\u9664: ${definition.title}`);
global.setTimeout(runNextUnlockToast, 80);
return;
}
toastActive = true;
clearTimeout(showUnlockToast.absorbTimer);
clearTimeout(showUnlockToast.hideTimer);
resetUnlockToastVisual();
dom.button?.classList.remove("achievement-absorb-target");
dom.toastTitle.textContent = definition.title;
dom.toast.classList.remove("hidden");
void dom.toast.offsetWidth;
showUnlockToast.absorbTimer = global.setTimeout(absorbUnlockToast, 2700);
}
function showUnlockToast(definition) {
if (!definition) return;
toastQueue.push(definition);
runNextUnlockToast();
}
function queuePending(id) {
if (!state.pending.includes(id)) state.pending.push(id);
}
async function request(payload = null) {
if (DEBUG_MODE || !API_URL || global.location?.protocol === "file:") throw new Error("api unavailable");
const controller = typeof AbortController === "function" ? new AbortController() : null;
const timeout = global.setTimeout(() => controller?.abort(), 3500);
try {
const options = payload ? {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: controller?.signal,
cache: "no-store",
} : { signal: controller?.signal, cache: "no-store" };
const url = payload ? API_URL : `${API_URL}?action=summary&playerId=${encodeURIComponent(playerId())}`;
const response = await fetch(url, options);
if (!response.ok) throw new Error(`api ${response.status}`);
const data = await response.json();
if (!data?.ok) throw new Error("api rejected");
return data;
} finally {
global.clearTimeout(timeout);
}
}
function applyShared(data) {
shared = {
connected: true,
loading: false,
totalPlayers: Math.max(0, Number(data?.totalPlayers || 0) || 0),
achievements: data?.achievements && typeof data.achievements === "object" ? data.achievements : {},
};
render();
}
async function refreshShared(options = {}) {
const serial = ++requestSerial;
shared.loading = true;
render();
try {
const data = await request(options.session ? {
action: "session",
playerId: playerId(),
gameVersion: GAME_VERSION,
} : null);
if (serial === requestSerial) applyShared(data);
return true;
} catch (_) {
if (serial === requestSerial) {
shared = { ...shared, connected: false, loading: false };
render();
}
return false;
}
}
function localUnlockSnapshot() {
const unlocked = {};
for (const definition of DEFINITIONS) {
const timestamp = Number(state.unlocked[definition.id] || 0) || 0;
if (timestamp > 0 && !state.debugUnlocked?.includes?.(definition.id)) unlocked[definition.id] = timestamp;
}
return unlocked;
}
async function performSyncPending(options = {}) {
let latest = null;
try {
if (state.resetPending) {
latest = await request({
action: "reset",
playerId: playerId(),
gameVersion: GAME_VERSION,
});
state.resetPending = false;
saveState();
}
const snapshot = localUnlockSnapshot();
const pendingIds = [...new Set(state.pending.filter(id => definitionById.has(id) && snapshot[id]))];
const unlocked = options.full
? snapshot
: Object.fromEntries(pendingIds.map(id => [id, snapshot[id]]));
const syncedIds = Object.keys(unlocked);
if (syncedIds.length || options.full) {
latest = await request({
action: "sync",
playerId: playerId(),
gameVersion: GAME_VERSION,
unlocked,
...(options.full ? { completionistEligible: allOtherAchievementsUnlocked() } : {}),
});
const syncedSet = new Set(syncedIds);
state.pending = state.pending.filter(id => !syncedSet.has(id));
saveState();
} else if (!latest) {
return refreshShared({ session: true });
}
if (latest) applyShared(latest);
return true;
} catch (_) {
shared = { ...shared, connected: false, loading: false };
render();
return false;
}
}
function syncPending(options = {}) {
const normalized = { full: Boolean(options.full) };
const run = () => performSyncPending(normalized);
syncQueue = syncQueue.then(run, run);
return syncQueue;
}
function schedulePendingSync() {
if (scheduledSyncTimer) global.clearTimeout(scheduledSyncTimer);
scheduledSyncTimer = global.setTimeout(() => {
scheduledSyncTimer = 0;
void syncPending();
}, SYNC_DEBOUNCE_MS);
}
function allOtherAchievementsUnlocked() {
return DEFINITIONS.every(definition => definition.id === COMPLETIONIST_ID || Boolean(state.unlocked[definition.id]));
}
function unlock(id, detail = {}) {
const definition = definitionById.get(String(id || ""));
if (!definition || state.unlocked[definition.id]) return false;
const now = Date.now();
const unlockedNow = [{ definition, detail }];
state.unlocked[definition.id] = now;
if (definition.id !== COMPLETIONIST_ID && !state.unlocked[COMPLETIONIST_ID] && allOtherAchievementsUnlocked()) {
const completionist = definitionById.get(COMPLETIONIST_ID);
state.unlocked[COMPLETIONIST_ID] = now;
unlockedNow.push({ definition: completionist, detail: { source: "all-achievements", trigger: definition.id } });
}
for (const entry of unlockedNow) {
if (DEBUG_MODE) {
if (!Array.isArray(state.debugUnlocked)) state.debugUnlocked = [];
if (!state.debugUnlocked.includes(entry.definition.id)) state.debugUnlocked.push(entry.definition.id);
} else {
queuePending(entry.definition.id);
}
}
saveState();
render({ forceList: true });
for (const entry of unlockedNow) {
showUnlockToast(entry.definition);
global.TarinaiEvents?.emit?.("achievement:unlocked", { achievement: entry.definition, detail: entry.detail });
}
if (!DEBUG_MODE) schedulePendingSync();
return true;
}
function runtimeTracker(world, now = 0) {
if (!world || (typeof world !== "object" && typeof world !== "function")) return null;
let tracker = runtimeByWorld.get(world);
if (!tracker) {
tracker = {
lastTime: Number(now) || 0,
birthTimes: [],
deathTimes: [],
};
runtimeByWorld.set(world, tracker);
}
const time = Number(now) || 0;
if (time + 0.001 < tracker.lastTime) {
tracker.birthTimes = [];
tracker.deathTimes = [];
}
tracker.lastTime = time;
return tracker;
}
function generationRunLength(values = []) {
const sorted = [...new Set((values || [])
.map(value => Math.max(1, Math.floor(Number(value) || 0)))
.filter(Number.isFinite))].sort((a, b) => a - b);
let best = 0;
let run = 0;
let previous = null;
for (const generation of sorted) {
run = previous !== null && generation === previous + 1 ? run + 1 : 1;
best = Math.max(best, run);
previous = generation;
}
return best;
}
function recordNaturalGeneration(world, tarinai, property, required) {
if (!world || !tarinai) return 0;
const generation = Math.max(1, Math.floor(Number(tarinai.generation || 1) || 1));
const current = Array.isArray(world[property]) ? world[property] : [];
world[property] = [...new Set([...current, generation])].sort((a, b) => a - b);
return generationRunLength(world[property]) >= required ? generation : 0;
}
function detailWorld(detail = {}) {
return detail.world || detail.item?.world || detail.tarinai?.world || detail.ant?.world || global.world || null;
}
function recordIntervention(detail = {}) {
const world = detailWorld(detail);
if (!world) return false;
world.achievementLastInterventionAt = Math.max(0, Number(world.time || 0) || 0);
return true;
}
function ensureProgress() {
if (!state.progress || typeof state.progress !== "object") state.progress = blankState().progress;
if (!Array.isArray(state.progress.linkTypes)) state.progress.linkTypes = [];
state.progress.placementCount = Math.max(0, Math.floor(Number(state.progress.placementCount || 0) || 0));
state.progress.signalActivated = Boolean(state.progress.signalActivated);
state.progress.antKills = Math.max(0, Math.floor(Number(state.progress.antKills || 0) || 0));
state.progress.birthCount = Math.max(0, Math.floor(Number(state.progress.birthCount || 0) || 0));
state.progress.quickDeleteCount = Math.max(0, Math.floor(Number(state.progress.quickDeleteCount || 0) || 0));
state.progress.undoCount = Math.max(0, Math.floor(Number(state.progress.undoCount || 0) || 0));
state.progress.redoCount = Math.max(0, Math.floor(Number(state.progress.redoCount || 0) || 0));
if (!Array.isArray(state.progress.medicineTypes)) state.progress.medicineTypes = [];
state.progress.medicineTypes = [...new Set(state.progress.medicineTypes.filter(type => MEDICINE_LEDGER_TYPES.includes(type)))];
state.progress.firstAidHeals = Math.max(0, Math.floor(Number(state.progress.firstAidHeals || 0) || 0));
state.progress.fightMochiFightCount = Math.max(0, Math.floor(Number(state.progress.fightMochiFightCount || 0) || 0));
state.progress.shotCount = Math.max(0, Math.floor(Number(state.progress.shotCount || 0) || 0));
state.progress.loveMochiBirthCount = Math.max(0, Math.floor(Number(state.progress.loveMochiBirthCount || 0) || 0));
state.progress.dailyPlayStreak = Math.min(DAILY_PLAY_TARGET, Math.max(0, Math.floor(Number(state.progress.dailyPlayStreak || 0) || 0)));
state.progress.dailyPlayLastDay = Math.max(0, Math.floor(Number(state.progress.dailyPlayLastDay || 0) || 0));
return state.progress;
}
function hexByte(value = 0) {
return Math.max(0, Math.min(255, Number(value) || 0)).toString(16).padStart(2, "0");
}
function unlockedIndexesFromMaskWords(lowWord = 0, highWord = 0) {
const low = Math.max(0, Math.min(0xffffffff, Math.floor(Number(lowWord) || 0)));
const highMax = (2 ** Math.max(0, DEFINITIONS.length - 32)) - 1;
const high = Math.max(0, Math.min(highMax, Math.floor(Number(highWord) || 0)));
const indexes = [];
for (let index = 0; index < DEFINITIONS.length; index += 1) {
const word = index < 32 ? low : high;
const bit = index < 32 ? index : index - 32;
if (Math.floor(word / (2 ** bit)) % 2) indexes.push(index);
}
return indexes;
}
function exportSpellState() {
let lowWord = 0;
let highWord = 0;
const timestamps = [];
for (let index = 0; index < DEFINITIONS.length; index += 1) {
const id = DEFINITIONS[index].id;
const timestamp = Number(state.unlocked[id] || 0) || 0;
if (timestamp <= 0 || state.debugUnlocked?.includes?.(id)) continue;
if (index < 32) lowWord += 2 ** index;
else highWord += 2 ** (index - 32);
timestamps.push(Math.max(1, Math.floor(timestamp / 60000)));
}
const baseTimestamp = timestamps.length ? Math.min(...timestamps) : 0;
const deltas = timestamps.map(timestamp => Math.max(0, timestamp - baseTimestamp));
const progress = ensureProgress();
let linkFlags = progress.signalActivated ? 8 : 0;
if (progress.linkTypes.includes("rope")) linkFlags |= 1;
if (progress.linkTypes.includes("rod")) linkFlags |= 2;
if (progress.linkTypes.includes("spring")) linkFlags |= 4;
let medicineMask = 0;
MEDICINE_LEDGER_TYPES.forEach((type, index) => {
if (progress.medicineTypes.includes(type)) medicineMask |= (1 << index);
});
return [
SPELL_STATE_VERSION,
lowWord >>> 0,
highWord >>> 0,
baseTimestamp,
deltas,
[
state.unlocked.placed_objects_100 ? 0 : Math.min(100, progress.placementCount),
state.unlocked.mechanized_industry ? 0 : linkFlags,
state.unlocked.ants_killed_100 ? 0 : Math.min(100, progress.antKills),
state.unlocked.great_mother_1000_births ? 0 : Math.min(3333, progress.birthCount),
state.unlocked.unplanned_city_30 ? 0 : Math.min(30, progress.quickDeleteCount),
state.unlocked.undo_20 ? 0 : Math.min(20, progress.undoCount),
state.unlocked.redo_20 ? 0 : Math.min(20, progress.redoCount),
state.unlocked.medicine_ledger_all ? 0 : medicineMask,
state.unlocked.town_doctor_50 ? 0 : Math.min(50, progress.firstAidHeals),
state.unlocked.chaos_seeker_666_fights ? 0 : Math.min(CHAOS_FIGHT_TARGET, progress.fightMochiFightCount),
state.unlocked.sniper_333_shots ? 0 : Math.min(SNIPER_SHOT_TARGET, progress.shotCount),
state.unlocked.fertility_seeker_721_love_births ? 0 : Math.min(FERTILITY_BIRTH_TARGET, progress.loveMochiBirthCount),
state.unlocked.daily_play_7_days ? 0 : Math.min(DAILY_PLAY_TARGET, progress.dailyPlayStreak),
state.unlocked.daily_play_7_days ? 0 : Math.min(131071, progress.dailyPlayLastDay),
],
];
}
function parseSpellState(payload = null) {
if (!Array.isArray(payload) || Number(payload[0]) !== SPELL_STATE_VERSION) return null;
const lowWord = Number(payload[1]);
const highWord = Number(payload[2]);
const highMax = (2 ** Math.max(0, DEFINITIONS.length - 32)) - 1;
if (!Number.isSafeInteger(lowWord) || lowWord < 0 || lowWord > 0xffffffff ||
!Number.isSafeInteger(highWord) || highWord < 0 || highWord > highMax) {
throw new Error("invalid achievement spell mask");
}
return {
unlockedIndexes: unlockedIndexesFromMaskWords(lowWord, highWord),
baseTimestamp: Math.max(0, Math.floor(Number(payload[3] || 0) || 0)),
deltas: Array.isArray(payload[4]) ? payload[4] : [],
packed: Array.isArray(payload[5]) ? payload[5] : [],
timestampUnitMs: 60000,
progressVersion: SPELL_STATE_VERSION,
};
}
function importSpellState(payload = null) {
const parsed = parseSpellState(payload);
if (!parsed) return { included: false, unlockedAdded: 0, progressChanged: false };
const { unlockedIndexes, baseTimestamp, deltas, packed, timestampUnitMs, progressVersion } = parsed;
if (deltas.length !== unlockedIndexes.length) throw new Error("invalid achievement spell timestamps");
const importedUnlocks = [];
let importedCompletionistAt = 0;
unlockedIndexes.forEach((definitionIndex, orderIndex) => {
const definition = DEFINITIONS[definitionIndex];
if (!definition) return;
const delta = Math.max(0, Math.floor(Number(deltas[orderIndex] || 0) || 0));
const timestamp = Math.max(1, (baseTimestamp + delta) * timestampUnitMs);
if (definition.id === COMPLETIONIST_ID) {
importedCompletionistAt = timestamp;
return;
}
importedUnlocks.push([definition.id, timestamp]);
});
let unlockedAdded = 0;
let changed = false;
for (const [id, timestamp] of importedUnlocks) {
const current = Number(state.unlocked[id] || 0) || 0;
if (!current) {
state.unlocked[id] = timestamp;
queuePending(id);
unlockedAdded += 1;
changed = true;
} else if (timestamp < current) {
state.unlocked[id] = timestamp;
changed = true;
}
}
const progress = ensureProgress();
const previousProgress = JSON.stringify(progress);
progress.placementCount = Math.max(progress.placementCount, Math.min(100, Math.max(0, Math.floor(Number(packed[0] || 0) || 0))));
const linkFlags = Math.max(0, Math.floor(Number(packed[1] || 0) || 0));
const importedLinks = [];
if (linkFlags & 1) importedLinks.push("rope");
if (linkFlags & 2) importedLinks.push("rod");
if (linkFlags & 4) importedLinks.push("spring");
progress.linkTypes = [...new Set([...(progress.linkTypes || []), ...importedLinks])];
progress.signalActivated = Boolean(progress.signalActivated || (linkFlags & 8));
progress.antKills = Math.max(progress.antKills, Math.min(100, Math.max(0, Math.floor(Number(packed[2] || 0) || 0))));
progress.birthCount = Math.max(progress.birthCount, Math.min(3333, Math.max(0, Math.floor(Number(packed[3] || 0) || 0))));
progress.quickDeleteCount = Math.max(progress.quickDeleteCount, Math.min(30, Math.max(0, Math.floor(Number(packed[4] || 0) || 0))));
progress.undoCount = Math.max(progress.undoCount, Math.min(20, Math.max(0, Math.floor(Number(packed[5] || 0) || 0))));
progress.redoCount = Math.max(progress.redoCount, Math.min(20, Math.max(0, Math.floor(Number(packed[6] || 0) || 0))));
const medicineMask = Math.max(0, Math.floor(Number(packed[7] || 0) || 0));
const importedMedicines = MEDICINE_LEDGER_TYPES.filter((_type, index) => medicineMask & (1 << index));
progress.medicineTypes = [...new Set([...(progress.medicineTypes || []), ...importedMedicines])];
progress.firstAidHeals = Math.max(progress.firstAidHeals, Math.min(50, Math.max(0, Math.floor(Number(packed[8] || 0) || 0))));
if (progressVersion === SPELL_STATE_VERSION) {
progress.fightMochiFightCount = Math.max(progress.fightMochiFightCount, Math.min(CHAOS_FIGHT_TARGET, Math.max(0, Math.floor(Number(packed[9] || 0) || 0))));
progress.shotCount = Math.max(progress.shotCount, Math.min(SNIPER_SHOT_TARGET, Math.max(0, Math.floor(Number(packed[10] || 0) || 0))));
progress.loveMochiBirthCount = Math.max(progress.loveMochiBirthCount, Math.min(FERTILITY_BIRTH_TARGET, Math.max(0, Math.floor(Number(packed[11] || 0) || 0))));
const importedDailyStreak = Math.min(DAILY_PLAY_TARGET, Math.max(0, Math.floor(Number(packed[12] || 0) || 0)));
const importedDailyLastDay = Math.min(131071, Math.max(0, Math.floor(Number(packed[13] || 0) || 0)));
if (importedDailyLastDay > progress.dailyPlayLastDay) {
progress.dailyPlayLastDay = importedDailyLastDay;
progress.dailyPlayStreak = importedDailyStreak;
} else if (importedDailyLastDay === progress.dailyPlayLastDay) {
progress.dailyPlayStreak = Math.max(progress.dailyPlayStreak, importedDailyStreak);
}
}
const progressChanged = previousProgress !== JSON.stringify(progress);
changed = changed || progressChanged;
if (!state.unlocked[COMPLETIONIST_ID] && allOtherAchievementsUnlocked()) {
const latestImportedAt = importedUnlocks.reduce((latest, entry) => Math.max(latest, Number(entry[1]) || 0), 0);
state.unlocked[COMPLETIONIST_ID] = importedCompletionistAt || latestImportedAt || Date.now();
queuePending(COMPLETIONIST_ID);
unlockedAdded += 1;
changed = true;
}
if (changed) {
saveState();
lastListSignature = "";
render({ forceList: true });
if (!DEBUG_MODE && unlockedAdded > 0) schedulePendingSync();
}
return { included: true, unlockedAdded, progressChanged };
}
function recordPlayerPlacement(detail = {}) {
const world = detailWorld(detail);
if (!world) return false;
recordIntervention({ ...detail, world });
const item = detail.item || null;
if (item) {
item._achievementPlayerPlaced = true;
item._achievementPlacedAt = Math.max(0, Number(world.time || 0) || 0);
}
let changed = evaluateCleanFreak(world, detail);
changed = evaluateMegalopolis(world, detail) || changed;
changed = evaluateSandbox(world, detail) || changed;
if (!state.unlocked.placed_objects_100) {
const progress = ensureProgress();
progress.placementCount = Math.min(100, progress.placementCount + 1);
saveState();
renderIfDialogOpen();
if (progress.placementCount >= 100) changed = unlock("placed_objects_100", { ...detail, world, count: progress.placementCount }) || changed;
}
return changed;
}
function recordPlayerDeletion(detail = {}) {
const world = detailWorld(detail);
const item = detail.item || null;
if (!world || !item) return false;
let changed = evaluateCleanFreak(world, detail);
changed = evaluateMegalopolis(world, detail) || changed;
if (!item._achievementPlayerPlaced) return changed;
const now = Math.max(0, Number(world.time || 0) || 0);
const placedAtRaw = item._achievementPlacedAt;
const placedAt = placedAtRaw == null ? NaN : Number(placedAtRaw);
if (!Number.isFinite(placedAt) || now < placedAt || now - placedAt > 30.0001) return changed;
const progress = ensureProgress();
if (!state.unlocked.unplanned_city_30) {
progress.quickDeleteCount = Math.min(30, progress.quickDeleteCount + 1);
saveState();
renderIfDialogOpen();
if (progress.quickDeleteCount >= 30) changed = unlock("unplanned_city_30", { ...detail, world, count: progress.quickDeleteCount }) || changed;
}
return changed;
}
function recordHistoryAction(kind, detail = {}) {
const action = String(kind || detail.kind || "");
if (action !== "undo" && action !== "redo") return false;
const progress = ensureProgress();
const key = action === "undo" ? "undoCount" : "redoCount";
const achievementId = action === "undo" ? "undo_20" : "redo_20";
progress[key] = Math.min(20, progress[key] + 1);
saveState();
renderIfDialogOpen();
let changed = false;
if (action === "undo" && Math.max(0, Number(detail.deadRestored || 0) || 0) >= 10) {
changed = unlock("undo_mass_revival", { ...detail, count: Math.max(0, Number(detail.deadRestored || 0) || 0) }) || changed;
}
if (progress[key] >= 20) changed = unlock(achievementId, { ...detail, count: progress[key] }) || changed;
return changed;
}
function evaluateMechanizedIndustry(detail = {}) {
const progress = ensureProgress();
const hasAllLinks = ["rope", "rod", "spring"].every(required => progress.linkTypes.includes(required));
if (progress.signalActivated && hasAllLinks) return unlock("mechanized_industry", detail);
return false;
}
function recordLinkPlaced(type, detail = {}) {
const linkType = String(type || detail.type || detail.item?.type || "");
const placementLinkTypes = ["rope", "rod", "spring", "wire", "insulated_wire"];
if (!placementLinkTypes.includes(linkType)) return false;
recordPlayerPlacement(detail);
if (state.unlocked.mechanized_industry || !["rope", "rod", "spring"].includes(linkType)) return false;
const progress = ensureProgress();
progress.linkTypes = [...new Set([...(progress.linkTypes || []), linkType])];
saveState();
renderIfDialogOpen();
return evaluateMechanizedIndustry({ ...detail, type: linkType });
}
function recordSignalActivation(detail = {}) {
if (state.unlocked.mechanized_industry) return false;
const progress = ensureProgress();
if (!progress.signalActivated) {
progress.signalActivated = true;
saveState();
renderIfDialogOpen();
}
return evaluateMechanizedIndustry(detail);
}
function recordAntPopulation(worldRef = global.world) {
if (!worldRef || state.unlocked.ants_alive_25) return false;
const aliveAnts = (worldRef.ants || []).reduce((count, ant) => count + (ant && !ant.dead ? 1 : 0), 0);
if (aliveAnts >= 25) return unlock("ants_alive_25", { world: worldRef, count: aliveAnts });
return false;
}
function evaluateAntNestWithoutTarinai(worldRef = global.world, detail = {}) {
if (!worldRef || state.unlocked.ant_nest_without_tarinai) return false;
const hasNest = (worldRef.items || []).some(item => item && !item.dead && item.type === "ant_nest");
if (!hasNest) return false;
const aliveTarinai = (worldRef.tarinai || []).some(tarinai => tarinai && !tarinai.dead);
if (aliveTarinai) return false;
return unlock("ant_nest_without_tarinai", { ...detail, world: worldRef });
}
function recordStickyBombPass(item = null, detail = {}) {
if (!item || item.dead || item.type !== "sticky_bomb" || state.unlocked.sticky_bomb_15_passes) return false;
item.stickyBombPassCount = Math.max(0, Math.floor(Number(item.stickyBombPassCount || 0) || 0)) + 1;
if (item.stickyBombPassCount < 15) return false;
return unlock("sticky_bomb_15_passes", { ...detail, item, count: item.stickyBombPassCount });
}
function recordCircuitBoardSignalTransfer(sourceBoard = null, targetBoard = null, wire = null, detail = {}) {
if (state.unlocked.information_industry) return false;
if (!sourceBoard || !targetBoard || sourceBoard === targetBoard) return false;
if (sourceBoard.type !== "circuit_board" || targetBoard.type !== "circuit_board") return false;
if (!wire || !["wire", "insulated_wire"].includes(wire.type) || !wire.signalActive) return false;
return unlock("information_industry", { ...detail, sourceBoard, targetBoard, wire });
}
function recordWirePowerState(wire = null, active = false) {
if (!wire || (typeof wire !== "object" && typeof wire !== "function")) return false;
if (!active) return false;
if (!wireShockTrackers.has(wire)) wireShockTrackers.set(wire, { targets: new WeakSet(), count: 0 });
return true;
}
function recordWireShock(wire = null, target = null, detail = {}) {
if (state.unlocked.wire_shock_7_tarinai || !wire || !target || wire.type !== "wire" || !wire.signalActive) return false;
const world = detail.world || wire.world || target.world || global.world || null;
if (!world || !(world.tarinai || []).includes(target) || target.dead) return false;
recordWirePowerState(wire, true);
const tracker = wireShockTrackers.get(wire);
if (!tracker || tracker.targets.has(target)) return false;
tracker.targets.add(target);
tracker.count += 1;
if (tracker.count < WIRE_SHOCK_TARGET) return false;
return unlock("wire_shock_7_tarinai", { ...detail, world, wire, target, count: tracker.count });
}
function recordAntKilled(detail = {}) {
let changed = false;
const progress = ensureProgress();
if (!state.unlocked.ants_killed_100) {
progress.antKills = Math.min(100, progress.antKills + 1);
saveState();
renderIfDialogOpen();
if (progress.antKills >= 100) changed = unlock("ants_killed_100", detail) || changed;
}
const world = detailWorld(detail);
const ant = detail.ant || null;
const now = Math.max(0, Number(world?.time || 0) || 0);
const aliveCount = (world?.tarinai || []).reduce((count, candidate) => count + (candidate && !candidate.dead ? 1 : 0), 0);
if (world && aliveCount < 30) world.achievementEnemyAntKills = 0;
if (!state.unlocked.enemy_enemy_friend && world && ant?._achievementLastDamageSource?.danger && aliveCount >= 30) {
world.achievementEnemyAntKills = Math.min(10, Math.max(0, Math.floor(Number(world.achievementEnemyAntKills || 0) || 0)) + 1);
renderIfDialogOpen();
if (world.achievementEnemyAntKills >= 10) changed = unlock("enemy_enemy_friend", { ...detail, world, count: world.achievementEnemyAntKills }) || changed;
}
return changed;
}
function dangerItemSource(source = null) {
if (!source) return null;
const configured = global.TarinaiItemToolMetadata?.TOOL_CATEGORIES
?.find?.(category => category?.id === "hazard")?.toolIds;
const hazardTypes = new Set(Array.isArray(configured)
? configured
: ["stone", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "pushpin", "oshibyo", "shoot", "ant_nest"]);
if (source.type && hazardTypes.has(String(source.type))) return source;
const inherited = source._achievementDangerSource || null;
return inherited?.type && hazardTypes.has(String(inherited.type)) ? inherited : null;
}
function recordDamageSource(tarinai, source = null, detail = {}) {
if (!tarinai) return false;
const world = detail.world || tarinai.world || global.world || null;
const now = Math.max(0, Number(world?.time || 0) || 0);
const dangerSource = dangerItemSource(source);
// This achievement tracks collateral damage caused by danger-category tools,
// not ordinary combat, hunger, disease, or ant attacks.
if (world && dangerSource) {
world.achievementLastTarinaiDamageAt = now;
world.achievementEnemyAntKills = 0;
}
tarinai._achievementLastDamageSource = dangerSource ? {
danger: true,
at: now,
itemId: String(dangerSource.id || ""),
itemType: String(dangerSource.type || ""),
} : { danger: false, at: now, itemId: "", itemType: "" };
return Boolean(dangerSource);
}
function recordAntDamageSource(ant, source = null, detail = {}) {
if (!ant) return false;
const world = detail.world || ant.world || global.world || null;
const now = Math.max(0, Number(world?.time || 0) || 0);
const dangerSource = dangerItemSource(source);
ant._achievementLastDamageSource = dangerSource ? {
danger: true,
at: now,
itemId: String(dangerSource.id || ""),
itemType: String(dangerSource.type || ""),
} : { danger: false, at: now, itemId: "", itemType: "" };
return Boolean(dangerSource);
}
function fightPairTracker(world) {
if (!world || (typeof world !== "object" && typeof world !== "function")) return null;
let tracker = dangerFightDeathsByWorld.get(world);
if (!tracker) {
tracker = new Map();
dangerFightDeathsByWorld.set(world, tracker);
}
return tracker;
}
function recordFightPairDangerDeath(detail = {}) {
if (state.unlocked.fight_pair_danger_kill) return false;
const tarinai = detail.tarinai || null;
const world = detail.world || tarinai?.world || null;
if (!tarinai || !world) return false;
const now = Math.max(0, Number(world.time || 0) || 0);
const source = tarinai._achievementLastDamageSource || null;
const lethalDanger = Boolean(detail.fromDamage || detail.achievementDangerKill);
if (!lethalDanger || !source?.danger || Math.abs(now - Number(source.at || 0)) > 0.05) return false;
const tracker = fightPairTracker(world);
if (!tracker) return false;
for (const [key, pending] of [...tracker.entries()]) {
if (!pending) {
tracker.delete(key);
continue;
}
if (pending.firstId !== tarinai.id && pending.otherId === tarinai.id) {
tracker.delete(key);
return unlock("fight_pair_danger_kill", { ...detail, pairKey: key, first: pending, secondSource: source });
}
}
const targets = [...new Set([...(Array.isArray(tarinai.fightTargetIds) ? tarinai.fightTargetIds : []), tarinai.fightTargetId].filter(Boolean))];
for (const targetId of targets) {
const opponent = (world.tarinai || []).find(candidate => candidate && candidate.id === targetId) || null;
if (!opponent || opponent === tarinai) continue;
const activePair = Boolean(world.isAlreadyFightingPair?.(tarinai, opponent))
|| Boolean((tarinai.fightTimer || 0) > 0.04 && (opponent.fightTimer || 0) > 0.04);
if (!activePair) continue;
const key = world.fightPairKey?.(tarinai, opponent)
|| [String(tarinai.id || ""), String(opponent.id || "")].sort().join(":");
if (!key) continue;
tracker.set(key, { firstId: tarinai.id, otherId: opponent.id, at: now, source });
return false;
}
return false;
}
function recordSaveSlotsFilled(detail = {}) {
if (state.unlocked.secret_collection_9_slots) return false;
const storage = global.TarinaiSaveStorage;
const slotCount = Math.max(0, Math.floor(Number(storage?.SLOT_COUNT || 9) || 9));
let filled = Number(detail.filled);
if (!Number.isFinite(filled)) {
filled = 0;
for (let slot = 1; slot <= slotCount; slot += 1) {
try { if (storage?.readSlot?.(slot)?.hash) filled += 1; } catch (_) {}
}
}
if (slotCount >= 9 && filled >= 9) return unlock("secret_collection_9_slots", { ...detail, filled, slotCount });
return false;
}
function recordEmptyClick(detail = {}) {
if (state.unlocked.pause_spam_4_in_1_second) return false;
const now = Number(detail.now);
const timestamp = Number.isFinite(now) ? now : Date.now();
emptyClickTimes = emptyClickTimes.filter(value => timestamp - value <= 1000 && timestamp >= value);
emptyClickTimes.push(timestamp);
if (emptyClickTimes.length >= 4) {
emptyClickTimes = [];
return unlock("pause_spam_4_in_1_second", { ...detail, count: 4, windowMs: 1000 });
}
return false;
}
function recordGroundChange(detail = {}) {
const world = detailWorld(detail);
const previous = String(detail.previous || "");
const next = String(detail.next || world?.groundType || "");
if (!world || detail.silent || !previous || !next || previous === next) return false;
let changed = false;
if (!state.unlocked.park_ground_changed && String(world.fieldType || "") === "park") {
changed = unlock("park_ground_changed", { ...detail, world, previous, next }) || changed;
}
if (!state.unlocked.ground_change_4_in_1_second) {
const now = Number(detail.now);
const timestamp = Number.isFinite(now) ? now : Date.now();
groundChangeTimes = groundChangeTimes.filter(value => timestamp - value <= 1000 && timestamp >= value);
groundChangeTimes.push(timestamp);
if (groundChangeTimes.length >= 4) {
groundChangeTimes = [];
changed = unlock("ground_change_4_in_1_second", { ...detail, world, count: 4, windowMs: 1000 }) || changed;
}
}
return changed;
}
function evaluateSandbox(world = global.world, detail = {}) {
if (!world || state.unlocked.sandbox_five_toilets) return false;
const count = (world.items || []).reduce((sum, item) => sum + (item && !item.dead && item.type === "toilet" ? 1 : 0), 0);
if (count < 5) return false;
return unlock("sandbox_five_toilets", { ...detail, world, count });
}
function recordLowFps(detail = {}) {
if (state.unlocked.low_fps_single_digit) return false;
const fps = Number.isFinite(Number(detail.fps)) ? Number(detail.fps) : Number(global.__tarinaiFps);
if (!Number.isFinite(fps) || fps <= 0 || fps >= 10) return false;
return unlock("low_fps_single_digit", { ...detail, fps });
}
function localDayNumber(value = Date.now()) {
const date = new Date(Number(value));
if (!Number.isFinite(date.getTime())) return 0;
return Math.floor(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / DAY_MS);
}
function recordDailyPlay(detail = {}) {
if (state.unlocked.daily_play_7_days) return false;
const today = localDayNumber(Number.isFinite(Number(detail.now)) ? Number(detail.now) : Date.now());
if (!today) return false;
const progress = ensureProgress();
const previousDay = progress.dailyPlayLastDay;
if (today < previousDay || today === previousDay) return false;
progress.dailyPlayStreak = today === previousDay + 1
? Math.min(DAILY_PLAY_TARGET, progress.dailyPlayStreak + 1)
: 1;
progress.dailyPlayLastDay = today;
saveState();
renderIfDialogOpen();
if (progress.dailyPlayStreak >= DAILY_PLAY_TARGET) {
return unlock("daily_play_7_days", { ...detail, day: today, streak: progress.dailyPlayStreak });
}
return false;
}
function recordContinuousPlay(detail = {}) {
if (state.unlocked.continuous_play_1_hour && state.unlocked.continuous_play_24_hours) return false;
const explicitElapsed = Number(detail.elapsedMs);
let elapsedMs;
if (Number.isFinite(explicitElapsed)) {
elapsedMs = Math.max(0, explicitElapsed);
} else {
const timestamp = Number.isFinite(Number(detail.now)) ? Number(detail.now) : Date.now();
if (!continuousPlayStartedAt || timestamp < continuousPlayStartedAt) continuousPlayStartedAt = timestamp;
continuousPlayLastHeartbeatAt = timestamp;
elapsedMs = Math.max(0, timestamp - continuousPlayStartedAt);
}
let changed = false;
if (!state.unlocked.continuous_play_1_hour && elapsedMs >= CONTINUOUS_PLAY_TARGET_MS) {
changed = unlock("continuous_play_1_hour", { ...detail, elapsedMs }) || changed;
}
if (!state.unlocked.continuous_play_24_hours && elapsedMs >= CONTINUOUS_PLAY_24H_TARGET_MS) {
changed = unlock("continuous_play_24_hours", { ...detail, elapsedMs }) || changed;
}
return changed;
}
function recordBelowAbsoluteZeroItem(worldRef = global.world, detail = {}) {
if (state.unlocked.below_absolute_zero_item || !worldRef) return false;
for (const item of worldRef.items || []) {
if (!item || item.dead) continue;
const temperature = Number(worldRef.temperatureAt?.(item.x, item.y, null));
if (!Number.isFinite(temperature) || temperature >= ABSOLUTE_ZERO_C) continue;
return unlock("below_absolute_zero_item", { ...detail, world: worldRef, item, temperature });
}
return false;
}
function recordRobotClean(detail = {}) {
if (state.unlocked.robot_cleaner_100) return false;
const world = detailWorld(detail);
const type = String(detail.type || detail.target?.type || "");
if (!world || !["zunchi", "ant_corpse", "trace", "splat"].includes(type)) return false;
world.achievementRobotCleanCount = Math.min(ROBOT_CLEAN_TARGET, Math.max(0, Math.floor(Number(world.achievementRobotCleanCount || 0) || 0)) + 1);
renderIfDialogOpen();
if (world.achievementRobotCleanCount >= ROBOT_CLEAN_TARGET) return unlock("robot_cleaner_100", { ...detail, world, count: world.achievementRobotCleanCount });
return false;
}
function robotCleanerIsRunning(robot) {
if (!robot || robot.dead || robot.type !== "robot_cleaner") return false;
return !robot._signalDriven || Boolean(robot._signalOn);
}
function evaluateCleanFreak(world = global.world, detail = {}) {
if (!world || state.unlocked.clean_freak_robot_only) return false;
const liveItems = (world.items || []).filter(item => item && !item.dead);
if (!liveItems.length || liveItems.some(item => item.type !== "robot_cleaner")) return false;
if ((world.tarinai || []).some(tarinai => tarinai && !tarinai.dead)) return false;
if ((world.ants || []).some(ant => ant && !ant.dead)) return false;
const qualifies = liveItems.some(robot => {
const mask = Number(global.TarinaiItemDynamicToolSystem?.robotCleanerMask?.(robot) ?? robot.robotCleanerMask ?? 0) | 0;
return (mask & ROBOT_ALL_TARGET_MASK) === ROBOT_ALL_TARGET_MASK
&& Boolean(global.TarinaiItemDynamicToolSystem?.robotCleanerHighSpeed?.(robot) ?? robot.robotCleanerHighSpeed)
&& robotCleanerIsRunning(robot);
});
if (!qualifies) return false;
return unlock("clean_freak_robot_only", { ...detail, world, robotCount: liveItems.length });
}
function evaluateStatusEffectAchievements(tarinai, detail = {}) {
if (!tarinai || tarinai.dead) return false;
let changed = false;
if (!state.unlocked.king_full_satisfaction
&& tarinai.isTarinaiChampion
&& tarinai.powerItemMode === "protein"
&& tarinai.sizeItemMode === "giant_drug") {
changed = unlock("king_full_satisfaction", { ...detail, tarinai, world: detail.world || tarinai.world || null }) || changed;
}
if (!state.unlocked.slave_zero_satisfaction
&& tarinai.isZunchiSlave
&& tarinai.powerItemMode === "niteropu"
&& tarinai.sizeItemMode === "dwarf_drug") {
changed = unlock("slave_zero_satisfaction", { ...detail, tarinai, world: detail.world || tarinai.world || null }) || changed;
}
return changed;
}
function recordFirstAidRecovery(detail = {}) {
if (state.unlocked.town_doctor_50) return false;
const recovered = Number(detail.recovered ?? detail.amount ?? 0);
if (!Number.isFinite(recovered) || recovered <= 0.0001) return false;
const progress = ensureProgress();
progress.firstAidHeals = Math.min(50, progress.firstAidHeals + 1);
saveState();
renderIfDialogOpen();
if (progress.firstAidHeals >= 50) return unlock("town_doctor_50", { ...detail, count: progress.firstAidHeals, recovered });
return false;
}
function recordPlushiePokeFling(detail = {}) {
return unlock("poke_plushie_fling", detail);
}
function recordConsumableUse(detail = {}) {
const type = String(detail.type || detail.item?.type || "");
const tarinai = detail.tarinai || null;
const world = detailWorld(detail);
let changed = false;
if (type === "fight_mochi" && tarinai && world) {
const serial = Math.max(1, Math.floor(Number(world.achievementFightMochiSerial || 0) || 0) + 1);
world.achievementFightMochiSerial = serial;
tarinai._achievementFightMochiAt = Math.max(0, Number(world.time || 0) || 0);
tarinai._achievementFightMochiSerial = serial;
tarinai._achievementFightMochiWorld = world;
}
if (MEDICINE_LEDGER_TYPES.includes(type) && !state.unlocked.medicine_ledger_all) {
const progress = ensureProgress();
if (!progress.medicineTypes.includes(type)) {
progress.medicineTypes = [...progress.medicineTypes, type];
saveState();
renderIfDialogOpen();
}
if (MEDICINE_LEDGER_TYPES.every(required => progress.medicineTypes.includes(required))) {
changed = unlock("medicine_ledger_all", { ...detail, count: progress.medicineTypes.length }) || changed;
}
}
changed = evaluateStatusEffectAchievements(tarinai, { ...detail, world }) || changed;
return changed;
}
function recordDirectCare(detail = {}) {
const world = detailWorld(detail);
const tarinai = detail.tarinai || null;
const type = String(detail.type || detail.item?.type || "");
if (!world || !tarinai) return false;
const foodGift = Boolean(detail.foodGift);
const treatment = Boolean(detail.treatment || DIRECT_TREATMENT_TYPES.has(type));
if (foodGift) {
tarinai._achievementDirectFeedCount = Math.max(0, Math.floor(Number(tarinai._achievementDirectFeedCount || 0) || 0)) + 1;
world.achievementSelfSufficientStartAt = -1;
world.achievementLastDirectFeedAt = Math.max(0, Number(world.time || 0) || 0);
}
if (treatment) tarinai._achievementDirectTreatmentCount = Math.max(0, Math.floor(Number(tarinai._achievementDirectTreatmentCount || 0) || 0)) + 1;
recordConsumableUse({ ...detail, world, tarinai, type });
if (tarinai._achievementDirectFeedCount >= 10 && tarinai._achievementDirectTreatmentCount >= 3) {
return unlock("overprotective", { ...detail, world, tarinai, feedCount: tarinai._achievementDirectFeedCount, treatmentCount: tarinai._achievementDirectTreatmentCount });
}
return false;
}
function recordFightStarted(detail = {}) {
const world = detailWorld(detail);
if (!world) return false;
let changed = false;
const fightMochiInfluenced = [detail.a, detail.b].some(tarinai => Boolean(tarinai) && (
(Number(tarinai.fightMochiTimer || 0) > 0.04) || Boolean(tarinai.hasFightMochiEffect?.())
));
if (!state.unlocked.chaos_seeker_666_fights && fightMochiInfluenced) {
const progress = ensureProgress();
progress.fightMochiFightCount = Math.min(CHAOS_FIGHT_TARGET, progress.fightMochiFightCount + 1);
if (progress.fightMochiFightCount >= CHAOS_FIGHT_TARGET) {
changed = unlock("chaos_seeker_666_fights", { ...detail, world, count: progress.fightMochiFightCount, fightMochiInfluenced: true }) || changed;
} else {
saveState();
renderIfDialogOpen();
}
}
const now = Math.max(0, Number(world.time || 0) || 0);
const candidates = [detail.a, detail.b].filter(Boolean);
for (const tarinai of candidates) {
const givenAtRaw = tarinai._achievementFightMochiAt;
const givenAt = givenAtRaw == null ? NaN : Number(givenAtRaw);
const serial = Math.floor(Number(tarinai._achievementFightMochiSerial || 0) || 0);
const validCurrentSessionMochi = serial > 0 && tarinai._achievementFightMochiWorld === world;
if (validCurrentSessionMochi && Number.isFinite(givenAt) && now >= givenAt && now - givenAt <= 30.0001) {
changed = unlock("fuel_to_fire", { ...detail, world, tarinai, elapsed: now - givenAt, serial }) || changed;
break;
}
}
return changed;
}
function recordShotFired(detail = {}) {
if (state.unlocked.sniper_333_shots) return false;
const progress = ensureProgress();
progress.shotCount = Math.min(SNIPER_SHOT_TARGET, progress.shotCount + 1);
if (progress.shotCount >= SNIPER_SHOT_TARGET) return unlock("sniper_333_shots", { ...detail, count: progress.shotCount });
saveState();
renderIfDialogOpen();
return false;
}
function recordLoveMochiBirth(detail = {}) {
if (state.unlocked.fertility_seeker_721_love_births || !detail.loveMochiInfluenced) return false;
const progress = ensureProgress();
progress.loveMochiBirthCount = Math.min(FERTILITY_BIRTH_TARGET, progress.loveMochiBirthCount + 1);
if (progress.loveMochiBirthCount >= FERTILITY_BIRTH_TARGET) {
return unlock("fertility_seeker_721_love_births", { ...detail, count: progress.loveMochiBirthCount });
}
saveState();
renderIfDialogOpen();
return false;
}
function evaluateMegalopolis(world = global.world, detail = {}) {
if (!world || state.unlocked.megalopolis) return false;
const aliveCount = (world.tarinai || []).reduce((count, tarinai) => count + (tarinai && !tarinai.dead ? 1 : 0), 0);
if (aliveCount < 100) return false;
let duplicatorCount = 0;
let shelterCount = 0;
for (const item of world.items || []) {
if (!item || item.dead) continue;
if (item.type === "duplicator") duplicatorCount += 1;
else if (item.type === "grass_bed" || item.type === "nest_box" || item.type === "pipe") shelterCount += 1;
}
if (duplicatorCount < 10 || shelterCount < 15) return false;
return unlock("megalopolis", { ...detail, world, aliveCount, duplicatorCount, shelterCount });
}
function resetWorldProgress(world = global.world) {
if (!world) return false;
world.achievementDirectFeedCount = 0;
world.achievementRobotCleanCount = 0;
world.achievementEnemyAntKills = 0;
world.achievementLastTarinaiDamageAt = -Infinity;
world.achievementSelfSufficientStartAt = -1;
world.achievementLastDirectFeedAt = -Infinity;
world.achievementNoDeathStartAt = -1;
world.achievementHappyStartAt = -1;
world.achievementFightMochiSerial = 0;
for (const tarinai of world.tarinai || []) {
if (!tarinai) continue;
tarinai._achievementFightMochiAt = null;
tarinai._achievementFightMochiSerial = 0;
tarinai._achievementFightMochiWorld = null;
}
world.achievementNaturalSlaveGenerations = [];
world.achievementNaturalKingGenerations = [];
return true;
}
function recordHeldTarinai(world = global.world, now = Date.now()) {
if (!world || state.unlocked.held_30_seconds) return false;
if (document.hidden) {
for (const tarinai of world.tarinai || []) {
if (tarinai) tarinai._achievementHeldStartedAt = null;
}
return false;
}
const timestamp = Number(now);
if (!Number.isFinite(timestamp)) return false;
for (const tarinai of world.tarinai || []) {
if (!tarinai || tarinai.dead) continue;
if (tarinai.playerHeld || tarinai._heldByPlayer) {
if (tarinai._achievementHeldStartedAt == null || !Number.isFinite(Number(tarinai._achievementHeldStartedAt))) tarinai._achievementHeldStartedAt = timestamp;
if (timestamp >= tarinai._achievementHeldStartedAt && timestamp - tarinai._achievementHeldStartedAt >= HELD_TARGET_MS) {
return unlock("held_30_seconds", { world, tarinai, elapsedMs: timestamp - tarinai._achievementHeldStartedAt });
}
} else {
tarinai._achievementHeldStartedAt = null;
}
}
return false;
}
function recordBirth(detail = {}) {
unlock("first_birth", detail);
const child = detail.child || null;
const world = detail.world || child?.world || null;
const generation = Math.max(1, Math.floor(Number(child?.generation || world?.maxGeneration || 1) || 1));
if (generation >= GENERATION_TARGET) unlock("eternal_history_generation_10", { ...detail, world, child, generation });
evaluateMegalopolis(world, detail);
const progress = ensureProgress();
if (!state.unlocked.great_mother_1000_births) {
progress.birthCount = Math.min(3333, progress.birthCount + 1);
saveState();
renderIfDialogOpen();
if (progress.birthCount >= 3333) unlock("great_mother_1000_births", { ...detail, count: progress.birthCount });
}
if (state.unlocked.birth_50_in_60_seconds) return;
const now = Number(world?.time || 0) || 0;
const tracker = runtimeTracker(world, now);
if (!tracker) return;
tracker.birthTimes.push(now);
tracker.birthTimes = tracker.birthTimes.filter(time => time >= now - 60.0001);
if (tracker.birthTimes.length >= 30) unlock("birth_50_in_60_seconds", detail);
}
function recordDeath(detail = {}) {
recordFightPairDangerDeath(detail);
const reason = String(detail.reason || detail.tarinai?.deathReason || "");
const lifespanDeath = /\u5bff\u547d|\u5929\u5bff/.test(reason);
const world = detail.world || detail.tarinai?.world || null;
if (world) {
evaluateMegalopolis(world, detail);
if (!lifespanDeath) {
const aliveAfterDeath = (world.tarinai || []).reduce((count, candidate) => count + (candidate && !candidate.dead ? 1 : 0), 0);
world.achievementNoDeathStartAt = aliveAfterDeath >= 25 ? Math.max(0, Number(world.time || 0) || 0) : -1;
}
}
const now = Number(world?.time || 0) || 0;
const tracker = runtimeTracker(world, now);
if (tracker) {
tracker.deathTimes.push(now);
tracker.deathTimes = tracker.deathTimes.filter(time => time >= now - 10.0001);
if (tracker.deathTimes.length >= 50) unlock("death_50_in_10_seconds", detail);
}
const tarinai = detail.tarinai || null;
if (reason.includes("\u5929\u5bff\u3092\u5168\u3046\u3057\u305f")) {
unlock("lifespan_completed", detail);
if (tarinai?.lifeItemMode === "mercury" || Number(tarinai?.mercuryLifeMultiplier || 0) > 1) unlock("mercury_lifespan", detail);
}
const attackerId = String(tarinai?._achievementLastFightAttackerId || "");
const attackerAt = Number(tarinai?._achievementLastFightDamageAt);
const attacker = attackerId ? (world?.tarinai || []).find(candidate => candidate && String(candidate.id || "") === attackerId) : null;
if (tarinai?.isTarinaiChampion && attacker?.isZunchiSlave && reason.includes("\u55a7\u5629") && Number.isFinite(attackerAt) && Math.abs(now - attackerAt) <= 0.1) {
unlock("revolution", { ...detail, attacker });
}
const laxativeActive = Boolean(tarinai?._achievementLaxativeActiveThisFrame || tarinai?.hasItemEffect?.("laxative") || Number(tarinai?.itemEffectTimers?.laxative || 0) > 0.04);
if (laxativeActive && reason.includes("\u98e2\u9913")) unlock("laxative_starvation", detail);
}
function recordNaturalStatus(kind, detail = {}) {
const tarinai = detail.tarinai || null;
const world = detail.world || tarinai?.world || null;
if (kind === "slave") {
unlock("natural_zunchi_slave", detail);
if (recordNaturalGeneration(world, tarinai, "achievementNaturalSlaveGenerations", 5)) {
unlock("natural_zunchi_slave_5_generations", detail);
}
evaluateStatusEffectAchievements(tarinai, detail);
return;
}
if (kind === "king") {
unlock("natural_tarinai_king", detail);
if (recordNaturalGeneration(world, tarinai, "achievementNaturalKingGenerations", 3)) {
unlock("natural_tarinai_king_3_generations", detail);
}
evaluateStatusEffectAchievements(tarinai, detail);
}
}
function recordDirectFeed(detail = {}) {
const world = detailWorld(detail);
if (!world) return false;
recordDirectCare({ ...detail, world, foodGift: true });
if (state.unlocked.direct_feed_33) return false;
world.achievementDirectFeedCount = Math.min(33, Math.max(0, Math.floor(Number(world.achievementDirectFeedCount || 0) || 0)) + 1);
renderIfDialogOpen();
if (world.achievementDirectFeedCount >= 33) return unlock("direct_feed_33", { ...detail, world, count: world.achievementDirectFeedCount });
return false;
}
function recordIgnition(detail = {}) {
return unlock("ignite_during_birth_ritual", detail);
}
function evaluateWorld(worldRef, mood = null) {
if (!worldRef) return false;
const maxGeneration = Math.max(1, Math.floor(Number(worldRef.maxGeneration || 1) || 1));
if (!state.unlocked.eternal_history_generation_10 && maxGeneration >= GENERATION_TARGET) {
unlock("eternal_history_generation_10", { world: worldRef, generation: maxGeneration });
}
const needsAliveScan = !state.unlocked.all_non_sleep_diseased_25
|| !state.unlocked.colony_happy
|| !state.unlocked.minimalist_happy
|| !state.unlocked.self_sufficient
|| !state.unlocked.rain_shelter_all
|| !state.unlocked.sauna_cold_plunge
|| !state.unlocked.idle_observer_5_minutes
|| !state.unlocked.safe_colony_25_5_minutes
|| !state.unlocked.ant_nest_without_tarinai;
const needsAntScan = !state.unlocked.ants_alive_25;
const needsTemperatureItemScan = !state.unlocked.below_absolute_zero_item;
const needsCleanFreakScan = !state.unlocked.clean_freak_robot_only;
if (!needsAliveScan && !needsAntScan && !needsTemperatureItemScan && !needsCleanFreakScan) return true;
const now = Math.max(0, Number(worldRef.time || 0) || 0);
const alive = needsAliveScan ? (worldRef.tarinai || []).filter(t => t && !t.dead) : [];
if (!state.unlocked.all_non_sleep_diseased_25
&& alive.length >= 25
&& alive.every(t => Boolean(t.zunchiDisease || t.explosionDisease || t.fightDisease))) {
unlock("all_non_sleep_diseased_25", { world: worldRef, count: alive.length });
}
const moodId = String(mood?.id || worldRef.colonyMood?.id || "");
const day = Math.max(1, Math.floor(Number(worldRef.day || 1) || 1));
if (alive.length > 0 && moodId === "happy" && day >= 6 && !state.unlocked.minimalist_happy) {
const placedAlive = (worldRef.items || []).reduce((count, item) => count + (item && !item.dead && item._achievementPlayerPlaced ? 1 : 0), 0);
if (placedAlive <= 10) unlock("minimalist_happy", { world: worldRef, mood: mood || worldRef.colonyMood, placedAlive });
}
if (!state.unlocked.colony_happy) {
if (alive.length > 0 && moodId === "happy" && day >= 6) {
let happyStart = Number(worldRef.achievementHappyStartAt);
if (!Number.isFinite(happyStart) || happyStart < 0 || happyStart > now) {
happyStart = now;
worldRef.achievementHappyStartAt = now;
}
const dayLength = Math.max(1, Number(global.CONFIG?.dayLength || 120) || 120);
if (now - happyStart >= dayLength) unlock("colony_happy", { world: worldRef, mood: mood || worldRef.colonyMood, elapsed: now - happyStart, day });
} else {
worldRef.achievementHappyStartAt = -1;
}
}
if (!state.unlocked.self_sufficient) {
if (alive.length >= 20 && !(worldRef.items || []).some(item => item && !item.dead && item.type === "duplicator")) {
let selfStart = Number(worldRef.achievementSelfSufficientStartAt);
if (!Number.isFinite(selfStart) || selfStart < 0 || selfStart > now) {
selfStart = now;
worldRef.achievementSelfSufficientStartAt = now;
}
if (now - selfStart >= 600) unlock("self_sufficient", { world: worldRef, count: alive.length, elapsed: now - selfStart });
} else {
worldRef.achievementSelfSufficientStartAt = -1;
}
}
if (!state.unlocked.rain_shelter_all
&& worldRef.weather === "light_rain"
&& alive.length >= 20
&& alive.every(tarinai => Boolean(global.TarinaiRainShelterSystem?.isSheltered?.(worldRef, tarinai.x, tarinai.y, tarinai)))) {
unlock("rain_shelter_all", { world: worldRef, count: alive.length });
}
const needsSauna = !state.unlocked.sauna_cold_plunge;
if (needsSauna) {
for (const tarinai of alive) {
const temperature = Number(worldRef.feltTemperatureFor?.(tarinai) ?? worldRef.temperatureAt?.(tarinai.x, tarinai.y, tarinai));
const status = Number.isFinite(temperature) ? worldRef.temperatureStatusFor?.(temperature, tarinai) : null;
const direction = String(status?.direction || "");
if (direction === "hot" && status?.comfortable !== true) tarinai._achievementSaunaHotAt = now;
const hotAt = tarinai._achievementSaunaHotAt == null ? NaN : Number(tarinai._achievementSaunaHotAt);
if (direction === "cold" && status?.comfortable !== true && Number.isFinite(hotAt) && now >= hotAt && now - hotAt <= 15.0001) {
unlock("sauna_cold_plunge", { world: worldRef, tarinai, elapsed: now - hotAt, temperature });
}
if (Number.isFinite(hotAt) && now - hotAt > 15.0001) tarinai._achievementSaunaHotAt = null;
}
}
if (!state.unlocked.idle_observer_5_minutes) {
let lastInterventionAt = Number(worldRef.achievementLastInterventionAt);
if (!Number.isFinite(lastInterventionAt) || lastInterventionAt < 0 || lastInterventionAt > now) {
lastInterventionAt = now;
worldRef.achievementLastInterventionAt = now;
}
if (now - lastInterventionAt >= 300) unlock("idle_observer_5_minutes", { world: worldRef, elapsed: now - lastInterventionAt });
}
if (!state.unlocked.safe_colony_25_5_minutes) {
if (alive.length >= 25) {
let safeStart = Number(worldRef.achievementNoDeathStartAt);
if (!Number.isFinite(safeStart) || safeStart < 0 || safeStart > now) {
safeStart = now;
worldRef.achievementNoDeathStartAt = now;
}
if (now - safeStart >= 300) unlock("safe_colony_25_5_minutes", { world: worldRef, count: alive.length, elapsed: now - safeStart });
} else {
worldRef.achievementNoDeathStartAt = -1;
}
}
if (!state.unlocked.sandbox_five_toilets) evaluateSandbox(worldRef, { source: "world-evaluation" });
if (!state.unlocked.ants_alive_25) recordAntPopulation(worldRef);
if (!state.unlocked.ant_nest_without_tarinai) evaluateAntNestWithoutTarinai(worldRef, { source: "world-evaluation" });
if (!state.unlocked.below_absolute_zero_item) recordBelowAbsoluteZeroItem(worldRef);
if (!state.unlocked.clean_freak_robot_only) evaluateCleanFreak(worldRef, { source: "world-evaluation" });
if (!state.unlocked.megalopolis && alive.length >= 100) evaluateMegalopolis(worldRef, { source: "world-evaluation" });
return true;
}
async function resetAchievements() {
const confirmed = await global.TarinaiGameDialogs?.confirm?.({
title: "\u5b9f\u7e3e\u306e\u30ea\u30bb\u30c3\u30c8",
message: "\u3059\u3079\u3066\u306e\u5b9f\u7e3e\u89e3\u9664\u72b6\u614b\u3068\u3001\u5171\u6709\u96c6\u8a08\u4e0a\u306e\u81ea\u5206\u306e\u89e3\u9664\u8a18\u9332\u3092\u524a\u9664\u3057\u307e\u3059\u3002",
confirmText: "\u30ea\u30bb\u30c3\u30c8",
cancelText: "\u30ad\u30e3\u30f3\u30bb\u30eb",
danger: true,
});
if (!confirmed) return false;
state = blankState();
state.resetPending = true;
runtimeByWorld = new WeakMap();
dangerFightDeathsByWorld = new WeakMap();
wireShockTrackers = new WeakMap();
toastQueue = [];
toastActive = false;
emptyClickTimes = [];
groundChangeTimes = [];
continuousPlayStartedAt = Date.now();
continuousPlayLastHeartbeatAt = continuousPlayStartedAt;
if (global.world) {
resetWorldProgress(global.world);
global.world.achievementLastInterventionAt = Math.max(0, Number(global.world.time || 0) || 0);
}
saveState();
recordDailyPlay({ source: "reset" });
render();
clearTimeout(showUnlockToast.absorbTimer);
clearTimeout(showUnlockToast.hideTimer);
dom.toast?.classList.add("hidden");
resetUnlockToastVisual();
dom.button?.classList.remove("achievement-absorb-target");
global.showToast?.("\u5b9f\u7e3e\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u307e\u3057\u305f\u3002");
void syncPending();
return true;
}
function unlockAllAchievementsForDebug() {
if (!DEBUG_MODE) return false;
const now = Date.now();
let changed = 0;
for (const definition of DEFINITIONS) {
if (state.unlocked[definition.id]) continue;
state.unlocked[definition.id] = now;
if (!Array.isArray(state.debugUnlocked)) state.debugUnlocked = [];
if (!state.debugUnlocked.includes(definition.id)) state.debugUnlocked.push(definition.id);
changed += 1;
}
saveState();
render();
global.showToast?.(changed ? `\u5168${DEFINITIONS.length}\u5b9f\u7e3e\u3092\u7372\u5f97\u3057\u307e\u3057\u305f\u3002` : "\u3059\u3079\u3066\u306e\u5b9f\u7e3e\u306f\u7372\u5f97\u6e08\u307f\u3067\u3059\u3002");
return changed > 0;
}
function openDialog() {
dom.dialog?.classList.remove("hidden");
render({ forceList: true });
dom.closeIconButton?.focus?.();
void syncPending({ full: true });
}
function closeDialog() {
dom.dialog?.classList.add("hidden");
dom.button?.focus?.();
}
document.addEventListener("visibilitychange", () => {
if (!document.hidden) continuousPlayLastHeartbeatAt = Date.now();
});
if (typeof global.setInterval === "function") {
global.setInterval(() => {
if (!document.hidden) recordContinuousPlay();
recordDailyPlay();
recordLowFps();
recordHeldTarinai(global.world, Date.now());
}, 1000);
if (!DEBUG_MODE) global.setInterval(() => void syncPending({ full: true }), FULL_SYNC_INTERVAL_MS);
}
dom.button?.addEventListener("click", () => {
global.audio?.uiClick?.();
openDialog();
});
dom.closeButton?.addEventListener("click", closeDialog);
dom.closeIconButton?.addEventListener("click", closeDialog);
dom.refreshButton?.addEventListener("click", () => void syncPending({ full: true }));
if (dom.unlockAllButton && DEBUG_MODE) dom.unlockAllButton.hidden = false;
dom.resetButton?.addEventListener("click", () => void resetAchievements());
dom.unlockAllButton?.addEventListener("click", unlockAllAchievementsForDebug);
dom.dialog?.addEventListener("click", event => {
if (event.target === dom.dialog) closeDialog();
});
document.addEventListener("keydown", event => {
if (event.key === "Escape" && !dom.dialog?.classList.contains("hidden")) closeDialog();
});
const api = Object.freeze({
definitions: DEFINITIONS,
categories: ACHIEVEMENT_CATEGORIES,
unlock,
open: openDialog,
close: closeDialog,
refresh() { return syncPending({ full: true }); },
reset: resetAchievements,
isUnlocked(id) { return Boolean(state.unlocked[String(id || "")]); },
recordBirth,
recordDeath,
recordNaturalZunchiSlave(detail = {}) { recordNaturalStatus("slave", detail); },
recordNaturalTarinaiKing(detail = {}) { recordNaturalStatus("king", detail); },
recordDirectFeed,
recordDirectCare,
recordConsumableUse,
recordFirstAidRecovery,
recordPlushiePokeFling,
evaluateStatusEffectAchievements,
recordIgnition,
recordIntervention,
recordPlayerPlacement,
recordPlayerDeletion,
recordHistoryAction,
recordRobotClean,
evaluateCleanFreak,
recordFightStarted,
recordShotFired,
recordLoveMochiBirth,
evaluateMegalopolis,
recordHeldTarinai,
resetWorldProgress,
recordLinkPlaced,
recordSignalActivation,
recordAntPopulation,
evaluateAntNestWithoutTarinai,
recordAntKilled,
recordStickyBombPass,
recordCircuitBoardSignalTransfer,
recordWirePowerState,
recordWireShock,
recordSaveSlotsFilled,
recordEmptyClick,
recordGroundChange,
evaluateSandbox,
recordLowFps,
recordContinuousPlay,
recordDailyPlay,
recordBelowAbsoluteZeroItem,
recordDamageSource,
recordAntDamageSource,
debugUnlockAll: unlockAllAchievementsForDebug,
exportSpellState,
importSpellState,
evaluateWorld,
recordSelfZunchiDeath(detail = {}) { return unlock("self_zunchi_death", detail); },
recordSoccerBallDeath(detail = {}) { return unlock("soccer_ball_death", detail); },
});
global.TarinaiAchievements = api;
recordDailyPlay({ source: "load" });
if (!state.unlocked[COMPLETIONIST_ID] && allOtherAchievementsUnlocked()) unlock(COMPLETIONIST_ID, { source: "load" });
render();
global.setTimeout(() => recordSaveSlotsFilled(), 0);
global.setTimeout(() => void syncPending({ full: true }), 700);
})(window);