This commit is contained in:
33333-33333 2026-07-20 14:36:59 +09:00
commit f6d7412744
94 changed files with 3836 additions and 1226 deletions

View file

@ -20,7 +20,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 ROBOT_ALL_TARGET_MASK = 0x7f;
const SYNC_DEBOUNCE_MS = 450;
const FULL_SYNC_INTERVAL_MS = 5 * 60 * 1000;
const SPELL_STATE_VERSION = 8;
@ -65,7 +65,10 @@
"elite_few",
"lively_making",
"memento_mori",
"mad_scientist",
"rain_shelter_all",
"self_zunchi_death",
"zunchi_collision_death",
"megalopolis",
]);
const ACHIEVEMENT_CATEGORIES = Object.freeze([
Object.freeze({
@ -95,7 +98,7 @@
id: "experiment",
title: "\u5b9f\u9a13",
ids: Object.freeze([
"self_zunchi_death", "death_50_in_10_seconds", "soccer_ball_death",
"self_zunchi_death", "zunchi_collision_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",
"mercury_lifespan", "revolution", "fuel_to_fire",
@ -134,11 +137,12 @@
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: "zunchi_collision_death", title: "\u304f\u3061\u3083\u3044\u306e\u3060\uff01", description: "\u305a\u3093\u3061\u3068\u306e\u885d\u7a81\u3067\u6b7b\u4ea1\u3059\u308b\u500b\u4f53\u304c\u51fa\u73fe\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: "great_mother_1000_births", title: "\u6bcd\u306a\u308b\u5927\u5730", 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" }),
@ -249,7 +253,7 @@
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, statsButtonPressCount: 0, totalDeathCount: 0, manualTarinaiAddedCount: 0, mysteryDrugTarinaiIds: [] } };
return { unlocked: {}, pending: [], resetPending: false, debugUnlocked: [], progress: { placementCount: 0, linkTypes: [], signalActivated: false, antKills: 0, birthCount: 0, quickDeleteCount: 0, undoCount: 0, redoCount: 0, directFeedCount: 0, medicineTypes: [], firstAidHeals: 0, fightMochiFightCount: 0, shotCount: 0, loveMochiBirthCount: 0, dailyPlayStreak: 0, dailyPlayLastDay: 0, statsButtonPressCount: 0, totalDeathCount: 0, manualTarinaiAddedCount: 0, mysteryDrugTarinaiIds: [] } };
}
function loadState() {
@ -261,15 +265,6 @@
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) {
@ -277,21 +272,17 @@
}
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),
linkTypes: [...new Set((Array.isArray(progressSource.linkTypes) ? progressSource.linkTypes : []).filter(type => ["rope", "rod", "spring"].includes(type)))],
signalActivated: Boolean(progressSource.signalActivated),
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)),
directFeedCount: Math.min(33, Math.max(0, Math.floor(Number(progressSource.directFeedCount || 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)),
@ -495,6 +486,7 @@
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.min(33, Math.max(0, Number(state.progress?.directFeedCount || 0) || 0)),
Math.min(MAD_SCIENTIST_TARGET, state.progress?.mysteryDrugTarinaiIds?.length || 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(":");
@ -918,6 +910,7 @@
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));
state.progress.directFeedCount = Math.min(33, Math.max(0, Math.floor(Number(state.progress.directFeedCount || 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));
@ -1003,6 +996,7 @@
state.unlocked.memento_mori ? 0 : Math.min(MEMENTO_MORI_TARGET, progress.totalDeathCount),
state.unlocked.lively_making ? 0 : Math.min(LIVELY_MAKING_TARGET, progress.manualTarinaiAddedCount),
state.unlocked.mad_scientist ? 0 : Math.min(MAD_SCIENTIST_TARGET, progress.mysteryDrugTarinaiIds.length),
state.unlocked.direct_feed_33 ? 0 : Math.min(33, progress.directFeedCount),
],
];
}
@ -1100,6 +1094,7 @@
while (progress.mysteryDrugTarinaiIds.length < importedMysteryDrugCount) {
progress.mysteryDrugTarinaiIds.push(`imported:${progress.mysteryDrugTarinaiIds.length + 1}`);
}
progress.directFeedCount = Math.max(progress.directFeedCount, Math.min(33, Math.max(0, Math.floor(Number(packed[18] || 0) || 0))));
const progressChanged = previousProgress !== JSON.stringify(progress);
changed = changed || progressChanged;
@ -1127,7 +1122,9 @@
recordIntervention({ ...detail, world });
const item = detail.item || null;
if (item) {
if (!item._achievementPlayerPlaced || item._achievementPlayerPlacedCounted === false) world.achievementPlayerPlacedActiveCount = Math.max(0, Number(world.achievementPlayerPlacedActiveCount || 0) || 0) + 1;
item._achievementPlayerPlaced = true;
item._achievementPlayerPlacedCounted = true;
item._achievementPlacedAt = Math.max(0, Number(world.time || 0) || 0);
}
let changed = evaluateCleanFreak(world, detail);
@ -1226,7 +1223,7 @@
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");
const hasNest = playstyleItemCount(worldRef, "ant_nest") > 0;
if (!hasNest) return false;
const aliveTarinai = (worldRef.tarinai || []).some(tarinai => tarinai && !tarinai.dead);
if (aliveTarinai) return false;
@ -1439,7 +1436,7 @@
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);
const count = playstyleItemCount(world, "toilet");
if (count < 5) return false;
return unlock("sandbox_five_toilets", { ...detail, world, count });
}
@ -1500,11 +1497,30 @@
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;
worldRef.ensureItemBuckets?.("achievement-absolute-zero");
const testItem = (item) => {
if (!item || item.dead) return false;
const temperature = Number(worldRef.temperatureAt?.(item.x, item.y, null));
if (!Number.isFinite(temperature) || temperature >= ABSOLUTE_ZERO_C) continue;
if (!Number.isFinite(temperature) || temperature >= ABSOLUTE_ZERO_C) return false;
return unlock("below_absolute_zero_item", { ...detail, world: worldRef, item, temperature });
};
const ambient = Number(worldRef.currentTemperature);
if (Number.isFinite(ambient) && ambient < ABSOLUTE_ZERO_C) {
for (const bucket of worldRef.itemTypeBuckets?.values?.() || []) {
for (const item of bucket || []) if (testItem(item)) return true;
}
return false;
}
const seen = new Set();
for (const sourceType of ["dry_ice", "fan"]) {
for (const source of worldRef.itemsOfType?.(sourceType) || []) {
if (!source || source.dead) continue;
for (const item of worldRef.nearbyNonGrassItems?.(source.x, source.y, 360) || []) {
if (!item || item.dead || seen.has(item)) continue;
seen.add(item);
if (testItem(item)) return true;
}
}
}
return false;
}
@ -1527,8 +1543,10 @@
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;
const activeObjects = Math.max(0, Number(world.activeObjectCount?.() ?? (world.items || []).reduce((count, item) => count + (item && !item.dead ? 1 : 0), 0)) || 0);
const robotCount = Math.max(0, Number(world.itemCounts?.robot_cleaner || 0) || 0);
if (!activeObjects || robotCount !== activeObjects) return false;
const liveItems = world.itemsOfType?.("robot_cleaner") || [];
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 => {
@ -1706,7 +1724,6 @@
if (!world) return false;
runtimeByWorld.delete(world);
dangerFightDeathsByWorld.delete(world);
world.achievementDirectFeedCount = 0;
world.achievementRobotCleanCount = 0;
world.achievementEnemyAntKills = 0;
world.achievementLastTarinaiDamageAt = -Infinity;
@ -1734,6 +1751,14 @@
}
world.achievementNaturalSlaveGenerations = [];
world.achievementNaturalKingGenerations = [];
if (!state.unlocked.mad_scientist) {
const progress = ensureProgress();
if (progress.mysteryDrugTarinaiIds.length) {
progress.mysteryDrugTarinaiIds = [];
saveState();
renderIfDialogOpen();
}
}
return true;
}
@ -1852,9 +1877,11 @@
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);
const progress = ensureProgress();
progress.directFeedCount = Math.min(33, progress.directFeedCount + 1);
saveState();
renderIfDialogOpen();
if (world.achievementDirectFeedCount >= 33) return unlock("direct_feed_33", { ...detail, world, count: world.achievementDirectFeedCount });
if (progress.directFeedCount >= 33) return unlock("direct_feed_33", { ...detail, world, count: progress.directFeedCount });
return false;
}
@ -2009,7 +2036,7 @@
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);
const placedAlive = Math.max(0, Number(worldRef.achievementPlayerPlacedActiveCount ?? worldRef.achievementPlayerPlacementCount ?? 0) || 0);
if (placedAlive <= 10) unlock("minimalist_happy", { world: worldRef, mood: mood || worldRef.colonyMood, placedAlive });
}
if (!state.unlocked.colony_happy) {
@ -2027,7 +2054,7 @@
}
if (!state.unlocked.self_sufficient) {
if (alive.length >= 20 && !(worldRef.items || []).some(item => item && !item.dead && item.type === "duplicator")) {
if (alive.length >= 20 && playstyleItemCount(worldRef, "duplicator") === 0) {
let selfStart = Number(worldRef.achievementSelfSufficientStartAt);
if (!Number.isFinite(selfStart) || selfStart < 0 || selfStart > now) {
selfStart = now;
@ -2264,6 +2291,7 @@
evaluateWorld,
evaluateEvent,
recordSelfZunchiDeath(detail = {}) { return unlock("self_zunchi_death", detail); },
recordZunchiCollisionDeath(detail = {}) { return unlock("zunchi_collision_death", detail); },
recordSoccerBallDeath(detail = {}) { return unlock("soccer_ball_death", detail); },
});
global.TarinaiAchievements = api;