This commit is contained in:
33333-33333 2026-07-15 14:44:29 +09:00
commit 63e10af865
86 changed files with 5400 additions and 1209 deletions

View file

@ -0,0 +1,539 @@
"use strict";
const fs = require("fs");
const vm = require("vm");
const path = require("path");
const source = fs.readFileSync(path.join(__dirname, "..", "js", "achievements.js"), "utf8");
class ClassList {
constructor() { this.values = new Set(["hidden"]); }
add(...names) { names.forEach(name => this.values.add(name)); }
remove(...names) { names.forEach(name => this.values.delete(name)); }
contains(name) { return this.values.has(name); }
toggle(name, force) {
const next = force === undefined ? !this.values.has(name) : Boolean(force);
if (next) this.values.add(name); else this.values.delete(name);
return next;
}
}
class Element {
constructor(id = "", tagName = "div") {
this.tagName = String(tagName || "div").toUpperCase();
this.open = false;
this.id = id;
this.className = "";
this.classList = new ClassList();
this.dataset = {};
this.styles = new Map();
this.style = {
setProperty: (name, value) => this.styles.set(name, value),
removeProperty: name => this.styles.delete(name),
};
this.children = [];
this.textContent = "";
this.tabIndex = -1;
this.isConnected = true;
}
append(...children) { this.children.push(...children); }
replaceChildren(...children) { this.children = children; }
setAttribute() {}
addEventListener() {}
focus() {}
getBoundingClientRect() { return { left: 0, top: 0, width: 100, height: 30 }; }
get offsetWidth() { return 100; }
}
function createHarness(existingStorage = null, search = "", options = {}) {
const storage = existingStorage || new Map();
const elements = new Map();
const ids = [
"achievementsBtn", "achievementButtonCount", "achievementsDialog", "achievementDialogCount",
"achievementsCloseBtn", "achievementsCloseIconBtn", "achievementRefreshBtn", "achievementResetBtn", "achievementUnlockAllBtn",
"achievementList", "achievementSharedStatus", "achievementToast", "achievementToastTitle", "pauseBtn",
];
ids.forEach(id => elements.set(id, new Element(id)));
elements.get("achievementsDialog").classList.add("hidden");
elements.get("achievementToast").classList.add("hidden");
let timerId = 0;
const context = {
console, Date, Intl, Math, JSON, Object, Array, Map, Set, WeakMap, Promise,
Number, String, Boolean, Error, AbortController, encodeURIComponent, URLSearchParams,
location: { protocol: options.protocol || "file:", search },
crypto: { randomUUID: () => "11111111-1111-4111-8111-111111111111" },
localStorage: {
getItem(key) { return storage.has(key) ? storage.get(key) : null; },
setItem(key, value) { storage.set(key, String(value)); },
removeItem(key) { storage.delete(key); },
},
document: {
hidden: false,
getElementById(id) { return elements.get(id) || null; },
createElement(tagName) { return new Element("", tagName); },
addEventListener() {},
},
TarinaiEvents: { emit() {} },
showToast() {},
audio: { uiClick() {} },
setTimeout() { timerId += 1; return timerId; },
clearTimeout() {},
setInterval() { timerId += 1; return timerId; },
clearInterval() {},
fetch: options.fetch || (async () => { throw new Error("offline"); }),
TARINAI_VERSION: "39.16.56",
TarinaiGameDialogs: { confirm: async () => true },
};
context.window = context;
context.globalThis = context;
vm.createContext(context);
vm.runInContext(source, context, { filename: "achievements.js" });
return { context, api: context.TarinaiAchievements, storage, elements };
}
function assert(condition, message) { if (!condition) throw new Error(message); }
function unlocked(h, id) { return h.api.isUnlocked(id); }
const expectedIds = [
"first_birth", "natural_zunchi_slave", "natural_tarinai_king", "self_zunchi_death",
"natural_zunchi_slave_5_generations", "natural_tarinai_king_3_generations",
"death_50_in_10_seconds", "birth_50_in_60_seconds", "great_mother_1000_births", "lifespan_completed",
"soccer_ball_death", "fight_pair_danger_kill", "all_non_sleep_diseased_25", "colony_happy", "direct_feed_33",
"ignite_during_birth_ritual", "laxative_starvation", "idle_observer_5_minutes",
"placed_objects_100", "mechanized_industry", "safe_colony_25_5_minutes",
"ants_alive_25", "ants_killed_100", "secret_collection_9_slots",
"pause_spam_4_in_1_second", "continuous_play_1_hour", "below_absolute_zero_item",
"undo_mass_revival", "robot_cleaner_100", "held_30_seconds", "minimalist_happy",
"overprotective", "self_sufficient", "unplanned_city_30", "sauna_cold_plunge",
"rain_shelter_all", "medicine_ledger_all", "mercury_lifespan", "enemy_enemy_friend",
"revolution", "fuel_to_fire", "undo_20", "redo_20",
"eternal_history_generation_10", "king_full_satisfaction", "slave_zero_satisfaction", "town_doctor_50", "poke_plushie_fling",
"chaos_seeker_666_fights", "clean_freak_robot_only", "true_tarinai_observer",
];
(async function run() {
let h = createHarness();
let storage;
assert(h.api.definitions.length === 51, "definition count must be 51");
assert(JSON.stringify(h.api.definitions.map(d => d.id)) === JSON.stringify(expectedIds), "definition IDs mismatch");
const categorizedIds = h.api.categories.flatMap(category => category.ids);
assert(categorizedIds.length === expectedIds.length, "achievement category count mismatch");
assert(new Set(categorizedIds).size === expectedIds.length, "achievement category contains duplicates");
assert(expectedIds.every(id => categorizedIds.includes(id)), "achievement category misses an ID");
h.api.open();
const titles = Object.fromEntries(h.api.definitions.map(def => [def.id, def.title]));
assert(titles.natural_zunchi_slave === "ずんちどれい", "slave title mismatch");
assert(titles.natural_tarinai_king === "たりない王", "king title mismatch");
assert(titles.self_zunchi_death === "あぁ^~ たまらねぇぜ", "self-zunchi title mismatch");
assert(titles.natural_zunchi_slave_5_generations === "末代までの恥", "five-generation title mismatch");
assert(titles.natural_tarinai_king_3_generations === "華麗なる王族", "three-generation title mismatch");
assert(titles.death_50_in_10_seconds === "6番目の大量絶滅", "mass-extinction title mismatch");
assert(titles.birth_50_in_60_seconds === "ベビーブーム", "baby-boom title mismatch");
assert(titles.idle_observer_5_minutes === "監視カメラ", "observer title mismatch");
assert(titles.laxative_starvation === "内臓全部出た", "laxative title mismatch");
assert(titles.colony_happy === "管理された楽園", "happy colony title mismatch");
assert(titles.soccer_ball_death === "豆野、殺ッカーやろうぜ!", "soccer title mismatch");
assert(titles.ants_alive_25 === "アリの惑星", "ant planet title mismatch");
assert(titles.all_non_sleep_diseased_25 === "緊急事態宣言", "emergency title mismatch");
assert(titles.secret_collection_9_slots === "秘密のコレクション", "save-slot title mismatch");
assert(titles.pause_spam_4_in_1_second === "何やってるの?", "pause-spam title mismatch");
assert(titles.continuous_play_1_hour === "めっちゃたりない観察", "continuous-play title mismatch");
assert(titles.below_absolute_zero_item === "物理法則を下側に超越", "absolute-zero title mismatch");
assert(titles.ignite_during_birth_ritual === "燃え上がる恋", "ritual ignition title mismatch");
assert(titles.safe_colony_25_5_minutes === "安全第一", "safe colony title mismatch");
assert(titles.great_mother_1000_births === "大いなる母", "great mother title mismatch");
assert(titles.fight_pair_danger_kill === "喧嘩両成敗", "fight-pair title mismatch");
assert(titles.direct_feed_33 === "餌やりじいさん", "feeding title mismatch");
assert(titles.eternal_history_generation_10 === "悠久の歴史", "generation title mismatch");
assert(titles.king_full_satisfaction === "五体満足", "king effect title mismatch");
assert(titles.slave_zero_satisfaction === "零体満足", "slave effect title mismatch");
assert(titles.chaos_seeker_666_fights === "混沌ヲ希求スル者", "chaos seeker title mismatch");
assert(titles.clean_freak_robot_only === "潔癖症", "clean freak title mismatch");
assert(titles.true_tarinai_observer === "真・たりない観察", "completionist title mismatch");
const descriptions = Object.fromEntries(h.api.definitions.map(def => [def.id, def.description]));
assert(descriptions.natural_zunchi_slave === "ずんちどれいが出現する。", "slave description mismatch");
assert(descriptions.natural_tarinai_king === "たりない王が出現する。", "king description mismatch");
assert(descriptions.self_zunchi_death === "おしり鋲を抜いた時、自分のずんちで自爆する。", "self-zunchi description mismatch");
assert(descriptions.natural_zunchi_slave_5_generations === "5代連続でずんちどれい鎖なし", "five-generation description mismatch");
assert(descriptions.natural_tarinai_king_3_generations === "3代連続でたりない王王冠なし", "three-generation description mismatch");
assert(descriptions.great_mother_1000_births.includes("3333体"), "great mother description threshold mismatch");
assert(descriptions.robot_cleaner_100.includes("200個"), "cleaner description threshold mismatch");
assert(descriptions.minimalist_happy.includes("6日目以降"), "minimalist start-day description mismatch");
assert(descriptions.chaos_seeker_666_fights.includes("666回"), "chaos seeker description mismatch");
assert(!source.includes("TarinaiTooltips"), "achievement tooltips were not removed");
assert(source.includes("PRE_UNLOCK_DESCRIPTION_IDS"), "pre-unlock disclosure allowlist is missing");
function walk(node, out = []) { for (const child of node?.children || []) { out.push(child); walk(child, out); } return out; }
const cards = walk(h.elements.get("achievementList")).filter(node => node.dataset?.achievementId);
const firstBirthCard = cards.find(node => node.dataset.achievementId === "first_birth");
const disclosedSlaveCard = cards.find(node => node.dataset.achievementId === "natural_zunchi_slave");
const disclosedKingCard = cards.find(node => node.dataset.achievementId === "natural_tarinai_king");
const disclosedRevolutionCard = cards.find(node => node.dataset.achievementId === "revolution");
const disclosedLaxativeCard = cards.find(node => node.dataset.achievementId === "laxative_starvation");
const eternalHistoryCard = cards.find(node => node.dataset.achievementId === "eternal_history_generation_10");
const hiddenCard = cards.find(node => node.dataset.achievementId === "self_zunchi_death");
assert(firstBirthCard.children[1].children[0].children[1].textContent === descriptions.first_birth, "disclosed locked condition is not shown in the UI");
for (const card of [disclosedSlaveCard, disclosedKingCard, disclosedRevolutionCard, disclosedLaxativeCard, eternalHistoryCard]) {
assert(card.children[1].children[0].children[1].textContent !== "", "requested pre-unlock condition is hidden");
}
assert(hiddenCard.children[1].children[0].children[1].textContent === "", "undisclosed locked condition is not hidden as question marks");
const groups = h.elements.get("achievementList").children;
assert(groups.length === 4, "achievement UI must contain exactly four category groups");
assert(JSON.stringify(groups.map(group => group.dataset.achievementGroup)) === JSON.stringify(["ecology", "experiment", "construction", "operation"]), "achievement category order mismatch");
assert(groups.every(group => group.tagName === "DETAILS"), "achievement categories are not collapsible details");
const categoryTitles = h.api.categories.map(category => category.title);
assert(JSON.stringify(categoryTitles) === JSON.stringify(["生態", "実験", "建築", "その他"]), "achievement category titles mismatch");
h.api.unlock("natural_zunchi_slave");
const rerenderedGroups = h.elements.get("achievementList").children;
const ecologyCards = walk(rerenderedGroups[0]).filter(node => node.dataset?.achievementId);
const allRenderedCards = rerenderedGroups.flatMap(group => walk(group).filter(node => node.dataset?.achievementId));
assert(ecologyCards[0]?.dataset.achievementId === "natural_zunchi_slave", "unlocked achievement was not moved to the top of its category");
assert(allRenderedCards.filter(card => card.dataset.achievementId === "natural_zunchi_slave").length === 1, "unlocked achievement is duplicated across categories");
assert(await h.api.reset(), "in-game achievement reset confirmation was not accepted");
assert(!h.api.isUnlocked("natural_zunchi_slave"), "achievement reset did not clear unlocked state");
assert(source.includes('card.style.setProperty("--achievement-rate"'), "achievement percentage bar is not wired");
h = createHarness();
h.api.recordBirth({ world: { time: 0 } });
assert(unlocked(h, "first_birth"), "first birth did not unlock");
h = createHarness();
const birthWorld = { time: 0 };
for (let i = 0; i < 29; i += 1) { birthWorld.time = i; h.api.recordBirth({ world: birthWorld }); }
assert(!unlocked(h, "birth_50_in_60_seconds"), "rapid birth unlocked before 30");
birthWorld.time = 59;
h.api.recordBirth({ world: birthWorld });
assert(unlocked(h, "birth_50_in_60_seconds"), "rapid birth did not unlock at 30");
storage = new Map();
h = createHarness(storage);
for (let i = 0; i < 2000; i += 1) h.api.recordBirth({ world: { time: i % 61 } });
assert(!unlocked(h, "great_mother_1000_births"), "great mother unlocked before 3333");
h = createHarness(storage);
for (let i = 0; i < 1332; i += 1) h.api.recordBirth({ world: { time: i % 61 } });
assert(!unlocked(h, "great_mother_1000_births"), "great mother unlocked at 3332");
h.api.recordBirth({ world: { time: 1 } });
assert(unlocked(h, "great_mother_1000_births"), "great mother did not persist to 3333");
h = createHarness();
const deathWorld = { time: 0 };
for (let i = 0; i < 49; i += 1) { deathWorld.time = i * 0.19; h.api.recordDeath({ world: deathWorld, reason: "accident" }); }
assert(!unlocked(h, "death_50_in_10_seconds"), "rapid death unlocked before 50");
deathWorld.time = 9.5;
h.api.recordDeath({ world: deathWorld, reason: "accident" });
assert(unlocked(h, "death_50_in_10_seconds"), "rapid death did not unlock at 50");
h = createHarness();
h.api.recordDeath({ world: { time: 1 }, tarinai: {}, reason: "天寿を全うした" });
assert(unlocked(h, "lifespan_completed"), "lifespan did not unlock");
h = createHarness();
h.api.recordDeath({ world: { time: 1 }, tarinai: { _achievementLaxativeActiveThisFrame: true }, reason: "飢餓" });
assert(unlocked(h, "laxative_starvation"), "laxative starvation did not unlock");
h = createHarness();
const slaveWorld = {};
[1, 2, 4, 5, 6].forEach(generation => h.api.recordNaturalZunchiSlave({ world: slaveWorld, tarinai: { generation } }));
assert(!unlocked(h, "natural_zunchi_slave_5_generations"), "slave run ignored generation gap");
h.api.recordNaturalZunchiSlave({ world: slaveWorld, tarinai: { generation: 3 } });
assert(unlocked(h, "natural_zunchi_slave_5_generations"), "slave five-generation run did not unlock");
h = createHarness();
const kingWorld = {};
[4, 5, 6].forEach(generation => h.api.recordNaturalTarinaiKing({ world: kingWorld, tarinai: { generation } }));
assert(unlocked(h, "natural_tarinai_king_3_generations"), "king three-generation run did not unlock");
h = createHarness();
const feedWorldA = { time: 0, achievementDirectFeedCount: 0 };
for (let i = 0; i < 32; i += 1) h.api.recordDirectFeed({ world: feedWorldA });
assert(!unlocked(h, "direct_feed_33"), "direct feed unlocked before 33");
const feedWorldB = { time: 0, achievementDirectFeedCount: 0 };
h.api.recordDirectFeed({ world: feedWorldB });
assert(!unlocked(h, "direct_feed_33"), "direct-feed count survived a field reset");
h.api.recordDirectFeed({ world: feedWorldA });
assert(unlocked(h, "direct_feed_33"), "direct feed did not unlock at 33 in one field");
h = createHarness();
const diseased = Array.from({ length: 25 }, () => ({ fightDisease: true, dead: false }));
h.api.evaluateWorld({ tarinai: diseased, ants: [], time: 0 }, { id: "relaxed" });
assert(unlocked(h, "all_non_sleep_diseased_25"), "disease colony did not unlock");
h = createHarness();
const happyWorld = { tarinai: [{ dead: false }], ants: [], items: [], time: 600, day: 6 };
h.api.evaluateWorld(happyWorld, { id: "happy" });
happyWorld.time = 719.99;
h.api.evaluateWorld(happyWorld, { id: "happy" });
assert(!unlocked(h, "colony_happy"), "happy colony unlocked before one day");
happyWorld.time = 720;
h.api.evaluateWorld(happyWorld, { id: "happy" });
assert(unlocked(h, "colony_happy"), "happy colony did not unlock after one day from day 6");
h = createHarness();
const interruptedHappyWorld = { tarinai: [{ dead: false }], ants: [], items: [], time: 600, day: 6 };
h.api.evaluateWorld(interruptedHappyWorld, { id: "happy" });
interruptedHappyWorld.time = 650;
h.api.evaluateWorld(interruptedHappyWorld, { id: "normal" });
interruptedHappyWorld.time = 700;
h.api.evaluateWorld(interruptedHappyWorld, { id: "happy" });
interruptedHappyWorld.time = 819.99;
h.api.evaluateWorld(interruptedHappyWorld, { id: "happy" });
assert(!unlocked(h, "colony_happy"), "interrupted happiness did not restart the one-day timer");
interruptedHappyWorld.time = 820;
h.api.evaluateWorld(interruptedHappyWorld, { id: "happy" });
assert(unlocked(h, "colony_happy"), "happy colony did not unlock one full day after restart");
h = createHarness();
h.api.recordIgnition({});
h.api.recordSelfZunchiDeath({});
h.api.recordSoccerBallDeath({});
assert(unlocked(h, "ignite_during_birth_ritual"), "ritual ignition failed");
assert(unlocked(h, "self_zunchi_death"), "self zunchi failed");
assert(unlocked(h, "soccer_ball_death"), "soccer death failed");
h = createHarness();
const dangerItem = { id: "danger-1", type: "firecracker", roles: { danger: true } };
const fighterA = { id: "a", dead: true, fightTimer: 2, fightTargetId: "b", fightTargetIds: ["b"] };
const fighterB = { id: "b", dead: false, fightTimer: 2, fightTargetId: "a", fightTargetIds: ["a"] };
const fightWorld = {
time: 5,
tarinai: [fighterA, fighterB],
fightPairKey(a, b) { return [a.id, b.id].sort().join(":"); },
isAlreadyFightingPair() { return true; },
};
fighterA.world = fightWorld; fighterB.world = fightWorld;
h.api.recordDamageSource(fighterA, dangerItem, { world: fightWorld });
h.api.recordDeath({ world: fightWorld, tarinai: fighterA, reason: "爆竹", fromDamage: true });
assert(!unlocked(h, "fight_pair_danger_kill"), "fight pair unlocked after only one death");
fightWorld.time = 6;
fighterB.dead = true; fighterB.fightTimer = 0; fighterB.fightTargetId = null; fighterB.fightTargetIds = [];
h.api.recordDamageSource(fighterB, dangerItem, { world: fightWorld });
h.api.recordDeath({ world: fightWorld, tarinai: fighterB, reason: "爆竹", fromDamage: true });
assert(unlocked(h, "fight_pair_danger_kill"), "fight pair danger kill did not unlock");
h = createHarness();
const falsePositiveA = { id: "fa", dead: true, fightTimer: 2, fightTargetId: "fb", fightTargetIds: ["fb"] };
const falsePositiveB = { id: "fb", dead: false, fightTimer: 2, fightTargetId: "fa", fightTargetIds: ["fa"] };
const falsePositiveWorld = {
time: 3,
tarinai: [falsePositiveA, falsePositiveB],
fightPairKey(a, b) { return [a.id, b.id].sort().join(":"); },
isAlreadyFightingPair() { return true; },
};
falsePositiveA.world = falsePositiveWorld; falsePositiveB.world = falsePositiveWorld;
h.api.recordDamageSource(falsePositiveA, dangerItem, { world: falsePositiveWorld });
h.api.recordDeath({ world: falsePositiveWorld, tarinai: falsePositiveA, reason: "飢餓", fromDamage: false });
falsePositiveWorld.time = 4;
falsePositiveB.dead = true;
h.api.recordDamageSource(falsePositiveB, dangerItem, { world: falsePositiveWorld });
h.api.recordDeath({ world: falsePositiveWorld, tarinai: falsePositiveB, reason: "爆竹", fromDamage: true });
assert(!unlocked(h, "fight_pair_danger_kill"), "non-lethal danger damage was misclassified as a danger kill");
h = createHarness();
const generationWorld = { time: 0, maxGeneration: 9 };
h.api.recordBirth({ world: generationWorld, child: { generation: 9, world: generationWorld } });
assert(!unlocked(h, "eternal_history_generation_10"), "generation history unlocked before generation 10");
generationWorld.maxGeneration = 10;
h.api.recordBirth({ world: generationWorld, child: { generation: 10, world: generationWorld } });
assert(unlocked(h, "eternal_history_generation_10"), "generation history did not unlock at generation 10");
h = createHarness();
const king = { dead: false, isTarinaiChampion: true, isZunchiSlave: false, powerItemMode: "protein", sizeItemMode: "", world: {} };
h.api.recordConsumableUse({ tarinai: king, world: king.world, type: "protein" });
assert(!unlocked(h, "king_full_satisfaction"), "king satisfaction unlocked without giant drug");
king.sizeItemMode = "giant_drug";
h.api.recordConsumableUse({ tarinai: king, world: king.world, type: "giant_drug" });
assert(unlocked(h, "king_full_satisfaction"), "king satisfaction did not unlock with both effects");
h = createHarness();
const slave = { dead: false, isTarinaiChampion: false, isZunchiSlave: true, powerItemMode: "niteropu", sizeItemMode: "", world: {} };
h.api.recordConsumableUse({ tarinai: slave, world: slave.world, type: "niteropu" });
assert(!unlocked(h, "slave_zero_satisfaction"), "slave satisfaction unlocked without dwarf drug");
slave.sizeItemMode = "dwarf_drug";
h.api.recordConsumableUse({ tarinai: slave, world: slave.world, type: "dwarf_drug" });
assert(unlocked(h, "slave_zero_satisfaction"), "slave satisfaction did not unlock with both effects");
h = createHarness();
const allExceptCompletionist = expectedIds.filter(id => id !== "true_tarinai_observer");
for (let i = 0; i < allExceptCompletionist.length - 1; i += 1) h.api.unlock(allExceptCompletionist[i]);
assert(!unlocked(h, "true_tarinai_observer"), "completionist unlocked before every other achievement");
h.api.unlock(allExceptCompletionist[allExceptCompletionist.length - 1]);
assert(unlocked(h, "true_tarinai_observer"), "completionist did not unlock with every other achievement");
h = createHarness();
const idleWorld = { time: 300, tarinai: [], ants: [], achievementLastInterventionAt: 0, achievementNoDeathStartAt: -1 };
h.api.evaluateWorld(idleWorld, { id: "relaxed" });
assert(unlocked(h, "idle_observer_5_minutes"), "idle observer did not unlock");
storage = new Map();
h = createHarness(storage);
for (let i = 0; i < 60; i += 1) h.api.recordPlayerPlacement({ world: { time: 1 }, item: { type: "grass" } });
assert(!unlocked(h, "placed_objects_100"), "placement unlocked before 100");
h = createHarness(storage);
for (let i = 0; i < 39; i += 1) h.api.recordPlayerPlacement({ world: { time: 1 }, item: { type: "grass" } });
assert(!unlocked(h, "placed_objects_100"), "placement unlocked at 99");
h.api.recordPlayerPlacement({ world: { time: 1 }, item: { type: "grass" } });
assert(unlocked(h, "placed_objects_100"), "placement did not persist across field reset/reload to 100");
storage = new Map();
h = createHarness(storage);
h.api.recordLinkPlaced("wire", { world: { time: 1 }, item: { type: "wire" } });
h.api.recordLinkPlaced("insulated_wire", { world: { time: 2 }, item: { type: "insulated_wire" } });
const wireProgress = JSON.parse(storage.get("tarinai_achievements_v2")).progress;
assert(wireProgress.placementCount === 2, "wire placements were not counted toward placed objects");
assert(wireProgress.linkTypes.length === 0, "wire placements incorrectly advanced mechanized link types");
h = createHarness();
h.api.recordLinkPlaced("rope", { world: { time: 1 } });
h.api.recordLinkPlaced("rod", { world: { time: 1 } });
h.api.recordLinkPlaced("spring", { world: { time: 1 } });
assert(!unlocked(h, "mechanized_industry"), "mechanized industry unlocked without signal");
h.api.recordSignalActivation({ world: {}, target: { type: "gate_fence" } });
assert(unlocked(h, "mechanized_industry"), "mechanized industry did not unlock after logical AND");
assert(!unlocked(h, "signal_automation_first") && !unlocked(h, "link_craftsman"), "removed achievements remain accessible");
h = createHarness();
const safeAlive = Array.from({ length: 25 }, () => ({ dead: false }));
const safeWorld = { time: 0, tarinai: safeAlive, ants: [], achievementLastInterventionAt: 0, achievementNoDeathStartAt: -1 };
h.api.evaluateWorld(safeWorld, { id: "relaxed" });
safeWorld.time = 300;
h.api.evaluateWorld(safeWorld, { id: "relaxed" });
assert(unlocked(h, "safe_colony_25_5_minutes"), "safe colony did not unlock");
h = createHarness();
const antWorld = { time: 1, tarinai: [], ants: Array.from({ length: 25 }, () => ({ dead: false })), achievementLastInterventionAt: 0, achievementNoDeathStartAt: -1 };
h.api.evaluateWorld(antWorld, { id: "relaxed" });
assert(unlocked(h, "ants_alive_25"), "ant herd did not unlock at 25");
storage = new Map();
h = createHarness(storage);
for (let i = 0; i < 65; i += 1) h.api.recordAntKilled({ world: {} });
assert(!unlocked(h, "ants_killed_100"), "ant extermination unlocked before 100");
h = createHarness(storage);
for (let i = 0; i < 34; i += 1) h.api.recordAntKilled({ world: {} });
assert(!unlocked(h, "ants_killed_100"), "ant extermination unlocked at 99");
h.api.recordAntKilled({ world: {} });
assert(unlocked(h, "ants_killed_100"), "ant extermination did not persist to 100");
h = createHarness();
h.api.recordSaveSlotsFilled({ filled: 8 });
assert(!unlocked(h, "secret_collection_9_slots"), "save-slot collection unlocked before 9");
h.api.recordSaveSlotsFilled({ filled: 9 });
assert(unlocked(h, "secret_collection_9_slots"), "save-slot collection did not unlock at 9");
h = createHarness();
[0, 250, 500].forEach(now => h.api.recordPauseClick({ now }));
assert(!unlocked(h, "pause_spam_4_in_1_second"), "pause-spam unlocked before four clicks");
h.api.recordPauseClick({ now: 999 });
assert(unlocked(h, "pause_spam_4_in_1_second"), "pause-spam did not unlock within one second");
h = createHarness();
[0, 400, 800, 1201].forEach(now => h.api.recordPauseClick({ now }));
assert(!unlocked(h, "pause_spam_4_in_1_second"), "pause-spam unlocked outside one-second window");
h = createHarness();
h.api.recordContinuousPlay({ elapsedMs: 3599999 });
assert(!unlocked(h, "continuous_play_1_hour"), "continuous play unlocked before one hour");
h.api.recordContinuousPlay({ elapsedMs: 3600000 });
assert(unlocked(h, "continuous_play_1_hour"), "continuous play did not unlock at one hour");
h = createHarness();
const thermalWorld = {
time: 0, tarinai: [], ants: [], items: [{ id: "cold", x: 10, y: 20, dead: false }],
temperatureAt() { return -273.15; }, achievementLastInterventionAt: 0, achievementNoDeathStartAt: -1,
};
h.api.evaluateWorld(thermalWorld, { id: "relaxed" });
assert(!unlocked(h, "below_absolute_zero_item"), "absolute-zero achievement unlocked at exactly absolute zero");
thermalWorld.temperatureAt = () => -273.16;
h.api.evaluateWorld(thermalWorld, { id: "relaxed" });
assert(unlocked(h, "below_absolute_zero_item"), "absolute-zero achievement did not unlock below absolute zero");
storage = new Map();
storage.set("tarinai_achievements_v2", JSON.stringify({
unlocked: { signal_automation_first: 1000, link_craftsman: 2000 },
pending: ["signal_automation_first", "link_craftsman"],
progress: { linkTypes: ["rope", "rod", "spring"] },
}));
h = createHarness(storage);
assert(unlocked(h, "mechanized_industry"), "legacy AND achievements were not migrated");
storage = new Map();
storage.set("tarinai_achievements_v2", JSON.stringify({
unlocked: { link_craftsman: 2000 },
pending: [],
progress: {},
}));
h = createHarness(storage);
h.api.recordSignalActivation({ world: {}, target: { type: "gate_fence" } });
assert(unlocked(h, "mechanized_industry"), "legacy link-craftsman progress was not reconstructed before a later signal");
storage = new Map();
h = createHarness(storage, "?debug=1");
assert(h.elements.get("achievementUnlockAllBtn").hidden === false, "debug all-achievements button was not shown");
h.api.debugUnlockAll();
assert(h.api.definitions.every(def => unlocked(h, def.id)), "debug all-achievements button did not unlock all");
const debugState = JSON.parse(storage.get("tarinai_achievements_v2"));
assert(debugState.pending.length === 0, "debug unlocks were queued for shared statistics");
assert(debugState.debugUnlocked.length === h.api.definitions.length, "debug unlocks were not marked as local-only");
h = createHarness(storage, "");
assert(h.api.definitions.every(def => !unlocked(h, def.id)), "debug-only unlocks leaked into normal play");
storage = new Map();
storage.set("tarinai_achievements_v2", JSON.stringify({
unlocked: { first_birth: 1700000000123, lifespan_completed: 1700000000456 },
pending: [],
progress: {},
}));
const recoveryRequests = [];
h = createHarness(storage, "", {
protocol: "https:",
fetch: async (_url, options = {}) => {
recoveryRequests.push(JSON.parse(options.body || "{}"));
return { ok: true, async json() { return { ok: true, totalPlayers: 1, achievements: {} }; } };
},
});
await h.api.refresh();
assert(recoveryRequests.length === 1, "recovery sync did not issue exactly one request");
assert(recoveryRequests[0].action === "sync", "recovery request did not use bulk sync");
assert(recoveryRequests[0].unlocked.first_birth === 1700000000123, "recovery sync omitted first birth timestamp");
assert(recoveryRequests[0].unlocked.lifespan_completed === 1700000000456, "recovery sync omitted lifespan timestamp");
h = createHarness();
for (let i = 0; i < 49; i += 1) h.api.recordFirstAidRecovery({ recovered: 10 });
assert(!unlocked(h, "town_doctor_50"), "doctor unlocked before 50 recoveries");
h.api.recordFirstAidRecovery({ recovered: 0 });
assert(!unlocked(h, "town_doctor_50"), "zero recovery counted for doctor");
h.api.recordFirstAidRecovery({ recovered: 1 });
assert(unlocked(h, "town_doctor_50"), "doctor did not unlock at 50 recoveries");
h = createHarness();
h.api.recordPlushiePokeFling({});
assert(unlocked(h, "poke_plushie_fling"), "plushie poke achievement did not unlock");
storage = new Map();
h = createHarness(storage);
const chaosWorld = { time: 0 };
for (let i = 0; i < 400; i += 1) h.api.recordFightStarted({ world: chaosWorld, a: {}, b: {} });
h = createHarness(storage);
for (let i = 0; i < 265; i += 1) h.api.recordFightStarted({ world: chaosWorld, a: {}, b: {} });
assert(!unlocked(h, "chaos_seeker_666_fights"), "chaos seeker unlocked at 665 fights");
h.api.recordFightStarted({ world: chaosWorld, a: {}, b: {} });
assert(unlocked(h, "chaos_seeker_666_fights"), "chaos seeker did not persist to 666 fights");
h = createHarness();
const cleaner = { type: "robot_cleaner", dead: false, robotCleanerMask: 0x3f, robotCleanerHighSpeed: true, _signalDriven: true, _signalOn: false };
const cleanWorld = { items: [cleaner], tarinai: [], ants: [], time: 0 };
h.api.evaluateCleanFreak(cleanWorld);
assert(!unlocked(h, "clean_freak_robot_only"), "clean freak unlocked while signal-powered cleaner was off");
cleaner._signalOn = true;
cleanWorld.tarinai.push({ dead: false });
h.api.evaluateCleanFreak(cleanWorld);
assert(!unlocked(h, "clean_freak_robot_only"), "clean freak ignored a living tarinai");
cleanWorld.tarinai.length = 0;
cleanWorld.items.push({ type: "grass", dead: false });
h.api.evaluateCleanFreak(cleanWorld);
assert(!unlocked(h, "clean_freak_robot_only"), "clean freak ignored another field item");
cleanWorld.items.pop();
h.api.evaluateCleanFreak(cleanWorld);
assert(unlocked(h, "clean_freak_robot_only"), "clean freak did not unlock with all settings and robot-only field");
console.log("[OK] all 51 achievement definitions, categorized UI, inline disclosures, and recovery synchronization passed");
})().catch(error => {
console.error(error);
process.exitCode = 1;
});

View file

@ -0,0 +1,106 @@
#!/usr/bin/env python3
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parents[1]
def read(name):
return (ROOT / name).read_text()
def require(condition, message):
if not condition:
raise AssertionError(message)
ach = read('js/achievements.js')
api = read('achievement_api.php')
css = read('css/components.css')
ids_js = re.findall(r'id: "([a-z0-9_]+)"', ach.split('const DEFINITIONS',1)[1].split(']);',1)[0])
ids_php = re.findall(r"'([a-z0-9_]+)'", api.split('const ACHIEVEMENT_IDS',1)[1].split('];',1)[0])
require(len(ids_js) == 51, 'JS achievement count is not 51')
require(ids_js == ids_php, 'JS/PHP achievement IDs are not synchronized')
require('signal_automation_first' not in ids_js and 'link_craftsman' not in ids_js, 'retired achievements remain defined')
require('mechanized_industry' in ids_js, 'mechanized industry is missing')
require('placed_objects_100' in ids_js and 'ants_killed_100' in ids_js, '100-count achievements are missing')
require('great_mother_1000_births' in ids_js and 'fight_pair_danger_kill' in ids_js, 'previous new achievements are missing')
require(all(item in ids_js for item in ['secret_collection_9_slots','pause_spam_4_in_1_second','continuous_play_1_hour','below_absolute_zero_item']), 'previous achievement IDs are missing')
require(all(item in ids_js for item in ['eternal_history_generation_10','king_full_satisfaction','slave_zero_satisfaction','town_doctor_50','poke_plushie_fling','chaos_seeker_666_fights','clean_freak_robot_only','true_tarinai_observer']), 'v56 achievement IDs are missing')
require(all(item in ids_js for item in ['undo_mass_revival','robot_cleaner_100','held_30_seconds','minimalist_happy','overprotective','self_sufficient','unplanned_city_30','sauna_cold_plunge','rain_shelter_all','medicine_ledger_all','mercury_lifespan','enemy_enemy_friend','revolution','fuel_to_fire','undo_20','redo_20']), 'playstyle achievement IDs are missing')
checks = {
'birth event': 'recordBirth?.({ world: this, child, parents: [a, b] })' in read('js/world_family_social.js'),
'death event after final reason': 'recordDeath?.({ world: this.world, tarinai: this, reason: finalReason, fromDamage: Boolean(opts.fromDamage), achievementDangerKill: Boolean(opts.achievementDangerKill) })' in read('js/tarinai_social_move_life.js'),
'natural slave event only in fight-record path': 'recordNaturalZunchiSlave?.({ world: this, tarinai, other, wins, losses, total, winRate })' in read('js/world_combat_effects.js'),
'natural king event only in fight-record path': 'recordNaturalTarinaiKing?.({ world: this, tarinai, other, wins, losses, total, winRate })' in read('js/world_combat_effects.js'),
'direct feed is food-only': 'if (foodGift) {' in read('js/tarinai_direct_feeding_system.js') and 'recordDirectFeed?.' in read('js/tarinai_direct_feeding_system.js'),
'ritual ignition checks state before mutation': 'const ignitedDuringBirthRitual = !already' in read('js/tarinai_disease_nest.js'),
'soccer unlock follows fatal ball impact': 'if (t.dead && ball.type === "ball")' in read('js/collision_response_system.js'),
'self-zunchi unlock checks owner and never-stopped': 'item.achievementBurstOwnerId === t.id && item.achievementBurstNeverStopped !== false' in read('js/item_dynamic_zunchi_system.js'),
'held launched zunchi is disqualified': 'if (item.playerHeld || item._heldByPlayer)' in read('js/item_dynamic_zunchi_system.js') and 'target.achievementBurstNeverStopped = false' in read('js/ui_input_shared.js'),
'generation runs persist in saves': 'achievementNaturalSlaveGenerations' in read('js/snapshot_system.js') and 'achievementNaturalKingGenerations' in read('js/snapshot_system.js'),
'direct-feed field progress persists in saves': 'achievementDirectFeedCount' in read('js/snapshot_system.js'),
'direct-feed field progress resets with world': 'this.achievementDirectFeedCount = 0;' in read('js/world.js'),
'simultaneous toast queue exists': 'let toastQueue = [];' in ach and 'runNextUnlockToast' in ach,
'achievement toast is top-left': 'top: 18px;' in css and 'left: 18px;' in css and 'right: auto;' in css,
'laxative frame-edge marker exists': '_achievementLaxativeActiveThisFrame' in read('js/tarinai_update_step_frame.js') and '_achievementLaxativeActiveThisFrame' in ach,
'disease condition excludes sleep-only': 't.zunchiDisease || t.explosionDisease || t.fightDisease' in ach,
'locked condition display policy': 'lockedDescriptionText' in ach and '\\uff1f\\uff1f\\uff1f' in ach and 'PRE_UNLOCK_DESCRIPTION_IDS' in ach,
'condition shown beside title': 'heading.append(title, description);' in ach,
'achievement tooltips removed': 'TarinaiTooltips' not in ach and 'dataset.tip' not in ach and 'achievementTooltipText' not in ach,
'percentage background bar including unlocked cards': '--achievement-rate' in ach and '.achievement-entry::before' in css and 'width: var(--achievement-rate);' in css and '.achievement-entry > *' in css and 'z-index: 0;' in css,
'achievement category folds': 'ACHIEVEMENT_CATEGORIES' in ach and 'createAchievementGroup' in ach and 'achievement-group' in css,
'category-local unlocked ordering': 'orderedDefinitions = [...unlockedDefinitions, ...lockedDefinitions]' in ach and 'id: "completed"' not in ach,
'requested pre-unlock disclosures': all(item in ach.split('const PRE_UNLOCK_DESCRIPTION_IDS', 1)[1].split(']);', 1)[0] for item in ['natural_zunchi_slave','natural_tarinai_king','revolution','laxative_starvation']),
'native browser confirmations removed': not re.search(r'\b(?:alert|confirm|prompt)\s*\(', ach + read('js/save_system.js')) and 'TarinaiGameDialogs' in ach and 'TarinaiGameDialogs' in read('js/save_system.js'),
'achiever count beside percentage': 'unlockedPlayers' in ach and 'sharedAchievementText' in ach and '\\u9054\\u6210\\u8005' in ach,
'persistent placement count': 'progress.placementCount' in ach and 'unlock("placed_objects_100"' in ach,
'mechanized logical AND': 'progress.signalActivated && hasAllLinks' in ach,
'persistent ant kill count': 'progress.antKills' in ach and 'unlock("ants_killed_100"' in ach,
'persistent reproduction count': 'progress.birthCount' in ach and 'progress.birthCount >= 3333' in ach and 'unlock("great_mother_1000_births"' in ach,
'danger damage source pipeline': 'recordDamageSource' in ach and 'options.source' in read('js/health.js') and '{ source: it }' in read('js/world_combat_effects.js') and 'type: "shoot"' in read('js/world_tool_actions.js') and '_achievementDangerSource = pin' in read('js/world_combat_effects.js') and 'recordDamageSource?.(target, home' in read('js/world_ants_system.js'),
'fight-pair danger tracker': 'recordFightPairDangerDeath' in ach and 'fight_pair_danger_kill' in ach and 'detail.fromDamage || detail.achievementDangerKill' in ach,
'debug unlock-all button': 'achievementUnlockAllBtn' in read('index.html') and 'unlockAllAchievementsForDebug' in ach and 'DEBUG_MODE' in ach and 'debugUnlocked' in ach,
'idle intervention hooks': 'recordIntervention?.({ world: this, tool: "poke"' in read('js/world_placement_log.js') and 'recordIntervention?.({ world, tool: "pinch"' in read('js/ui_input_shared.js') and 'recordIntervention?.({ world: this, tool: "shoot"' in read('js/world_tool_actions.js'),
'link placement hook': 'recordLinkPlaced?.(constraintType' in read('js/world_tool_actions.js') and 'insulated_wire' in ach and 'recordPlayerPlacement(detail);' in ach,
'signal activation hook': 'recordSignalActivation?.({ world: worldRef' in read('js/signal_system.js'),
'ant kill hook': 'recordAntKilled?.({ world: this.world, ant: this' in read('js/ants.js'),
'ant population spawn hook': read('js/world_ants_system.js').count('recordAntPopulation?.(this)') >= 2,
'ant herd threshold': 'if (aliveAnts >= 25)' in ach,
'safe colony timer': 'now - safeStart >= 300' in ach,
'idle observer timer': 'now - lastInterventionAt >= 300' in ach,
'server legacy AND migration': "unset($unlocks['signal_automation_first'], $unlocks['link_craftsman'])" in api and "mechanized_industry" in api,
'automatic aggregate recovery sync': 'localUnlockSnapshot' in ach and 'action: "sync"' in ach and "['session', 'unlock', 'sync', 'reset', 'summary']" in api and "$action === 'sync'" in api,
'save-slot collection hook': 'recordSaveSlotsFilled' in ach and 'recordSaveSlotsFilled?.' in read('js/save_storage.js'),
'pause-spam hook': 'pauseButton' in ach and 'recordPauseClick' in ach and 'pauseBtn' in read('index.html'),
'continuous play heartbeat': 'CONTINUOUS_PLAY_TARGET_MS' in ach and 'recordContinuousPlay' in ach and 'visibilitychange' in ach,
'absolute-zero item evaluation': 'ABSOLUTE_ZERO_C' in ach and 'recordBelowAbsoluteZeroItem(worldRef)' in ach and 'temperatureAt?.(item.x, item.y, null)' in ach,
'recovery sync is additive': "if (isset($state['unlocks'][$achievementId][$playerId])) continue;" in api,
'debug unlocks excluded from recovery': 'state.debugUnlocked?.includes?.(definition.id)' in ach,
'hidden achievement UI skips list rebuild': 'dom.dialog?.classList.contains("hidden")' in ach and 'achievementListSignature' in ach,
'achievement category definitions are cached': 'CATEGORY_DEFINITIONS' in ach,
'achievement sync is debounced and pending-only': 'SYNC_DEBOUNCE_MS' in ach and 'schedulePendingSync' in ach and 'pendingIds' in ach,
'completionist excludes itself': 'definition.id === COMPLETIONIST_ID' in ach and 'allOtherAchievementsUnlocked' in ach,
'generation ten is event-driven': 'generation >= GENERATION_TARGET' in ach and 'eternal_history_generation_10' in ach,
'status effect combinations are event-driven': 'evaluateStatusEffectAchievements' in ach and 'king_full_satisfaction' in ach and 'slave_zero_satisfaction' in ach,
'doctor counts actual recovery only': 'recordFirstAidRecovery' in ach and 'recovered > 0.0001' in read('js/item_effect_definitions.js'),
'plushie poke uses shared target and overlay': 'findPokeTargetAt' in read('js/world_placement_log.js') and 'hoverPokeTarget' in read('js/ui_input_shared.js') and 'recordPlushiePokeFling' in ach,
'persistent chaos fight counter': 'progress.fightCount' in ach and 'CHAOS_FIGHT_TARGET = 666' in ach and 'recordFightStarted' in ach,
'clean-freak robot-only evaluation': 'evaluateCleanFreak' in ach and 'ROBOT_ALL_TARGET_MASK = 0x3f' in ach and 'robotCleanerHighSpeed' in ach and 'signal-power' in read('js/signal_system.js'),
'medicine ledger excludes chain and crown': 'slave_chain' not in ach.split('const MEDICINE_LEDGER_TYPES', 1)[1].split(']);', 1)[0] and 'golden_crown' not in ach.split('const MEDICINE_LEDGER_TYPES', 1)[1].split(']);', 1)[0],
'cleaner target raised to 200': 'ROBOT_CLEAN_TARGET = 200' in ach and r'\u7d2f\u8a08200\u500b' in ach,
'minimalist starts on day six': 'day >= 6 && !state.unlocked.minimalist_happy' in ach,
'managed paradise starts day six': 'day >= 6' in ach and 'now - happyStart >= dayLength' in ach,
'compact server state v2': 'function compact_state' in api and "['v' => 2, 'p' => $players, 'u' => $unlocks]" in api,
}
for name, ok in checks.items():
require(ok, name)
social = read('js/world_family_social.js') + read('js/tarinai_social_action_runtime.js')
require('!!a.isZunchiSlave !== !!b.isZunchiSlave' in social or '!!t.isZunchiSlave === !!other.isZunchiSlave' in social, 'slave mating rule not found')
require('recordNaturalGeneration(world, tarinai, "achievementNaturalSlaveGenerations", 5)' in ach, 'slave achievement still uses impossible direct-line logic')
require('\\u52dd\\u738730%\\u4ee5\\u4e0b' in read('js/world_combat_effects.js'), 'slave fight-record text does not state the 30% threshold')
require('legacyLinkTypes' in ach, 'legacy link-craftsman partial migration is missing')
print('[OK] achievement integration audit passed')
for name in checks:
print(f'[OK] {name}')

View file

@ -0,0 +1,58 @@
"use strict";
const fs = require("fs");
const path = require("path");
const vm = require("vm");
const root = path.join(__dirname, "..");
const context = {
console,
Date,
Math,
JSON,
Object,
Array,
Map,
Set,
Uint8Array,
TextEncoder,
TextDecoder,
URLSearchParams,
performance,
location: { search: "" },
CompressionStream: undefined,
DecompressionStream: undefined,
TarinaiSnapshot: { version: 35 },
TarinaiSaveSchema: {
BINARY_SCHEMA_VERSION: 41,
FIELD_IDS: ["garden"],
itemTypeValue(_id, fallback = "") { return fallback; },
},
};
context.globalThis = context;
context.window = context;
vm.createContext(context);
vm.runInContext(fs.readFileSync(path.join(root, "js", "save_codec.js"), "utf8"), context, { filename: "save_codec.js" });
(async () => {
const metadata = {
w: [[1, 2], [3], 12, 87, 6, 145.5, 20, 18, 100, 90],
t: [[10, 3, 120, 130]],
i: [[1, 121]],
};
const snapshot = {
v: 35,
a: "tj1",
m: [1, 0, "garden"],
w: [0, 0, 0, 120, 0, -9990, 1, 0, "seed", 0, 0, 0],
t: [],
i: [],
g: metadata,
};
const encoded = await context.TarinaiSaveCodec.encodeSnapshot(snapshot);
const decoded = await context.TarinaiSaveCodec.decodeSnapshot(encoded);
if (JSON.stringify(decoded.g) !== JSON.stringify(metadata)) throw new Error("achievement metadata changed during binary save roundtrip");
console.log("[OK] achievement metadata survives current binary save roundtrip");
})().catch(error => {
console.error(error);
process.exitCode = 1;
});

View file

@ -0,0 +1,186 @@
"use strict";
const fs = require("fs");
const vm = require("vm");
const path = require("path");
const root = path.join(__dirname, "..");
const source = fs.readFileSync(path.join(root, "js", "achievements.js"), "utf8");
class ClassList {
constructor() { this.values = new Set(["hidden"]); }
add(...names) { names.forEach(name => this.values.add(name)); }
remove(...names) { names.forEach(name => this.values.delete(name)); }
contains(name) { return this.values.has(name); }
toggle(name, force) { const next = force === undefined ? !this.values.has(name) : Boolean(force); if (next) this.values.add(name); else this.values.delete(name); return next; }
}
class Element {
constructor(id = "") { this.id = id; this.className = ""; this.classList = new ClassList(); this.dataset = {}; this.styles = new Map(); this.style = { setProperty: (n,v)=>this.styles.set(n,v), removeProperty:n=>this.styles.delete(n) }; this.children=[]; this.textContent=""; this.tabIndex=-1; this.isConnected=true; this.hidden=true; }
append(...children){this.children.push(...children);} replaceChildren(...children){this.children=children;} setAttribute(){} addEventListener(){} focus(){} getBoundingClientRect(){return{left:0,top:0,width:100,height:30};} get offsetWidth(){return 100;}
}
function harness(storage = new Map()) {
const elements = new Map();
["achievementsBtn","achievementButtonCount","achievementsDialog","achievementDialogCount","achievementsCloseBtn","achievementsCloseIconBtn","achievementRefreshBtn","achievementResetBtn","achievementUnlockAllBtn","achievementList","achievementSharedStatus","achievementToast","achievementToastTitle","pauseBtn"].forEach(id=>elements.set(id,new Element(id)));
let timer=0;
const c={console,Date,Intl,Math,JSON,Object,Array,Map,Set,WeakMap,Promise,Number,String,Boolean,Error,AbortController,encodeURIComponent,URLSearchParams,
location:{protocol:"file:",search:""},crypto:{randomUUID:()=>"11111111-1111-4111-8111-111111111111"},
localStorage:{getItem:k=>storage.has(k)?storage.get(k):null,setItem:(k,v)=>storage.set(k,String(v)),removeItem:k=>storage.delete(k)},
document:{hidden:false,getElementById:id=>elements.get(id)||null,createElement:()=>new Element(),addEventListener(){}},
TarinaiEvents:{emit(){}},showToast(){},TarinaiGameDialogs:{confirm:async()=>true},audio:{uiClick(){}},setTimeout(){return ++timer;},clearTimeout(){},setInterval(){return ++timer;},clearInterval(){},fetch:async()=>{throw new Error("offline");},TARINAI_VERSION:"39.16.56"};
c.window=c;c.globalThis=c;vm.createContext(c);vm.runInContext(source,c,{filename:"achievements.js"});
return {c,api:c.TarinaiAchievements,storage,elements};
}
function assert(v,m){if(!v)throw new Error(m);} function off(h,id){return !h.api.isUnlocked(id);} function on(h,id){return h.api.isUnlocked(id);}
function alive(n){return Array.from({length:n},(_,i)=>({id:`t${i}`,dead:false,x:i,y:0}));}
function findCard(h,id){const stack=[h.elements.get("achievementList")];while(stack.length){const node=stack.shift();if(node?.dataset?.achievementId===id)return node;stack.push(...(node?.children||[]));}return null;}
(function run(){
let h=harness();
assert(h.api.definitions.length===51,"must have 51 achievements");
h.api.open();
const title=Object.fromEntries(h.api.definitions.map(d=>[d.id,d.title]));
assert(title.medicine_ledger_all==="おくすり手帳全埋め","medicine title");
assert(title.mercury_lifespan==="不老不死まであと一歩","mercury title");
const description=(id)=>findCard(h,id).children[1].children[0].children[1].textContent;
assert(description("robot_cleaner_100").includes("ずんち・死骸・汚れを累計200個処理させる。"),"cleaner condition disclosed in UI");
assert(description("mercury_lifespan").includes("水銀を使用したたりないが天寿を全うする。"),"mercury condition disclosed in UI");
assert(findCard(h,"enemy_enemy_friend").children[1].children.some(node=>node.textContent.includes("現在の達成率 0.0%")),"enemy progress shown in UI");
assert(description("overprotective")==="","other new condition hidden");
// One Undo only: multiple smaller Undo operations never aggregate.
h.api.recordHistoryAction("undo",{deadRestored:9}); h.api.recordHistoryAction("undo",{deadRestored:1});
assert(off(h,"undo_mass_revival"),"mass undo aggregated across operations");
h.api.recordHistoryAction("undo",{deadRestored:10}); assert(on(h,"undo_mass_revival"),"mass undo boundary");
// Undo/Redo counters persist across field resets and reloads.
let storage=new Map(); h=harness(storage);
for(let i=0;i<12;i++)h.api.recordHistoryAction("undo",{});
h.api.resetWorldProgress({});
h=harness(storage); for(let i=0;i<7;i++)h.api.recordHistoryAction("undo",{});
assert(off(h,"undo_20"),"undo unlocked at 19"); h.api.recordHistoryAction("undo",{}); assert(on(h,"undo_20"),"undo did not persist to 20");
for(let i=0;i<11;i++)h.api.recordHistoryAction("redo",{}); h=harness(storage); for(let i=0;i<8;i++)h.api.recordHistoryAction("redo",{});
assert(off(h,"redo_20"),"redo unlocked at 19"); h.api.recordHistoryAction("redo",{}); assert(on(h,"redo_20"),"redo did not persist to 20");
// Cleaner is field-local.
h=harness(); const wa={time:0},wb={time:0}; h.api.recordRobotClean({world:wa,type:"tarinai"}); assert((wa.achievementRobotCleanCount||0)===0,"cleaner counted non-cleanup target");
for(let i=0;i<199;i++)h.api.recordRobotClean({world:wa,type:i%2?"zunchi":"trace"});
assert(off(h,"robot_cleaner_100"),"cleaner before 200"); h.api.recordRobotClean({world:wb,type:"ant_corpse"}); assert(off(h,"robot_cleaner_100"),"cleaner crossed field reset");
h.api.recordRobotClean({world:wa,type:"splat"}); assert(on(h,"robot_cleaner_100"),"cleaner at 200");
// Continuous pinch must be uninterrupted and same individual.
h=harness(); const held={dead:false,playerHeld:true}; const hw={tarinai:[held]};
h.api.recordHeldTarinai(hw,1000); h.api.recordHeldTarinai(hw,30999); assert(off(h,"held_30_seconds"),"held before 30s");
held.playerHeld=false; h.api.recordHeldTarinai(hw,31000); held.playerHeld=true; h.api.recordHeldTarinai(hw,40000); h.api.recordHeldTarinai(hw,69999); assert(off(h,"held_30_seconds"),"held did not reset after release");
h.c.document.hidden=true; h.api.recordHeldTarinai(hw,70000); h.c.document.hidden=false;
h.api.recordHeldTarinai(hw,70001); h.api.recordHeldTarinai(hw,100000); assert(off(h,"held_30_seconds"),"hidden tab time counted toward hold");
h.api.recordHeldTarinai(hw,100001); assert(on(h,"held_30_seconds"),"held at 30s after visible reset");
// Minimalist counts only surviving player placements.
h=harness(); let mw={time:0,day:5,tarinai:alive(1),ants:[],items:Array.from({length:11},()=>({_achievementPlayerPlaced:true,dead:false}))};
h.api.evaluateWorld(mw,{id:"happy"}); assert(off(h,"minimalist_happy"),"minimalist allowed 11 items");
mw.items[10].dead=true; h.api.evaluateWorld(mw,{id:"happy"}); assert(off(h,"minimalist_happy"),"minimalist unlocked before day 6");
mw.day=6; h.api.evaluateWorld(mw,{id:"happy"}); assert(on(h,"minimalist_happy"),"minimalist at day 6 with 10 items");
// Overprotective requires both thresholds on the same individual.
h=harness(); const cared={dead:false}; const cw={time:0};
for(let i=0;i<9;i++)h.api.recordDirectFeed({world:cw,tarinai:cared,type:"food"});
for(let i=0;i<3;i++)h.api.recordDirectCare({world:cw,tarinai:cared,type:"first_aid",treatment:true});
assert(off(h,"overprotective"),"overprotective before 10 feeds"); h.api.recordDirectFeed({world:cw,tarinai:cared,type:"food"}); assert(on(h,"overprotective"),"overprotective thresholds");
// Self-sufficient: 20+, no direct feed, no duplicator, 10 game minutes.
h=harness(); let sw={time:0,tarinai:alive(20),ants:[],items:[]}; h.api.evaluateWorld(sw,{}); sw.time=599.99;h.api.evaluateWorld(sw,{});assert(off(h,"self_sufficient"),"self sufficient early");
sw.items.push({type:"duplicator",dead:false});sw.time=600;h.api.evaluateWorld(sw,{});assert(off(h,"self_sufficient"),"self sufficient with duplicator");
sw.items=[];sw.time=700;h.api.evaluateWorld(sw,{});sw.time=1300;h.api.evaluateWorld(sw,{});assert(on(h,"self_sufficient"),"self sufficient after clean 10m");
h=harness(); sw={time:0,tarinai:alive(20),ants:[],items:[]};h.api.evaluateWorld(sw,{});sw.time=500;h.api.recordDirectFeed({world:sw,tarinai:sw.tarinai[0],type:"food"});sw.time=600;h.api.evaluateWorld(sw,{});assert(off(h,"self_sufficient"),"direct feed did not reset self sufficient");
// Unplanned city is persistent and deletion must be within 30 seconds.
storage=new Map(); h=harness(storage); let qw={time:0};
for(let i=0;i<15;i++){const item={};h.api.recordPlayerPlacement({world:qw,item});qw.time=30;h.api.recordPlayerDeletion({world:qw,item});qw.time=0;}
h=harness(storage);qw={time:0};for(let i=0;i<14;i++){const item={};h.api.recordPlayerPlacement({world:qw,item});qw.time=30;h.api.recordPlayerDeletion({world:qw,item});qw.time=0;}
assert(off(h,"unplanned_city_30"),"unplanned at 29");let item={};h.api.recordPlayerPlacement({world:qw,item});qw.time=30.001;h.api.recordPlayerDeletion({world:qw,item});assert(off(h,"unplanned_city_30"),"late deletion counted");
item={_achievementPlayerPlaced:true,_achievementPlacedAt:null};qw.time=0;h.api.recordPlayerDeletion({world:qw,item});assert(off(h,"unplanned_city_30"),"missing placement timestamp counted");
item={};qw.time=0;h.api.recordPlayerPlacement({world:qw,item});qw.time=30;h.api.recordPlayerDeletion({world:qw,item});assert(on(h,"unplanned_city_30"),"unplanned at 30");
// Sauna -> cold within 15 seconds; cold alone must never unlock.
h=harness(); const st={dead:false,x:0,y:0}; let temp=0; const tw={time:0,tarinai:[st],ants:[],items:[],feltTemperatureFor(){return temp;},temperatureStatusFor(v){return {direction:v>25?"hot":v<10?"cold":"comfort",comfortable:v>=10&&v<=25};}};
temp=0;h.api.evaluateWorld(tw,{});assert(off(h,"sauna_cold_plunge"),"cold alone unlocked sauna");temp=40;h.api.evaluateWorld(tw,{});tw.time=15;temp=0;h.api.evaluateWorld(tw,{});assert(on(h,"sauna_cold_plunge"),"sauna boundary 15s");
h=harness(); const st2={dead:false,x:0,y:0};temp=40;const tw2={time:0,tarinai:[st2],ants:[],items:[],feltTemperatureFor(){return temp;},temperatureStatusFor(v){return {direction:v>25?"hot":"cold",comfortable:false};}};h.api.evaluateWorld(tw2,{});tw2.time=15.01;temp=0;h.api.evaluateWorld(tw2,{});assert(off(h,"sauna_cold_plunge"),"sauna allowed over 15s");
// Rain shelter requires all 20 and accepts nest/pipe via shelter system.
h=harness(); h.c.TarinaiRainShelterSystem={isSheltered(_w,_x,_y,t){return Boolean(t.sheltered);}};
const rw={time:0,weather:"light_rain",tarinai:alive(20).map(t=>({...t,sheltered:true})),ants:[],items:[]};h.api.evaluateWorld(rw,{});assert(on(h,"rain_shelter_all"),"all sheltered rain achievement");
h=harness();h.c.TarinaiRainShelterSystem={isSheltered(_w,_x,_y,t){return Boolean(t.sheltered);}};rw.tarinai[19].sheltered=false;h.api.evaluateWorld(rw,{});assert(off(h,"rain_shelter_all"),"rain achievement allowed one exposed");
// Nest boxes and pipes are genuine weather blockers, not only achievement stubs.
h=harness();
vm.runInContext(fs.readFileSync(path.join(root,"js","tarinai_local_environment_system.js"),"utf8"),h.c,{filename:"tarinai_local_environment_system.js"});
const nest={id:"n1",type:"nest_box",dead:false};
const pipe={id:"p1",type:"pipe",dead:false};
const shelterWorld={items:[nest,pipe],nestBoxById(id){return this.items.find(x=>x.id===id)||null;}};
assert(h.c.TarinaiRainShelterSystem.isSheltered(shelterWorld,0,0,{insideNestBoxId:"n1"}),"nest box did not block weather");
assert(h.c.TarinaiRainShelterSystem.isSheltered(shelterWorld,0,0,{insideNestBoxId:"p1"}),"pipe did not block weather");
assert(!h.c.TarinaiRainShelterSystem.isSheltered(shelterWorld,0,0,{}),"unsheltered target treated as sheltered");
// Nest-box and pipe occupants must also be insulated from weather temperature offsets.
{
class FakeWorld {}
const tc={console,Math,Number,Object,Array,Set,Map,WeakMap,Date,CONFIG:{standardTemperature:15,temperatureItemRange:190},
World:FakeWorld,clamp:(v,a,b)=>Math.max(a,Math.min(b,v)),distXY:(x1,y1,x2,y2)=>Math.hypot(x1-x2,y1-y2),
TarinaiParasolSystem:h.c.TarinaiRainShelterSystem};
tc.window=tc;tc.globalThis=tc;vm.createContext(tc);
vm.runInContext(fs.readFileSync(path.join(root,"js","world_temperature_system.js"),"utf8"),tc,{filename:"world_temperature_system.js"});
const box={id:"box",type:"nest_box",dead:false,x:0,y:0,r:42};
const pipeItem={id:"pipe",type:"pipe",dead:false,x:0,y:0,r:42};
const weatherWorld=new FakeWorld();Object.assign(weatherWorld,{currentTemperature:15,temperatureAuto:true,groundType:"soil",weather:"light_rain",items:[box,pipeItem],
nearbyItems(x,y,r){return this.items.filter(it=>Math.hypot(it.x-x,it.y-y)<=r);},nestBoxById(id){return this.items.find(x=>x.id===id)||null;},nestBoxBaseRect(){return null;}});
const outside={x:500,y:0}; const inBox={x:0,y:0,insideNestBoxId:"box"}; const inPipe={x:0,y:0,insideNestBoxId:"pipe"};
assert(weatherWorld.feltTemperatureFor(outside)<15,"rain weather offset missing outside");
assert(weatherWorld.feltTemperatureFor(inBox)>=15,"nest box still received rain temperature offset");
assert(weatherWorld.feltTemperatureFor(inPipe)>=15,"pipe still received rain temperature offset");
}
// Every medicine item once; chain and crown are explicitly excluded.
h=harness(); const meds=["first_aid","sedative","sleep_drug","laxative","protein","niteropu","ammo","mystery_drug","mercury","giant_drug","dwarf_drug"];
meds.slice(0,-1).forEach(type=>h.api.recordConsumableUse({type}));assert(off(h,"medicine_ledger_all"),"medicine ledger early");h.api.recordConsumableUse({type:meds.at(-1)});assert(on(h,"medicine_ledger_all"),"medicine ledger complete");
// Mercury lifespan, revolution, fight mochi.
h=harness();h.api.recordDeath({world:{time:1,tarinai:[]},tarinai:{lifeItemMode:"mercury"},reason:"天寿を全うした"});assert(on(h,"mercury_lifespan"),"mercury lifespan");
h=harness();const slave={id:"s",isZunchiSlave:true,dead:false};const king={id:"k",isTarinaiChampion:true,dead:true,_achievementLastFightAttackerId:"s",_achievementLastFightDamageAt:5};const revWorld={time:5,tarinai:[slave,king]};h.api.recordDeath({world:revWorld,tarinai:king,reason:"喧嘩",fromDamage:true});assert(on(h,"revolution"),"revolution");
h=harness();const oil={};const fw={time:0};h.api.recordConsumableUse({world:fw,tarinai:oil,type:"fight_mochi"});fw.time=30;h.api.recordFightStarted({world:fw,a:oil,b:{}});assert(on(h,"fuel_to_fire"),"fight mochi at 30s");
h=harness();const noOil={};h.api.recordFightStarted({world:{time:0},a:noOil,b:{}});assert(off(h,"fuel_to_fire"),"fight without mochi unlocked at time zero");
h=harness();const oil2={};const fw2={time:0};h.api.recordConsumableUse({world:fw2,tarinai:oil2,type:"fight_mochi"});fw2.time=30.01;h.api.recordFightStarted({world:fw2,a:oil2,b:{}});assert(off(h,"fuel_to_fire"),"fight mochi over 30s");
// Enemy's enemy: 30 alive, no tarinai damage for 10 sec, danger source, field-local count.
h=harness(); const ew={time:20,tarinai:alive(30),achievementLastTarinaiDamageAt:0};
for(let i=0;i<9;i++){const ant={world:ew};h.api.recordAntDamageSource(ant,{type:"firecracker"},{world:ew});h.api.recordAntKilled({world:ew,ant});}
assert(off(h,"enemy_enemy_friend"),"enemy achievement early");const ant10={world:ew};h.api.recordAntDamageSource(ant10,{type:"firecracker"},{world:ew});h.api.recordAntKilled({world:ew,ant:ant10});assert(on(h,"enemy_enemy_friend"),"enemy achievement at 10");
h=harness();const ew2={time:20,tarinai:alive(30),achievementLastTarinaiDamageAt:11};for(let i=0;i<10;i++){const ant={world:ew2};h.api.recordAntDamageSource(ant,{type:"firecracker"},{world:ew2});h.api.recordAntKilled({world:ew2,ant});}assert(off(h,"enemy_enemy_friend"),"enemy counted within 10s of tarinai damage");
ew2.time=22;ew2.achievementLastTarinaiDamageAt=11;const ordinary={world:ew2};h.api.recordAntDamageSource(ordinary,{type:"food"},{world:ew2});h.api.recordAntKilled({world:ew2,ant:ordinary});assert(off(h,"enemy_enemy_friend"),"non-danger ant kill counted");
const noCollateral={time:50,tarinai:alive(30),achievementLastTarinaiDamageAt:-Infinity};
h.api.recordDamageSource(noCollateral.tarinai[0],{type:"food"},{world:noCollateral});
assert(noCollateral.achievementLastTarinaiDamageAt===-Infinity,"ordinary damage incorrectly blocked enemy achievement");
h.api.recordDamageSource(noCollateral.tarinai[0],{type:"firecracker"},{world:noCollateral});
assert(noCollateral.achievementLastTarinaiDamageAt===50,"danger collateral was not recorded");
h=harness();const ew3={time:20,tarinai:alive(30),achievementLastTarinaiDamageAt:0};const overwritten={world:ew3};h.api.recordAntDamageSource(overwritten,{type:"firecracker"},{world:ew3});h.api.recordAntDamageSource(overwritten,{id:"t0"},{world:ew3});h.api.recordAntKilled({world:ew3,ant:overwritten});assert((ew3.achievementEnemyAntKills||0)===0,"stale danger attribution survived later non-danger damage");
// Static integration guards for all production hook points.
const checks={
"js/command_dispatcher.js":["deadRestored","recordHistoryAction"],
"js/robot_cleaner_system.js":["recordRobotClean"],
"js/world_tool_actions.js":["recordPlayerDeletion","recordAntDamageSource"],
"js/world_family_social.js":["resetWorldProgress","recordFightStarted"],
"js/tarinai_social_move_life.js":["_achievementLastFightAttackerId"],
"js/tarinai_direct_feeding_system.js":["recordDirectCare"],
"js/tarinai_consumable_behavior.js":["recordConsumableUse"],
"js/tarinai_contact_item_system.js":["recordConsumableUse", "slave_chain", "golden_crown"],
"js/world_combat_effects.js":["recordAntDamageSource","source: it"],
"js/impact_response_system.js":["source: it"],
"js/ants.js":["recordAntDamageSource", "tarinai_struggle"],
"js/signal_system.js":["recordAntDamageSource", "electric_wire"],
"js/tarinai_item_effects.js":["recordAntDamageSource", "tarinai_ammo_collision"],
"js/tarinai_local_environment_system.js":["enclosedShelterFor","nest_box","pipe"],
"js/tarinai_update_step_frame.js":["isSheltered?.(t.world, t.x, t.y, t)"],
"js/tarinai_disease_nest.js":["isSheltered?.(this.world, this.x, this.y, this)"],
};
for(const [file,needles] of Object.entries(checks)){const text=fs.readFileSync(path.join(root,file),"utf8");for(const needle of needles)assert(text.includes(needle),`${file} missing ${needle}`);}
console.log("[OK] playstyle achievements: boundaries, reset scope, persistence, inline disclosures, and hook coverage passed");
})();

View file

@ -2104,12 +2104,16 @@ def check_connection_overlay_and_time_detector() -> None:
fail(f"detector dual time slider is missing {control_id}")
if '.pressure-time-thumb-start' not in styles or '.pressure-time-thumb-end' not in styles:
fail("detector dual-thumb slider styling is missing")
if 'item.pressureTarget === "time" ? 2' not in snapshot or 'item.pressureTimeStart' not in snapshot or 'item.pressureTimeEnd' not in snapshot:
fail("time detector snapshot persistence is missing")
if 'data.pressureTarget = item.pressureTarget === "time"' not in placement or 'item.pressureTarget = data.pressureTarget === "time"' not in placement:
fail("time detector copy persistence is missing")
if 'item.pressureTarget === "time") continue' not in render:
fail("time detectors still render a spatial detection rectangle")
if 'const targets = ["tarinai", "item", "time", "hunger_avg"' not in snapshot or 'item.pressureTimeStart' not in snapshot or 'item.pressureTimeEnd' not in snapshot:
fail("expanded detector snapshot persistence is missing")
if 'data.pressureTarget = item.pressureTarget || "tarinai"' not in placement or 'targets.has(data.pressureTarget)' not in placement or 'data.pressureComparator' not in placement:
fail("expanded detector copy persistence is missing")
if 'item.pressureTarget === "time" || item.pressureTarget === "temperature"' not in render:
fail("non-spatial detectors still render a detection rectangle")
removed = ["bio_sensor", "power_relay", "automationItemEditor"]
for token in removed:
if token in placement or token in snapshot or token in signal:
fail(f"removed automation item remains in runtime source: {token}")
runtime = f"""
const vm = require('vm');

View file

@ -0,0 +1,19 @@
"use strict";
const fs = require("fs");
const path = require("path");
const root = path.join(__dirname, "..");
const achievement = fs.readFileSync(path.join(root, "js", "achievements.js"), "utf8");
const save = fs.readFileSync(path.join(root, "js", "save_system.js"), "utf8");
const dialog = fs.readFileSync(path.join(root, "js", "game_dialogs.js"), "utf8");
const index = fs.readFileSync(path.join(root, "index.html"), "utf8");
function assert(value, message) { if (!value) throw new Error(message); }
assert(!/\b(?:window\.|global\.)?(?:alert|confirm|prompt)\s*\(/.test(achievement + save), "native browser popup call remains");
assert(achievement.includes("TarinaiGameDialogs") && save.includes("TarinaiGameDialogs"), "in-game confirmation API is not used");
assert(dialog.includes('role", "alertdialog"') && dialog.includes("data-game-dialog-confirm"), "shared in-game confirmation dialog is incomplete");
assert(!achievement.includes("TarinaiTooltips") && !achievement.includes("dataset.tip"), "achievement tooltip binding remains");
assert(achievement.includes("ACHIEVEMENT_CATEGORIES") && achievement.includes("createAchievementGroup"), "achievement category rendering is missing");
assert(!achievement.includes('id: "completed"'), "separate completed group still exists");
assert(achievement.includes("orderedDefinitions = [...unlockedDefinitions, ...lockedDefinitions]"), "unlocked achievements are not sorted to the top inside categories");
assert(["\\u751f\\u614b", "\\u5b9f\\u9a13", "\\u5efa\\u7bc9", "\\u305d\\u306e\\u4ed6"].every(label => achievement.includes(`title: "${label}"`)), "category titles are incorrect");
assert(index.includes('placeholder="&#x691C;&#x7D22;"'), "search placeholder was not shortened");
console.log("[OK] achievement grouping, inline disclosures, and in-game confirmation dialogs passed");

View file

@ -0,0 +1,64 @@
"use strict";
const fs = require("fs");
const vm = require("vm");
const path = require("path");
const root = path.join(__dirname, "..");
function assert(v, m) { if (!v) throw new Error(m); }
// Verify the manual push direction for all three passive mechanical types.
const context = {
console, Math, Number, Object, Array, Set, Map, WeakMap, Date,
clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); },
TarinaiCollisionFootprints: { hitTestItem() { return { hit: false, distance: Infinity }; } },
TarinaiPhysicsBodySystem: {
ensureBody(item) { return item.physicsBody; },
scalar(item, key, fallback = 0) {
const b = item.physicsBody || {};
if (key === "spin") return b.velocity?.angular ?? fallback;
if (key === "slideSpeed") return b.velocity?.linear ?? fallback;
if (key === "xv") return b.velocity?.x ?? fallback;
if (key === "yv") return b.velocity?.y ?? fallback;
if (key === "motorOn") return b.motor?.powered ?? fallback;
if (key === "railOn") return b.motor?.powered ?? fallback;
return b.state?.[key] ?? fallback;
},
setScalar(item, key, value) {
const b = item.physicsBody;
b.velocity ||= { x: 0, y: 0, angular: 0, linear: 0 };
b.state ||= {};
if (key === "spin") b.velocity.angular = value;
else if (key === "slideSpeed") b.velocity.linear = value;
else if (key === "xv") b.velocity.x = value;
else if (key === "yv") b.velocity.y = value;
else b.state[key] = value;
return true;
},
applyBodyState() { return true; },
},
TarinaiSignalSystem: { powerOverride() { return null; } },
};
context.window = context; context.globalThis = context;
vm.createContext(context);
vm.runInContext(fs.readFileSync(path.join(root, "js", "mechanical_system.js"), "utf8"), context, { filename: "mechanical_system.js" });
const body = (type) => ({
type, x: 0, y: 0, angle: 0, r: 40, world: { time: 0, markSpatialDirty() {} },
physicsBody: { type, pose: { x: 0, y: 0, angle: 0 }, velocity: { x: 0, y: 0, angular: 0, linear: 0 }, motor: { powered: false }, rail: { axisAngle: 0 }, state: { mass: 1, inertia: 3200 } },
});
let item = body("reciprocator");
context.TarinaiMechanicalSystem.applyPokeImpulse(item, 30, 0, item.world);
assert(item.physicsBody.velocity.linear < 0, "reciprocator moved toward the poke point");
item = body("poison_block");
context.TarinaiMechanicalSystem.applyPokeImpulse(item, 30, 0, item.world);
assert(item.physicsBody.velocity.x < 0, "poison block moved toward the poke point");
item = body("rotator");
context.TarinaiMechanicalSystem.applyPokeImpulse(item, 0, -30, item.world);
assert(item.physicsBody.velocity.angular > 0, "rotator poke torque is reversed");
const placement = fs.readFileSync(path.join(root, "js", "world_placement_log.js"), "utf8");
const input = fs.readFileSync(path.join(root, "js", "ui_input_shared.js"), "utf8");
const render = fs.readFileSync(path.join(root, "js", "render.js"), "utf8");
assert(placement.includes("findPokeTargetAt"), "shared poke target finder is missing");
assert(placement.indexOf('item.type !== "plushie"') < placement.indexOf('item.type !== "ball"'), "plushie is not prioritized by poke targeting");
assert(placement.includes("recordPlushiePokeFling"), "plushie achievement hook is missing");
assert(input.includes("hoverPokeTarget") && render.includes('label: "\\u3064\\u3064\\u304f"'), "poke overlay is missing");
console.log("[OK] poke targeting, overlay, plushie fling, and passive mechanism push directions passed");