tarinai/scripts/self_zunchi_regression_audit.js
2026-07-17 12:25:23 +09:00

129 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const vm = require("vm");
const path = require("path");
function assert(condition, message) {
if (!condition) throw new Error(message);
}
function createHarness({ owner = true, lethal = true, neverStopped = true, clearedOwner = true } = {}) {
let unlocks = 0;
let damageCalls = 0;
const ownerId = "t-owner";
const tarinai = {
id: owner ? ownerId : "t-other",
releasedLiveId: "",
dead: false,
x: 100,
y: 100,
radius: 20,
vx: 0,
vy: 0,
fallTimer: 0,
fallMax: 0,
enterPanic() {},
};
const item = {
type: "zunchi",
dead: false,
x: 100,
y: 100,
prevX: 100,
prevY: 100,
r: 14,
amount: 240,
vx: 200,
vy: 0,
achievementBurstOwnerId: ownerId,
achievementBurstNeverStopped: neverStopped,
achievementBurstClearedOwner: clearedOwner,
};
const world = {
time: 10,
w: 1000,
h: 1000,
tarinai: [tarinai],
drawListDirty: false,
liveTarinaiById(id) { return tarinai.id === id && !tarinai.dead ? tarinai : null; },
nearbyTarinai() { return [tarinai]; },
isTarinaiHiddenInNestBox() { return false; },
applyImpactDamage(target) {
damageCalls += 1;
if (!lethal) return true;
target.dead = true;
target.releasedLiveId = target.id;
target.id = "";
return true;
},
spawnFallEffect() {},
effects: [],
};
const context = {
console,
globalThis: null,
window: undefined,
CONFIG: { worldPadding: 30 },
clamp: (value, min, max) => Math.max(min, Math.min(max, value)),
distXY: (ax, ay, bx, by) => Math.hypot(ax - bx, ay - by),
rand: () => 0,
Effect: function Effect() {},
TarinaiPhysics: { applyKinematicItemMotion() {} },
TarinaiItemDynamicBallSystem: { resolveObstacleCollisions() {} },
TarinaiAchievements: {
recordSelfZunchiDeath() { unlocks += 1; },
},
};
context.globalThis = context;
vm.createContext(context);
const source = fs.readFileSync(path.join(__dirname, "..", "js", "item_dynamic_zunchi_system.js"), "utf8");
vm.runInContext(source, context, { filename: "item_dynamic_zunchi_system.js" });
return { context, world, tarinai, item, get unlocks() { return unlocks; }, get damageCalls() { return damageCalls; } };
}
function runOne(options = {}) {
const h = createHarness(options);
h.context.TarinaiItemDynamicZunchiSystem.updateMotion(h.item, 0.016, h.world);
return h;
}
let result = runOne({ owner: true, lethal: true, neverStopped: true, clearedOwner: true });
assert(result.unlocks === 1, "fatal owner return collision should unlock even after live ID release");
result = runOne({ owner: true, lethal: false, neverStopped: true, clearedOwner: true });
assert(result.unlocks === 0, "nonfatal owner collision must not unlock");
result = runOne({ owner: false, lethal: true, neverStopped: true, clearedOwner: true });
assert(result.unlocks === 0, "fatal collision with another tarinai must not unlock");
result = runOne({ owner: true, lethal: true, neverStopped: false, clearedOwner: true });
assert(result.unlocks === 0, "stopped or player-held burst zunchi must remain disqualified");
// Exact route prerequisite: the emitted zunchi begins inside its owner, but that
// spawn overlap must neither damage the owner nor consume the later return hit.
result = runOne({ owner: true, lethal: true, neverStopped: true, clearedOwner: false });
assert(result.unlocks === 0, "spawn overlap must not unlock");
assert(result.damageCalls === 0, "spawn overlap must not damage or slow the owner collision path");
assert(result.item.achievementBurstClearedOwner === false, "spawn overlap must remain uncleared while still inside owner");
// Move outside once (the route's outbound leg / bounce-fence approach), then
// return at lethal speed. This must unlock.
result = createHarness({ owner: true, lethal: true, neverStopped: true, clearedOwner: false });
result.item.x = 180;
result.item.prevX = 100;
result.item.vx = 320;
result.context.TarinaiItemDynamicZunchiSystem.updateMotion(result.item, 0.016, result.world);
assert(result.item.achievementBurstClearedOwner === true, "burst zunchi must arm after clearing its owner");
assert(result.damageCalls === 0, "outbound leg must not damage the owner");
result.item.x = 100;
result.item.prevX = 180;
result.item.vx = -900; // representative accelerated return after a bounce fence
result.item.vy = 0;
result.world.time += 0.2;
result.context.TarinaiItemDynamicZunchiSystem.updateMotion(result.item, 0.016, result.world);
assert(result.unlocks === 1, "cleared, accelerated return collision with owner must unlock when fatal");
console.log("[OK] self-zunchi spawn-clear-return regression cases passed");