y
This commit is contained in:
parent
61718e2981
commit
f657b6a4a4
94 changed files with 3970 additions and 1241 deletions
|
|
@ -4,7 +4,7 @@
|
|||
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 GAME_VERSION = String(global.TARINAI_VERSION || "39.16.77");
|
||||
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;
|
||||
|
|
@ -23,9 +23,17 @@
|
|||
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 SPELL_STATE_VERSION = 8;
|
||||
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 OBSERVER_GAME_DAYS = 3;
|
||||
const STRENGTH_IN_NUMBERS_TARGET = 100;
|
||||
const ELITE_FEW_MAX_POPULATION = 25;
|
||||
const ELITE_FEW_TARGET_DAYS = 5;
|
||||
const STATISTICIAN_TARGET = 30;
|
||||
const LIVELY_MAKING_TARGET = 100;
|
||||
const MEMENTO_MORI_TARGET = 10000;
|
||||
const FULL_SEASON_CYCLE_DAYS = 20;
|
||||
const PRE_UNLOCK_DESCRIPTION_IDS = new Set([
|
||||
"natural_zunchi_slave",
|
||||
"natural_tarinai_king",
|
||||
|
|
@ -53,20 +61,32 @@
|
|||
"chaos_seeker_666_fights",
|
||||
"fertility_seeker_721_love_births",
|
||||
"daily_play_7_days",
|
||||
"elite_few",
|
||||
"lively_making",
|
||||
"memento_mori",
|
||||
]);
|
||||
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",
|
||||
"natural_zunchi_slave", "natural_tarinai_king", "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",
|
||||
"self_sufficient", "rain_shelter_all", "medicine_ledger_all",
|
||||
"enemy_enemy_friend", "king_full_satisfaction", "slave_zero_satisfaction",
|
||||
"town_doctor_50", "ant_nest_without_tarinai", "zunchi_overflow",
|
||||
"comfortable_beds", "stone_pillow", "favorite_one",
|
||||
]),
|
||||
}),
|
||||
Object.freeze({
|
||||
id: "population",
|
||||
title: "\u7e41\u6b96\u30fb\u4eba\u53e3",
|
||||
ids: Object.freeze([
|
||||
"first_birth", "natural_zunchi_slave_5_generations", "natural_tarinai_king_3_generations",
|
||||
"birth_50_in_60_seconds", "great_mother_1000_births", "eternal_history_generation_10",
|
||||
"fertility_seeker_721_love_births", "strength_in_numbers", "elite_few",
|
||||
"lively_making", "memento_mori",
|
||||
]),
|
||||
}),
|
||||
Object.freeze({
|
||||
|
|
@ -76,8 +96,7 @@
|
|||
"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",
|
||||
"mercury_lifespan", "revolution", "fuel_to_fire",
|
||||
"chaos_seeker_666_fights", "sticky_bomb_15_passes", "wire_shock_7_tarinai",
|
||||
]),
|
||||
}),
|
||||
|
|
@ -92,12 +111,19 @@
|
|||
}),
|
||||
Object.freeze({
|
||||
id: "operation",
|
||||
title: "\u305d\u306e\u4ed6",
|
||||
title: "\u64cd\u4f5c",
|
||||
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",
|
||||
"poke_plushie_fling", "sniper_333_shots", "statistician", "well_informed",
|
||||
]),
|
||||
}),
|
||||
Object.freeze({
|
||||
id: "other",
|
||||
title: "\u305d\u306e\u4ed6",
|
||||
ids: Object.freeze([
|
||||
"low_fps_single_digit", "daily_play_7_days", "continuous_play_24_hours",
|
||||
"continuous_play_1_hour", "across_seasons", "true_tarinai_observer",
|
||||
]),
|
||||
}),
|
||||
]);
|
||||
|
|
@ -119,7 +145,7 @@
|
|||
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: "idle_observer_5_minutes", title: "\u76e3\u8996\u30ab\u30e1\u30e9", description: "3\u30b2\u30fc\u30e0\u65e5\u3001\u64cd\u4f5c\u305b\u305a\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" }),
|
||||
|
|
@ -166,6 +192,17 @@
|
|||
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" }),
|
||||
Object.freeze({ id: "strength_in_numbers", title: "\u6570\u3053\u305d\u529b", description: "\u751f\u5b58\u3057\u3066\u3044\u308b\u305f\u308a\u306a\u3044\u304c100\u4f53\u306b\u5230\u9054\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "elite_few", title: "\u5c11\u6570\u7cbe\u92ed", description: "\u305f\u308a\u306a\u3044\u304c25\u4f53\u4ee5\u4e0b\u306e\u72b6\u614b\u30925\u65e5\u9593\u7dad\u6301\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "zunchi_overflow", title: "\u8e29\u5c4e\u611f", description: "\u305f\u308a\u306a\u3044\u304c20\u4f53\u4ee5\u4e0a\u3044\u308b\u72b6\u614b\u3067\u3001\u305a\u3093\u3061\u306e\u6570\u304c\u305f\u308a\u306a\u3044\u306e\u6570\u3092\u4e0a\u56de\u308b\u3002" }),
|
||||
Object.freeze({ id: "comfortable_beds", title: "\u5feb\u9069\u306a\u5bdd\u5e8a", description: "\u751f\u5b58\u3057\u3066\u3044\u308b\u5168\u3066\u306e\u305f\u308a\u306a\u3044\u306b\u884c\u304d\u6e21\u308b\u6570\u306e\u5bdd\u5e8a\u3092\u7528\u610f\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "stone_pillow", title: "\u77f3\u306b\u6795\u3059", description: "\u305f\u308a\u306a\u3044\u304c30\u4f53\u4ee5\u4e0a\u3044\u308b\u72b6\u614b\u3067\u3001\u5bdd\u5e8a\u5145\u8db3\u7387\u30920%\u306b\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "across_seasons", title: "\u5b63\u7bc0\u3092\u8d8a\u3048\u3066", description: "\u6625\u590f\u79cb\u51ac\u306e\u56db\u5b63\u3092\u4e00\u5de1\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "favorite_one", title: "\u63a8\u3057", description: "\u3042\u308b\u500b\u4f53\u3092\u304a\u6c17\u306b\u5165\u308a\u306b\u767b\u9332\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "statistician", title: "\u7d71\u8a08\u5b66\u8005", description: "\u7d71\u8a08\u306e\u500b\u4f53\u6570\u30fb\u6b32\u6c42\u30fb\u74b0\u5883\u30fb\u30b0\u30e9\u30d5\u8868\u793a\u5207\u66ff\u30dc\u30bf\u30f3\u3092\u7d2f\u8a0830\u56de\u62bc\u3059\u3002" }),
|
||||
Object.freeze({ id: "well_informed", title: "\u60c5\u5831\u901a", description: "\u30d7\u30c3\u30b7\u30e5\u901a\u77e5\u3092ON\u306b\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "lively_making", title: "\u8cd1\u308f\u3044\u3065\u304f\u308a", description: "\u300c\u305f\u308a\u306a\u3044\u3092\u8ffd\u52a0\u300d\u3067100\u4f53\u8ffd\u52a0\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "memento_mori", title: "\u30e1\u30e1\u30f3\u30c8\u30fb\u30e2\u30ea", description: "\u305f\u308a\u306a\u3044\u304c\u5408\u8a0810000\u4f53\u6b7b\u4ea1\u3059\u308b\u3002" }),
|
||||
]);
|
||||
const definitionById = new Map(DEFINITIONS.map(def => [def.id, def]));
|
||||
const CATEGORY_DEFINITIONS = new Map(ACHIEVEMENT_CATEGORIES.map(category => [
|
||||
|
|
@ -187,6 +224,7 @@
|
|||
sharedStatus: document.getElementById("achievementSharedStatus"),
|
||||
toast: document.getElementById("achievementToast"),
|
||||
toastTitle: document.getElementById("achievementToastTitle"),
|
||||
toastCondition: document.getElementById("achievementToastCondition"),
|
||||
pauseButton: document.getElementById("pauseBtn"),
|
||||
};
|
||||
|
||||
|
|
@ -205,9 +243,10 @@
|
|||
let continuousPlayLastHeartbeatAt = continuousPlayStartedAt;
|
||||
let lastListSignature = "";
|
||||
let scheduledSyncTimer = 0;
|
||||
let scheduledProgressSaveTimer = 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 } };
|
||||
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, statsButtonPressCount: 0, totalDeathCount: 0, manualTarinaiAddedCount: 0 } };
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
|
|
@ -257,6 +296,9 @@
|
|||
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)),
|
||||
statsButtonPressCount: Math.min(STATISTICIAN_TARGET, Math.max(0, Math.floor(Number(progressSource.statsButtonPressCount || 0) || 0))),
|
||||
totalDeathCount: Math.min(MEMENTO_MORI_TARGET, Math.max(0, Math.floor(Number(progressSource.totalDeathCount || 0) || 0))),
|
||||
manualTarinaiAddedCount: Math.min(LIVELY_MAKING_TARGET, Math.max(0, Math.floor(Number(progressSource.manualTarinaiAddedCount || 0) || 0))),
|
||||
};
|
||||
const missingCompletionRequirement = DEFINITIONS.some(definition => definition.id !== COMPLETIONIST_ID && !unlocked[definition.id]);
|
||||
if (unlocked[COMPLETIONIST_ID] && missingCompletionRequirement) {
|
||||
|
|
@ -275,6 +317,14 @@
|
|||
catch (_) {}
|
||||
}
|
||||
|
||||
function scheduleProgressSave(delay = 350) {
|
||||
if (scheduledProgressSaveTimer) return;
|
||||
scheduledProgressSaveTimer = global.setTimeout(() => {
|
||||
scheduledProgressSaveTimer = 0;
|
||||
saveState();
|
||||
}, Math.max(0, Number(delay) || 0));
|
||||
}
|
||||
|
||||
function createPlayerId() {
|
||||
if (global.crypto?.randomUUID) return global.crypto.randomUUID();
|
||||
const hex = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
|
||||
|
|
@ -376,6 +426,29 @@
|
|||
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 === "elite_few") {
|
||||
const world = global.world || null;
|
||||
const dayLength = Math.max(1, Number(world?.config?.dayLength ?? global.CONFIG?.dayLength ?? 120) || 120);
|
||||
const now = Math.max(0, Number(world?.time || 0) || 0);
|
||||
const startedAt = Number(world?.achievementEliteFewStartAt);
|
||||
const elapsedDays = Number.isFinite(startedAt) && startedAt >= 0 && now >= startedAt
|
||||
? Math.min(ELITE_FEW_TARGET_DAYS, (now - startedAt) / dayLength)
|
||||
: 0;
|
||||
const aliveCount = (world?.tarinai || []).reduce((count, tarinai) => count + (tarinai && !tarinai.dead ? 1 : 0), 0);
|
||||
return `\u73fe\u5728 ${elapsedDays.toFixed(1)} / ${ELITE_FEW_TARGET_DAYS}\u65e5\uff08\u500b\u4f53\u6570 ${aliveCount} / ${ELITE_FEW_MAX_POPULATION}\u4ee5\u4e0b\uff09`;
|
||||
}
|
||||
if (id === "statistician") {
|
||||
const count = state.unlocked[id] ? STATISTICIAN_TARGET : Math.min(STATISTICIAN_TARGET, Math.max(0, Math.floor(Number(state.progress?.statsButtonPressCount || 0) || 0)));
|
||||
return `\u73fe\u5728 ${count} / ${STATISTICIAN_TARGET}\u56de`;
|
||||
}
|
||||
if (id === "lively_making") {
|
||||
const count = state.unlocked[id] ? LIVELY_MAKING_TARGET : Math.min(LIVELY_MAKING_TARGET, Math.max(0, Math.floor(Number(state.progress?.manualTarinaiAddedCount || 0) || 0)));
|
||||
return `\u73fe\u5728 ${count} / ${LIVELY_MAKING_TARGET}\u4f53`;
|
||||
}
|
||||
if (id === "memento_mori") {
|
||||
const count = state.unlocked[id] ? MEMENTO_MORI_TARGET : Math.min(MEMENTO_MORI_TARGET, Math.max(0, Math.floor(Number(state.progress?.totalDeathCount || 0) || 0)));
|
||||
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(count / MEMENTO_MORI_TARGET * 100).toFixed(1)}%\uff08${count} / ${MEMENTO_MORI_TARGET}\u4f53\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`;
|
||||
|
|
@ -411,6 +484,10 @@
|
|||
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),
|
||||
Math.min(STATISTICIAN_TARGET, Math.max(0, Number(state.progress?.statsButtonPressCount || 0) || 0)),
|
||||
Math.min(MEMENTO_MORI_TARGET, Math.max(0, Number(state.progress?.totalDeathCount || 0) || 0)),
|
||||
Math.min(LIVELY_MAKING_TARGET, Math.max(0, Number(state.progress?.manualTarinaiAddedCount || 0) || 0)),
|
||||
Math.floor(Math.max(0, (Number(global.world?.time || 0) || 0) - Math.max(0, Number(global.world?.achievementEliteFewStartAt || 0) || 0)) / Math.max(1, Number(global.world?.config?.dayLength ?? global.CONFIG?.dayLength ?? 120) || 120) * 10),
|
||||
].join(":");
|
||||
return `${unlocks}|${rates}|${local}`;
|
||||
}
|
||||
|
|
@ -580,7 +657,7 @@
|
|||
if (toastActive || !toastQueue.length) return;
|
||||
const definition = toastQueue.shift();
|
||||
if (!dom.toast || !dom.toastTitle) {
|
||||
global.showToast?.(`\u5b9f\u7e3e\u89e3\u9664: ${definition.title}`);
|
||||
global.showToast?.(`\u5b9f\u7e3e\u89e3\u9664: ${definition.title} \u2014 ${definition.description || ""}`);
|
||||
global.setTimeout(runNextUnlockToast, 80);
|
||||
return;
|
||||
}
|
||||
|
|
@ -590,6 +667,7 @@
|
|||
resetUnlockToastVisual();
|
||||
dom.button?.classList.remove("achievement-absorb-target");
|
||||
dom.toastTitle.textContent = definition.title;
|
||||
if (dom.toastCondition) dom.toastCondition.textContent = definition.description || "";
|
||||
dom.toast.classList.remove("hidden");
|
||||
void dom.toast.offsetWidth;
|
||||
showUnlockToast.absorbTimer = global.setTimeout(absorbUnlockToast, 2700);
|
||||
|
|
@ -796,10 +874,17 @@
|
|||
|
||||
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;
|
||||
const depthKey = property === "achievementNaturalSlaveGenerations"
|
||||
? "_achievementNaturalSlaveLineageDepth"
|
||||
: "_achievementNaturalKingLineageDepth";
|
||||
const parentIds = Array.isArray(tarinai.parents) ? tarinai.parents.filter(Boolean) : [];
|
||||
let parentDepth = 0;
|
||||
for (const parentId of parentIds) {
|
||||
const parent = (world.tarinai || []).find(candidate => candidate && String(candidate.id || "") === String(parentId));
|
||||
parentDepth = Math.max(parentDepth, Math.max(0, Math.floor(Number(parent?.[depthKey] || 0) || 0)));
|
||||
}
|
||||
tarinai[depthKey] = Math.max(1, parentDepth + 1);
|
||||
return tarinai[depthKey] >= required ? tarinai[depthKey] : 0;
|
||||
}
|
||||
|
||||
function detailWorld(detail = {}) {
|
||||
|
|
@ -831,6 +916,9 @@
|
|||
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));
|
||||
state.progress.statsButtonPressCount = Math.min(STATISTICIAN_TARGET, Math.max(0, Math.floor(Number(state.progress.statsButtonPressCount || 0) || 0)));
|
||||
state.progress.totalDeathCount = Math.min(MEMENTO_MORI_TARGET, Math.max(0, Math.floor(Number(state.progress.totalDeathCount || 0) || 0)));
|
||||
state.progress.manualTarinaiAddedCount = Math.min(LIVELY_MAKING_TARGET, Math.max(0, Math.floor(Number(state.progress.manualTarinaiAddedCount || 0) || 0)));
|
||||
return state.progress;
|
||||
}
|
||||
|
||||
|
|
@ -838,14 +926,15 @@
|
|||
return Math.max(0, Math.min(255, Number(value) || 0)).toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
function unlockedIndexesFromMaskWords(lowWord = 0, highWord = 0) {
|
||||
function unlockedIndexesFromMaskWords(lowWord = 0, highWord = 0, extraWord = 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 high = Math.max(0, Math.min(0xffffffff, Math.floor(Number(highWord) || 0)));
|
||||
const extraMax = (2 ** Math.max(0, DEFINITIONS.length - 64)) - 1;
|
||||
const extra = Math.max(0, Math.min(extraMax, Math.floor(Number(extraWord) || 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;
|
||||
const word = index < 32 ? low : (index < 64 ? high : extra);
|
||||
const bit = index < 32 ? index : (index < 64 ? index - 32 : index - 64);
|
||||
if (Math.floor(word / (2 ** bit)) % 2) indexes.push(index);
|
||||
}
|
||||
return indexes;
|
||||
|
|
@ -854,13 +943,15 @@
|
|||
function exportSpellState() {
|
||||
let lowWord = 0;
|
||||
let highWord = 0;
|
||||
let extraWord = 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);
|
||||
else if (index < 64) highWord += 2 ** (index - 32);
|
||||
else extraWord += 2 ** (index - 64);
|
||||
timestamps.push(Math.max(1, Math.floor(timestamp / 60000)));
|
||||
}
|
||||
const baseTimestamp = timestamps.length ? Math.min(...timestamps) : 0;
|
||||
|
|
@ -878,6 +969,7 @@
|
|||
SPELL_STATE_VERSION,
|
||||
lowWord >>> 0,
|
||||
highWord >>> 0,
|
||||
extraWord >>> 0,
|
||||
baseTimestamp,
|
||||
deltas,
|
||||
[
|
||||
|
|
@ -895,33 +987,39 @@
|
|||
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),
|
||||
state.unlocked.statistician ? 0 : Math.min(STATISTICIAN_TARGET, progress.statsButtonPressCount),
|
||||
state.unlocked.memento_mori ? 0 : Math.min(MEMENTO_MORI_TARGET, progress.totalDeathCount),
|
||||
state.unlocked.lively_making ? 0 : Math.min(LIVELY_MAKING_TARGET, progress.manualTarinaiAddedCount),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function parseSpellState(payload = null) {
|
||||
if (!Array.isArray(payload) || Number(payload[0]) !== SPELL_STATE_VERSION) return null;
|
||||
if (!Array.isArray(payload)) return null;
|
||||
const version = Number(payload[0]);
|
||||
if (version !== 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;
|
||||
const extraWord = Number(payload[3]);
|
||||
const extraMax = (2 ** Math.max(0, DEFINITIONS.length - 64)) - 1;
|
||||
if (!Number.isSafeInteger(lowWord) || lowWord < 0 || lowWord > 0xffffffff ||
|
||||
!Number.isSafeInteger(highWord) || highWord < 0 || highWord > highMax) {
|
||||
!Number.isSafeInteger(highWord) || highWord < 0 || highWord > 0xffffffff ||
|
||||
!Number.isSafeInteger(extraWord) || extraWord < 0 || extraWord > extraMax) {
|
||||
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] : [],
|
||||
unlockedIndexes: unlockedIndexesFromMaskWords(lowWord, highWord, extraWord),
|
||||
baseTimestamp: Math.max(0, Math.floor(Number(payload[4] || 0) || 0)),
|
||||
deltas: Array.isArray(payload[5]) ? payload[5] : [],
|
||||
packed: Array.isArray(payload[6]) ? payload[6] : [],
|
||||
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;
|
||||
const { unlockedIndexes, baseTimestamp, deltas, packed, timestampUnitMs } = parsed;
|
||||
if (deltas.length !== unlockedIndexes.length) throw new Error("invalid achievement spell timestamps");
|
||||
const importedUnlocks = [];
|
||||
let importedCompletionistAt = 0;
|
||||
|
|
@ -971,19 +1069,20 @@
|
|||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
progress.statsButtonPressCount = Math.max(progress.statsButtonPressCount, Math.min(STATISTICIAN_TARGET, Math.max(0, Math.floor(Number(packed[14] || 0) || 0))));
|
||||
progress.totalDeathCount = Math.max(progress.totalDeathCount, Math.min(MEMENTO_MORI_TARGET, Math.max(0, Math.floor(Number(packed[15] || 0) || 0))));
|
||||
progress.manualTarinaiAddedCount = Math.max(progress.manualTarinaiAddedCount, Math.min(LIVELY_MAKING_TARGET, Math.max(0, Math.floor(Number(packed[16] || 0) || 0))));
|
||||
const progressChanged = previousProgress !== JSON.stringify(progress);
|
||||
changed = changed || progressChanged;
|
||||
|
||||
|
|
@ -1006,6 +1105,7 @@
|
|||
|
||||
function recordPlayerPlacement(detail = {}) {
|
||||
const world = detailWorld(detail);
|
||||
if (world) recordIntervention({ ...detail, world, tool: detail.tool || "placement" });
|
||||
if (!world) return false;
|
||||
recordIntervention({ ...detail, world });
|
||||
const item = detail.item || null;
|
||||
|
|
@ -1039,6 +1139,9 @@
|
|||
if (!Number.isFinite(placedAt) || now < placedAt || now - placedAt > 30.0001) return changed;
|
||||
const progress = ensureProgress();
|
||||
if (!state.unlocked.unplanned_city_30) {
|
||||
const operationKey = detail.operationId == null ? "" : String(detail.operationId);
|
||||
if (operationKey && world._achievementLastQuickDeleteOperation === operationKey) return changed;
|
||||
if (operationKey) world._achievementLastQuickDeleteOperation = operationKey;
|
||||
progress.quickDeleteCount = Math.min(30, progress.quickDeleteCount + 1);
|
||||
saveState();
|
||||
renderIfDialogOpen();
|
||||
|
|
@ -1050,6 +1153,8 @@
|
|||
function recordHistoryAction(kind, detail = {}) {
|
||||
const action = String(kind || detail.kind || "");
|
||||
if (action !== "undo" && action !== "redo") return false;
|
||||
const world = detailWorld(detail);
|
||||
if (world) recordIntervention({ ...detail, world, tool: action });
|
||||
const progress = ensureProgress();
|
||||
const key = action === "undo" ? "undoCount" : "redoCount";
|
||||
const achievementId = action === "undo" ? "undo_20" : "redo_20";
|
||||
|
|
@ -1149,7 +1254,8 @@
|
|||
function recordAntKilled(detail = {}) {
|
||||
let changed = false;
|
||||
const progress = ensureProgress();
|
||||
if (!state.unlocked.ants_killed_100) {
|
||||
const playerCaused = Boolean(detail.playerCaused || detail.ant?._achievementLastDamageSource?.danger);
|
||||
if (!state.unlocked.ants_killed_100 && playerCaused) {
|
||||
progress.antKills = Math.min(100, progress.antKills + 1);
|
||||
saveState();
|
||||
renderIfDialogOpen();
|
||||
|
|
@ -1277,6 +1383,8 @@
|
|||
}
|
||||
|
||||
function recordEmptyClick(detail = {}) {
|
||||
const world = detailWorld(detail);
|
||||
if (world) recordIntervention({ ...detail, world, tool: "empty_click" });
|
||||
if (state.unlocked.pause_spam_4_in_1_second) return false;
|
||||
const now = Number(detail.now);
|
||||
const timestamp = Number.isFinite(now) ? now : Date.now();
|
||||
|
|
@ -1294,6 +1402,7 @@
|
|||
const previous = String(detail.previous || "");
|
||||
const next = String(detail.next || world?.groundType || "");
|
||||
if (!world || detail.silent || !previous || !next || previous === next) return false;
|
||||
recordIntervention({ ...detail, world, tool: "ground_change" });
|
||||
let changed = false;
|
||||
if (!state.unlocked.park_ground_changed && String(world.fieldType || "") === "park") {
|
||||
changed = unlock("park_ground_changed", { ...detail, world, previous, next }) || changed;
|
||||
|
|
@ -1478,11 +1587,12 @@
|
|||
|
||||
function recordDirectCare(detail = {}) {
|
||||
const world = detailWorld(detail);
|
||||
if (world) recordIntervention({ ...detail, world, tool: "direct_care" });
|
||||
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));
|
||||
const treatment = Boolean(detail.beneficialRecovery);
|
||||
if (foodGift) {
|
||||
tarinai._achievementDirectFeedCount = Math.max(0, Math.floor(Number(tarinai._achievementDirectFeedCount || 0) || 0)) + 1;
|
||||
world.achievementSelfSufficientStartAt = -1;
|
||||
|
|
@ -1552,15 +1662,12 @@
|
|||
|
||||
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);
|
||||
const aliveCount = playstylePopulationCount(world);
|
||||
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;
|
||||
}
|
||||
const duplicatorCount = playstyleItemCount(world, "duplicator");
|
||||
const shelterCount = playstyleItemCount(world, "grass_bed")
|
||||
+ playstyleItemCount(world, "nest_box")
|
||||
+ playstyleItemCount(world, "pipe");
|
||||
if (duplicatorCount < 10 || shelterCount < 15) return false;
|
||||
return unlock("megalopolis", { ...detail, world, aliveCount, duplicatorCount, shelterCount });
|
||||
}
|
||||
|
|
@ -1578,11 +1685,22 @@
|
|||
world.achievementNoDeathStartAt = -1;
|
||||
world.achievementHappyStartAt = -1;
|
||||
world.achievementFightMochiSerial = 0;
|
||||
world.achievementEliteFewStartAt = -1;
|
||||
for (const tarinai of world.tarinai || []) {
|
||||
if (!tarinai) continue;
|
||||
tarinai._achievementFightMochiAt = null;
|
||||
tarinai._achievementFightMochiSerial = 0;
|
||||
tarinai._achievementFightMochiWorld = null;
|
||||
tarinai._achievementDirectFeedCount = 0;
|
||||
tarinai._achievementDirectTreatmentCount = 0;
|
||||
tarinai._achievementSaunaHotAt = null;
|
||||
tarinai._achievementSaunaWasHot = false;
|
||||
tarinai._achievementHeldStartedAt = null;
|
||||
}
|
||||
for (const item of world.items || []) {
|
||||
if (!item) continue;
|
||||
if (item.type === "sticky_bomb") item.stickyBombPassCount = 0;
|
||||
item._achievementPlacedAt = null;
|
||||
}
|
||||
world.achievementNaturalSlaveGenerations = [];
|
||||
world.achievementNaturalKingGenerations = [];
|
||||
|
|
@ -1619,6 +1737,8 @@
|
|||
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 });
|
||||
if (child?.isZunchiSlave) recordNaturalStatus("slave", { ...detail, world, tarinai: child, bornWithStatus: true });
|
||||
if (child?.isTarinaiChampion) recordNaturalStatus("king", { ...detail, world, tarinai: child, bornWithStatus: true });
|
||||
evaluateMegalopolis(world, detail);
|
||||
const progress = ensureProgress();
|
||||
if (!state.unlocked.great_mother_1000_births) {
|
||||
|
|
@ -1638,12 +1758,18 @@
|
|||
|
||||
function recordDeath(detail = {}) {
|
||||
recordFightPairDangerDeath(detail);
|
||||
if (!state.unlocked.memento_mori) {
|
||||
const progress = ensureProgress();
|
||||
progress.totalDeathCount = Math.min(MEMENTO_MORI_TARGET, progress.totalDeathCount + 1);
|
||||
if (progress.totalDeathCount >= MEMENTO_MORI_TARGET) unlock("memento_mori", { ...detail, count: progress.totalDeathCount });
|
||||
else { scheduleProgressSave(); renderIfDialogOpen(); }
|
||||
}
|
||||
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);
|
||||
const aliveAfterDeath = (world.tarinai || []).reduce((count, candidate) => count + (candidate && !candidate.dead ? 1 : 0), 0);
|
||||
const aliveAfterDeath = playstylePopulationCount(world);
|
||||
if (!state.unlocked.enemy_enemy_friend && aliveAfterDeath < 30) world.achievementEnemyAntKills = 0;
|
||||
if (!lifespanDeath) {
|
||||
world.achievementNoDeathStartAt = aliveAfterDeath >= 25 ? Math.max(0, Number(world.time || 0) || 0) : -1;
|
||||
|
|
@ -1706,12 +1832,126 @@
|
|||
return unlock("ignite_during_birth_ritual", detail);
|
||||
}
|
||||
|
||||
function recordFavoriteTarinai(detail = {}) {
|
||||
if (state.unlocked.favorite_one) return false;
|
||||
const tarinai = detail.tarinai || detail.target || null;
|
||||
if (!tarinai || tarinai.dead || !tarinai.favorite) return false;
|
||||
return unlock("favorite_one", detail);
|
||||
}
|
||||
|
||||
function recordStatsButtonPress(detail = {}) {
|
||||
if (state.unlocked.statistician) return false;
|
||||
const progress = ensureProgress();
|
||||
progress.statsButtonPressCount = Math.min(STATISTICIAN_TARGET, progress.statsButtonPressCount + 1);
|
||||
if (progress.statsButtonPressCount >= STATISTICIAN_TARGET) {
|
||||
return unlock("statistician", { ...detail, count: progress.statsButtonPressCount });
|
||||
}
|
||||
saveState();
|
||||
renderIfDialogOpen();
|
||||
return false;
|
||||
}
|
||||
|
||||
function recordPushNotificationsEnabled(detail = {}) {
|
||||
if (state.unlocked.well_informed) return false;
|
||||
return unlock("well_informed", detail);
|
||||
}
|
||||
|
||||
function recordManualTarinaiAdded(detail = {}) {
|
||||
if (state.unlocked.lively_making) return false;
|
||||
const progress = ensureProgress();
|
||||
progress.manualTarinaiAddedCount = Math.min(LIVELY_MAKING_TARGET, progress.manualTarinaiAddedCount + 1);
|
||||
if (progress.manualTarinaiAddedCount >= LIVELY_MAKING_TARGET) {
|
||||
return unlock("lively_making", { ...detail, count: progress.manualTarinaiAddedCount });
|
||||
}
|
||||
saveState();
|
||||
renderIfDialogOpen();
|
||||
return false;
|
||||
}
|
||||
|
||||
function playstylePopulationCount(worldRef) {
|
||||
const cached = worldRef?.tarinaiCounts?.();
|
||||
if (cached && Number.isFinite(Number(cached.alive))) return Math.max(0, Number(cached.alive) || 0);
|
||||
return (worldRef?.tarinai || []).reduce((count, tarinai) => count + (tarinai && !tarinai.dead ? 1 : 0), 0);
|
||||
}
|
||||
|
||||
function playstyleItemCount(worldRef, type) {
|
||||
const counts = worldRef?.itemCounts;
|
||||
if (counts && typeof counts === "object") return Math.max(0, Number(counts[type] || 0) || 0);
|
||||
let count = 0;
|
||||
for (const item of worldRef?.items || []) if (item && !item.dead && item.type === type) count += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
function playstyleBedCapacity(worldRef) {
|
||||
return playstyleItemCount(worldRef, "bed")
|
||||
+ playstyleItemCount(worldRef, "grass_bed")
|
||||
+ playstyleItemCount(worldRef, "nest_box") * 5
|
||||
+ playstyleItemCount(worldRef, "pipe") * 3;
|
||||
}
|
||||
|
||||
function evaluateEvent(worldRef, trigger = "timer", detail = {}) {
|
||||
if (!worldRef) return false;
|
||||
const kind = String(trigger || "timer");
|
||||
const populationEvent = kind === "population" || kind === "timer" || kind === "season" || kind === "restore";
|
||||
const itemEvent = kind === "items" || populationEvent;
|
||||
const now = Math.max(0, Number(worldRef.time || 0) || 0);
|
||||
const aliveCount = populationEvent || itemEvent ? playstylePopulationCount(worldRef) : 0;
|
||||
let changed = false;
|
||||
|
||||
if (populationEvent) {
|
||||
if (!state.unlocked.strength_in_numbers && aliveCount >= STRENGTH_IN_NUMBERS_TARGET) {
|
||||
changed = unlock("strength_in_numbers", { ...detail, world: worldRef, count: aliveCount }) || changed;
|
||||
}
|
||||
if (!state.unlocked.elite_few) {
|
||||
if (aliveCount > 0 && aliveCount <= ELITE_FEW_MAX_POPULATION) {
|
||||
let startedAt = Number(worldRef.achievementEliteFewStartAt);
|
||||
if (!Number.isFinite(startedAt) || startedAt < 0 || startedAt > now) {
|
||||
startedAt = now;
|
||||
worldRef.achievementEliteFewStartAt = now;
|
||||
}
|
||||
const dayLength = Math.max(1, Number(worldRef.config?.dayLength ?? global.CONFIG?.dayLength ?? 120) || 120);
|
||||
const targetDuration = dayLength * ELITE_FEW_TARGET_DAYS;
|
||||
if (now - startedAt >= targetDuration) {
|
||||
changed = unlock("elite_few", { ...detail, world: worldRef, count: aliveCount, elapsed: now - startedAt, days: ELITE_FEW_TARGET_DAYS }) || changed;
|
||||
}
|
||||
} else {
|
||||
worldRef.achievementEliteFewStartAt = -1;
|
||||
}
|
||||
}
|
||||
changed = evaluateMegalopolis(worldRef, detail) || changed;
|
||||
}
|
||||
|
||||
if (itemEvent) {
|
||||
const zunchiCount = playstyleItemCount(worldRef, "zunchi");
|
||||
const bedCapacity = playstyleBedCapacity(worldRef);
|
||||
if (!state.unlocked.zunchi_overflow && aliveCount >= 20 && zunchiCount > aliveCount) {
|
||||
changed = unlock("zunchi_overflow", { ...detail, world: worldRef, aliveCount, zunchiCount }) || changed;
|
||||
}
|
||||
if (!state.unlocked.comfortable_beds && aliveCount > 0 && bedCapacity >= aliveCount) {
|
||||
changed = unlock("comfortable_beds", { ...detail, world: worldRef, aliveCount, bedCapacity }) || changed;
|
||||
}
|
||||
if (!state.unlocked.stone_pillow && aliveCount >= 30 && bedCapacity === 0) {
|
||||
changed = unlock("stone_pillow", { ...detail, world: worldRef, aliveCount, bedCapacity }) || changed;
|
||||
}
|
||||
changed = evaluateMegalopolis(worldRef, detail) || changed;
|
||||
}
|
||||
|
||||
if ((kind === "season" || kind === "timer" || kind === "restore") && !state.unlocked.across_seasons) {
|
||||
const elapsedDays = Number.isFinite(Number(worldRef.elapsedDays))
|
||||
? Math.max(0, Number(worldRef.elapsedDays) || 0)
|
||||
: Math.max(0, Math.floor(now / Math.max(1, Number(worldRef.config?.dayLength ?? global.CONFIG?.dayLength ?? 120) || 120)));
|
||||
if (elapsedDays >= FULL_SEASON_CYCLE_DAYS) changed = unlock("across_seasons", { ...detail, world: worldRef, elapsedDays }) || changed;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
evaluateEvent(worldRef, "timer", { mood });
|
||||
const needsAliveScan = !state.unlocked.all_non_sleep_diseased_25
|
||||
|| !state.unlocked.colony_happy
|
||||
|| !state.unlocked.minimalist_happy
|
||||
|
|
@ -1727,6 +1967,7 @@
|
|||
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) : [];
|
||||
const aliveCount = alive.length;
|
||||
if (!state.unlocked.all_non_sleep_diseased_25
|
||||
&& alive.length >= 25
|
||||
&& alive.every(t => Boolean(t.zunchiDisease || t.explosionDisease || t.fightDisease))) {
|
||||
|
|
@ -1778,12 +2019,24 @@
|
|||
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 });
|
||||
const isHot = direction === "hot" && status?.comfortable !== true;
|
||||
const isCold = direction === "cold" && status?.comfortable !== true;
|
||||
let hotAt = tarinai._achievementSaunaHotAt == null ? NaN : Number(tarinai._achievementSaunaHotAt);
|
||||
let wasHot = tarinai._achievementSaunaWasHot;
|
||||
if (wasHot == null) wasHot = Boolean(isHot && Number.isFinite(hotAt));
|
||||
if (isHot) {
|
||||
if (!wasHot || !Number.isFinite(hotAt) || hotAt > now) {
|
||||
hotAt = now;
|
||||
tarinai._achievementSaunaHotAt = now;
|
||||
}
|
||||
tarinai._achievementSaunaWasHot = true;
|
||||
} else {
|
||||
tarinai._achievementSaunaWasHot = false;
|
||||
if (isCold && 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 (Number.isFinite(hotAt) && now - hotAt > 15.0001) tarinai._achievementSaunaHotAt = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1793,7 +2046,9 @@
|
|||
lastInterventionAt = now;
|
||||
worldRef.achievementLastInterventionAt = now;
|
||||
}
|
||||
if (now - lastInterventionAt >= 300) unlock("idle_observer_5_minutes", { world: worldRef, elapsed: now - lastInterventionAt });
|
||||
const dayLength = Math.max(1, Number(worldRef.config?.dayLength ?? global.CONFIG?.dayLength ?? 120) || 120);
|
||||
const observerTarget = dayLength * OBSERVER_GAME_DAYS;
|
||||
if (now - lastInterventionAt >= observerTarget) unlock("idle_observer_5_minutes", { world: worldRef, elapsed: now - lastInterventionAt, gameDays: OBSERVER_GAME_DAYS });
|
||||
}
|
||||
|
||||
if (!state.unlocked.safe_colony_25_5_minutes) {
|
||||
|
|
@ -1913,6 +2168,7 @@
|
|||
document.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape" && !dom.dialog?.classList.contains("hidden")) closeDialog();
|
||||
});
|
||||
global.addEventListener?.("pagehide", saveState);
|
||||
|
||||
const api = Object.freeze({
|
||||
definitions: DEFINITIONS,
|
||||
|
|
@ -1945,6 +2201,10 @@
|
|||
recordLoveMochiBirth,
|
||||
evaluateMegalopolis,
|
||||
recordHeldTarinai,
|
||||
recordFavoriteTarinai,
|
||||
recordStatsButtonPress,
|
||||
recordPushNotificationsEnabled,
|
||||
recordManualTarinaiAdded,
|
||||
resetWorldProgress,
|
||||
recordLinkPlaced,
|
||||
recordSignalActivation,
|
||||
|
|
@ -1969,6 +2229,7 @@
|
|||
exportSpellState,
|
||||
importSpellState,
|
||||
evaluateWorld,
|
||||
evaluateEvent,
|
||||
recordSelfZunchiDeath(detail = {}) { return unlock("self_zunchi_death", detail); },
|
||||
recordSoccerBallDeath(detail = {}) { return unlock("soccer_ball_death", detail); },
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue