RELEASE
planning/2.8.5/mockups/FO3/audit-u9-harness.mjs
sha256 18684ab37650d516 · 11029 bytes ·
original held in the private archive
// AUDIT U9 — independent Opus stage-3 harness. Not committed logic; lives under gitignored planning/.
import { chromium } from 'playwright';
import { fileURLToPath } from 'url';
import path from 'path';
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
const INDEX = 'file://' + path.join(ROOT, 'index.html').split(path.sep).join('/');
const OUT = path.join(ROOT, 'planning', 'mockups', 'fo3');
function saveFO3({ la = 'OK', ra = 'OK', ll = 'OK', rl = 'OK', hd = 'OK', hpCur = 100, rads = 0 } = {}) {
return {
activeContext: 'FO3',
campaigns: {
FO3: {
lvl: 12, hpCur, hpMax: 100, rads, caps: 350,
la, ra, ll, rl, hd,
s_s: 6, s_p: 7, s_e: 5, s_c: 4, s_i: 8, s_a: 6, s_l: 5,
inventory: Array.from({ length: 24 }, (_, i) => ({ name: 'Test Item ' + i, qty: 15, wgt: 1, val: 10, type: i % 2 ? 'weapon' : 'aid' })),
collectibles: [], perks: [],
status: [{ name: 'Well Rested', ticks: 4, type: 'BUFF' }],
},
},
};
}
async function boot(browser, save, viewport) {
const ctx = await browser.newContext({ viewport, hasTouch: true, isMobile: true });
await ctx.addInitScript(s => localStorage.setItem('robco_v8', JSON.stringify(s)), save);
const page = await ctx.newPage();
await page.goto(INDEX);
await page.waitForTimeout(2200);
return { ctx, page };
}
async function gotoStatus(page) {
await page.evaluate(() => window.selectSubsystem && window.selectSubsystem('operator'));
await page.waitForTimeout(200);
await page.evaluate(() => {
const b = [...document.querySelectorAll('#fo3SubtabRail button')].find(x => x.textContent.trim() === 'STATUS');
if (b) b.click();
});
await page.waitForTimeout(300);
}
// geometry of each limb box vs its figure limb, plus computed crippled state
async function limbGeo(page) {
return page.evaluate(() => {
const fig = document.querySelector('.vaultboy-fig');
const figRect = fig.getBoundingClientRect();
const figCx = figRect.left + figRect.width / 2;
const ids = ['la', 'ra', 'll', 'rl', 'hd'];
const out = {};
for (const id of ids) {
const btn = document.getElementById('btn_l_' + id);
const grp = document.querySelector('.vaultboy-fig [data-limb="' + id + '"]');
const br = btn ? btn.getBoundingClientRect() : null;
const lr = grp ? grp.getBoundingClientRect() : null;
out[id] = {
btnSide: br ? (br.left + br.width / 2 < figCx ? 'left' : 'right') : null,
limbSide: lr ? (lr.left + lr.width / 2 < figCx ? 'left' : 'right') : null,
limbCrippled: grp ? grp.classList.contains('crippled') : null,
btnText: btn ? btn.textContent.replace(/\s+/g, ' ').trim() : null,
};
}
return out;
});
}
// Sweep every element inside the FO3 glass for non-green computed colour.
async function redSweep(page, label) {
return page.evaluate(lbl => {
const RED = [];
const AMBER = [];
const root = document.querySelector('.glass-frame') || document.body;
const isVisible = el => {
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) return false;
const cs = getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) return false;
return true;
};
const parse = c => {
const m = c.match(/rgba?\(([^)]+)\)/);
if (!m) return null;
const p = m[1].split(',').map(s => parseFloat(s));
return { r: p[0], g: p[1], b: p[2], a: p[3] === undefined ? 1 : p[3] };
};
const els = root.querySelectorAll('*');
for (const el of els) {
if (!isVisible(el)) continue;
const cs = getComputedStyle(el);
// only leaf-ish text nodes matter for "colour"; but check color + borderColor + boxShadow
const txt = (el.childNodes.length && [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim()));
const col = parse(cs.color);
if (txt && col && col.a > 0.1) {
// red-ish: r dominant, g and b low
if (col.r > 150 && col.g < 120 && col.b < 120 && col.r - col.g > 60) {
RED.push({ kind: 'text-color', tag: el.tagName, id: el.id, cls: el.className.toString().slice(0, 40), color: cs.color, text: (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 30) });
} else if (col.r > 170 && col.g > 110 && col.g < 200 && col.b < 90 && col.r - col.b > 90) {
AMBER.push({ kind: 'text-color', tag: el.tagName, id: el.id, cls: el.className.toString().slice(0, 40), color: cs.color, text: (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 30) });
}
}
// border
const bc = parse(cs.borderColor);
if (bc && bc.a > 0.15 && (cs.borderTopWidth !== '0px')) {
if (bc.r > 150 && bc.g < 120 && bc.b < 120 && bc.r - bc.g > 60) {
RED.push({ kind: 'border', tag: el.tagName, id: el.id, cls: el.className.toString().slice(0, 40), color: cs.borderColor });
}
}
// box-shadow red (vignette)
if (cs.boxShadow && cs.boxShadow !== 'none' && /rgb/.test(cs.boxShadow)) {
const bs = parse(cs.boxShadow);
if (bs && bs.r > 150 && bs.g < 120 && bs.b < 120 && bs.r - bs.g > 60) {
RED.push({ kind: 'box-shadow', tag: el.tagName, id: el.id, cls: el.className.toString().slice(0, 40), color: cs.boxShadow.slice(0, 60) });
}
}
}
return { label: lbl, RED, AMBER };
}, label);
}
(async () => {
const browser = await chromium.launch();
const report = [];
// ---- TEST 1: mirror — LEFT-only crippled ----
{
const { ctx, page } = await boot(browser, saveFO3({ la: 'CRIPPLED', ll: 'CRIPPLED', hpCur: 60, rads: 100 }), { width: 780, height: 360 });
await gotoStatus(page);
const geo = await limbGeo(page);
await page.screenshot({ path: path.join(OUT, 'audit-u9-mirror-left-crippled.png') });
report.push({ test: 'LEFT-only crippled (la,ll)', geo });
await ctx.close();
}
// ---- TEST 2: mirror — RIGHT-only crippled ----
{
const { ctx, page } = await boot(browser, saveFO3({ ra: 'CRIPPLED', rl: 'CRIPPLED', hpCur: 60, rads: 100 }), { width: 780, height: 360 });
await gotoStatus(page);
const geo = await limbGeo(page);
await page.screenshot({ path: path.join(OUT, 'audit-u9-mirror-right-crippled.png') });
report.push({ test: 'RIGHT-only crippled (ra,rl)', geo });
await ctx.close();
}
// ---- TEST 3: DATA correctness — click L.ARM box, read state.la ----
{
const { ctx, page } = await boot(browser, saveFO3({ hpCur: 60, rads: 100 }), { width: 780, height: 360 });
await gotoStatus(page);
const rd = () => page.evaluate(() => {
const s = (typeof state !== 'undefined') ? state : (window.state || {});
return { la: s.la, ra: s.ra, ll: s.ll, rl: s.rl };
});
const before = await rd();
// click the L.ARM box
await page.evaluate(() => document.getElementById('btn_l_la').click());
await page.waitForTimeout(150);
const after = await rd();
const geo = await limbGeo(page);
await page.screenshot({ path: path.join(OUT, 'audit-u9-tap-larm.png') });
report.push({ test: 'TAP L.ARM box → state', before, after, laLimbCrippled: geo.la.limbCrippled, raLimbCrippled: geo.ra.limbCrippled, laLimbSide: geo.la.limbSide, laBtnSide: geo.la.btnSide });
await ctx.close();
}
// ---- TEST 4: RED SWEEP — critical HP + high rad + no radaway, all 6 boards ----
{
const { ctx, page } = await boot(browser, saveFO3({ la: 'CRIPPLED', hpCur: 18, rads: 850 }), { width: 780, height: 360 });
const boards = ['operator', 'operations', 'ops']; // will enumerate subsystems below
// enumerate subsystems + subtabs
const subs = await page.evaluate(() => [...document.querySelectorAll('.nav-cluster .navkey, #bezelNav button, .navkey')].map(b => b.getAttribute('data-subsystem') || b.textContent.trim()).filter(Boolean));
report.push({ test: 'subsystems-found', subs });
// Sweep STATUS specifically
await gotoStatus(page);
await page.screenshot({ path: path.join(OUT, 'audit-u9-red-status.png') });
const sweepStatus = await redSweep(page, 'STATUS (critical HP, high rad)');
report.push(sweepStatus);
// body class check
const bodyClass = await page.evaluate(() => document.body.className);
report.push({ test: 'body-class', bodyClass });
// sweep each FO3 subtab under operator
const subtabs = await page.evaluate(() => [...document.querySelectorAll('#fo3SubtabRail button')].map(b => b.textContent.trim()));
report.push({ test: 'operator-subtabs', subtabs });
for (const st of subtabs) {
await page.evaluate(name => {
const b = [...document.querySelectorAll('#fo3SubtabRail button')].find(x => x.textContent.trim() === name);
if (b) b.click();
}, st);
await page.waitForTimeout(250);
const sweep = await redSweep(page, 'operator/' + st);
report.push(sweep);
await page.screenshot({ path: path.join(OUT, 'audit-u9-board-operator-' + st.replace(/[^a-z0-9]/gi, '') + '.png') });
}
await ctx.close();
}
// ---- TEST 5: other subsystems (operations/comms/etc) red sweep ----
{
const { ctx, page } = await boot(browser, saveFO3({ la: 'CRIPPLED', hpCur: 18, rads: 850 }), { width: 780, height: 360 });
const subsystems = await page.evaluate(() => {
const set = new Set();
document.querySelectorAll('[data-subsystem]').forEach(b => set.add(b.getAttribute('data-subsystem')));
return [...set];
});
report.push({ test: 'all-subsystems', subsystems });
for (const s of subsystems) {
await page.evaluate(sub => window.selectSubsystem && window.selectSubsystem(sub), s);
await page.waitForTimeout(300);
const sweep = await redSweep(page, 'subsystem:' + s);
report.push(sweep);
await page.screenshot({ path: path.join(OUT, 'audit-u9-subsystem-' + s + '.png') });
}
await ctx.close();
}
// ---- TEST 6: NV untouched at 360/412/desktop ----
for (const vp of [{ width: 360, height: 800, k: '360' }, { width: 412, height: 915, k: '412' }, { width: 1440, height: 900, k: 'desktop' }]) {
const nvSave = { activeContext: 'FNV', campaigns: { FNV: { lvl: 10, hpCur: 18, hpMax: 100, rads: 200, caps: 100, s_s: 5, s_p: 5, s_e: 5, s_c: 5, s_i: 5, s_a: 5, s_l: 5, inventory: [{ name: 'Item', qty: 1, wgt: 1, val: 1, type: 'aid' }], status: [{ name: 'Rested', ticks: 2, type: 'BUFF' }] } } };
const { ctx, page } = await boot(browser, nvSave, { width: vp.width, height: vp.height });
await page.screenshot({ path: path.join(OUT, 'audit-u9-nv-' + vp.k + '.png') });
// NV should KEEP red delete buttons — sweep and expect some red present
const sweep = await redSweep(page, 'NV-' + vp.k);
report.push({ test: 'NV-' + vp.k + ' redCount', redCount: sweep.RED.length, sample: sweep.RED.slice(0, 5) });
await ctx.close();
}
await browser.close();
console.log(JSON.stringify(report, null, 2));
})();
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).