129 lines
4.7 KiB
JavaScript
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");
|