Lane E S16: sixteen pins for gate 5 — 10 auto scale/node checks on the five new GLBs + 6 named: pergola resolves through the MANIFEST (never a literal) and prices between gutter and swing; the wreck fails partially with byte-close decks; the glasshouse offers NOTHING from three directions and prices as the apex; panes pinned translucent, wreck pinned emptied-not-felled; the pool's 14 denials pinned per-post BY NAME (RULE 1's regex cannot see them — proven, control M3). 434/0/0 in the browser
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
453f6cb9d2
commit
b74383722b
@ -100,6 +100,22 @@ const ASSETS = [
|
|||||||
nodes: ['frame', 'crossbar', 'swings', 'frame_anchor_01', 'frame_anchor_02'] },
|
nodes: ['frame', 'crossbar', 'swings', 'frame_anchor_01', 'frame_anchor_02'] },
|
||||||
{ name: 'swing_set_01_wrecked', h: [0.80, 1.05],
|
{ name: 'swing_set_01_wrecked', h: [0.80, 1.05],
|
||||||
nodes: ['frame', 'crossbar', 'swings'] },
|
nodes: ['frame', 'crossbar', 'swings'] },
|
||||||
|
// SPRINT16 gate 5 — the next temptations. Height floors carry design claims:
|
||||||
|
// the pergola's 2.55 floor sits ABOVE the swing set's 2.15 ceiling (the rung
|
||||||
|
// it out-rates must stand taller than the rung below it), and the pool
|
||||||
|
// fence's band brackets 1.2 m because the compliance height IS the flavour.
|
||||||
|
{ name: 'pergola_01', h: [2.55, 2.85],
|
||||||
|
nodes: ['deck', 'posts', 'beams', 'rafters',
|
||||||
|
'pergola_anchor_01', 'pergola_anchor_02'] },
|
||||||
|
{ name: 'pergola_01_wrecked', h: [2.55, 2.85],
|
||||||
|
nodes: ['deck', 'posts', 'beams', 'beam_down', 'rafters_down'] },
|
||||||
|
{ name: 'glasshouse_01', h: [2.15, 2.40],
|
||||||
|
nodes: ['frame', 'panes', 'door', 'ridge_anchor'] },
|
||||||
|
{ name: 'glasshouse_01_wrecked', h: [2.15, 2.40],
|
||||||
|
nodes: ['frame', 'door', 'panes_left', 'shards'] },
|
||||||
|
{ name: 'pool_kit_01', h: [1.12, 1.32],
|
||||||
|
nodes: ['pool_shell', 'pool_water', 'fence_rails', 'pool_gate',
|
||||||
|
'fence_post_01', 'fence_post_14'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
function sizeOf(gltf) {
|
function sizeOf(gltf) {
|
||||||
@ -502,7 +518,9 @@ export default async function run(t) {
|
|||||||
for (const [intact, broken] of [['garden_gnome_01', 'garden_gnome_01_broken'],
|
for (const [intact, broken] of [['garden_gnome_01', 'garden_gnome_01_broken'],
|
||||||
['fence_panel', 'fence_panel_snapped'],
|
['fence_panel', 'fence_panel_snapped'],
|
||||||
['swing_set_01', 'swing_set_01_wrecked'],
|
['swing_set_01', 'swing_set_01_wrecked'],
|
||||||
['house_yardside', 'house_yardside_wrecked']]) {
|
['house_yardside', 'house_yardside_wrecked'],
|
||||||
|
['pergola_01', 'pergola_01_wrecked'],
|
||||||
|
['glasshouse_01', 'glasshouse_01_wrecked']]) {
|
||||||
for (const n of [intact, broken]) {
|
for (const n of [intact, broken]) {
|
||||||
const box = new THREE.Box3().setFromObject(loaded.get(n).scene);
|
const box = new THREE.Box3().setFromObject(loaded.get(n).scene);
|
||||||
assert(Math.abs(box.min.y) < 0.03,
|
assert(Math.abs(box.min.y) < 0.03,
|
||||||
@ -792,6 +810,246 @@ export default async function run(t) {
|
|||||||
`gum is ${g.y.toFixed(2)} m tall and ${Math.max(g.x, g.z).toFixed(2)} m across — the control is broken`);
|
`gum is ${g.y.toFixed(2)} m tall and ${Math.max(g.x, g.z).toFixed(2)} m across — the control is broken`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// SPRINT16 gate 5 — THE NEXT TEMPTATIONS. Three props, three different ways
|
||||||
|
// of saying no: the pergola says "yes, at 0.65" (the ladder's middle rung),
|
||||||
|
// the glasshouse says "no, and here is the bill if you make me" (the
|
||||||
|
// collateral apex), the pool ring says "no" fourteen times in a perfect
|
||||||
|
// circle. Every claim below is a number read off the shipped GLB.
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// The pergola's rating is pinned against the MANIFEST, not a literal — the
|
||||||
|
// gate-1.2 lesson applied to the asset that gate's mechanism was built FOR:
|
||||||
|
// a site declaring a node-less `pergola` anchor must resolve these exact
|
||||||
|
// extras, so the manifest entry existing (and being unambiguous) IS the
|
||||||
|
// feature. A pin that hard-coded 0.65 would stay green while the manifest
|
||||||
|
// silently dropped the type.
|
||||||
|
t.test('the pergola is the ladder\'s middle rung, and the manifest can resolve it', () => {
|
||||||
|
const types = FACTORY_ANCHOR_RATINGS.types;
|
||||||
|
const p = types.pergola;
|
||||||
|
assert(p, 'the manifest does not rate "pergola" — either the bake is missing or the '
|
||||||
|
+ 'type went AMBIGUOUS (its nodes disagree), and a node-less pergola anchor would '
|
||||||
|
+ 'fall back to the default hint: the sail_post bug shape, on the asset built to bury it');
|
||||||
|
assert(p.collateral === 'pergola', `manifest collateral is ${JSON.stringify(p.collateral)}, want "pergola"`);
|
||||||
|
const swing = types.swing_frame.rating_hint, post = types.post.rating_hint;
|
||||||
|
assert(p.rating_hint > swing && p.rating_hint < post,
|
||||||
|
`pergola rates ${p.rating_hint} — the rung must sit strictly between the swing frame `
|
||||||
|
+ `(${swing}, bolted to nothing) and the post (${post}, bolted to the planet); at either `
|
||||||
|
+ 'end the ladder loses the rung D endorsed and fascia-or-nothing is back');
|
||||||
|
|
||||||
|
const g = loaded.get('pergola_01');
|
||||||
|
assert(g, 'pergola_01 did not load');
|
||||||
|
for (const n of ['pergola_anchor_01', 'pergola_anchor_02']) {
|
||||||
|
const o = g.scene.getObjectByName(n);
|
||||||
|
assert(o, `${n} missing`);
|
||||||
|
assert(o.userData?.anchor_type === 'pergola',
|
||||||
|
`${n} is typed ${JSON.stringify(o.userData?.anchor_type)} — a pergola is not a post; `
|
||||||
|
+ 'the type string is the player\'s pre-rig read and "post" promises concrete');
|
||||||
|
assert(o.userData?.rating_hint === p.rating_hint,
|
||||||
|
`${n} bakes ${o.userData?.rating_hint} but the manifest says ${p.rating_hint}`);
|
||||||
|
assert(o.userData?.collateral === 'pergola', `${n} must name what it takes with it`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The price ladder, read off the GLBs so site JSON and factory can't tell
|
||||||
|
// two stories: gutter < pergola < swing set < carport. The pergola bills
|
||||||
|
// UNDER the swing set because its failure is partial (a beam and a post;
|
||||||
|
// the bolts hold) where the swing's is total (the whole frame on the
|
||||||
|
// grass) — prices must agree with the wrecks the player actually sees.
|
||||||
|
const price = (a) => loaded.get(a)?.scene.getObjectByName(a)?.userData?.collateral_value;
|
||||||
|
const cost = price('pergola_01');
|
||||||
|
assert(typeof cost === 'number', 'pergola_01 carries no collateral_value');
|
||||||
|
const root = g.scene.getObjectByName('pergola_01');
|
||||||
|
assert(root.userData?.collateral_key === 'pergola',
|
||||||
|
'collateral_key must name the string the anchors carry, or the price prices nothing');
|
||||||
|
assert(cost > price('house_yardside') && cost < price('swing_set_01'),
|
||||||
|
`the pergola (${cost}) must sit between the gutter (${price('house_yardside')}) and the `
|
||||||
|
+ `swing set (${price('swing_set_01')}) — a partial timber repair beats a run of guttering `
|
||||||
|
+ 'and loses to a whole frame replacement');
|
||||||
|
|
||||||
|
const w = loaded.get('pergola_01_wrecked')?.scene.getObjectByName('pergola_01_wrecked');
|
||||||
|
assert(w?.userData?.collateral_value === cost,
|
||||||
|
'the wrecked pergola must be priced the same as the one it used to be');
|
||||||
|
assert(w?.userData?.broken_variant_of === 'pergola_01',
|
||||||
|
'the wreck must name its intact twin so A can pair the swap');
|
||||||
|
});
|
||||||
|
|
||||||
|
// The 0.65 story told by the wreck: a bolted structure fails PARTIALLY.
|
||||||
|
// The front beam and the rafters come down; the deck, the bolts and the
|
||||||
|
// back half stand. Measured, because "partial" is exactly the kind of word
|
||||||
|
// that erodes into "rubble" one art pass at a time.
|
||||||
|
t.test('the pergola fails partially: rafters hinge down, the deck stands, no anchor survives', () => {
|
||||||
|
const w = loaded.get('pergola_01_wrecked');
|
||||||
|
assert(w, 'pergola_01_wrecked did not load');
|
||||||
|
for (const n of ['pergola_anchor_01', 'pergola_anchor_02']) {
|
||||||
|
assert(!w.scene.getObjectByName(n),
|
||||||
|
`${n} survives the wreck — you cannot re-bolt a snapped post mid-storm, and an anchor `
|
||||||
|
+ 'that outlives its structure is the free-failure bug in a costume');
|
||||||
|
}
|
||||||
|
// The lean-to read: rafters still reach the surviving back beam AND rest
|
||||||
|
// on the deck — high at one end, low at the other, in the same node.
|
||||||
|
const down = new THREE.Box3().setFromObject(w.scene.getObjectByName('rafters_down'));
|
||||||
|
assert(down.max.y > 2.3,
|
||||||
|
`rafters_down tops out at y=${down.max.y.toFixed(2)} — they must still hang from the back beam`);
|
||||||
|
assert(down.min.y < 0.5,
|
||||||
|
`rafters_down bottoms out at y=${down.min.y.toFixed(2)} — the front ends should rest on the deck`);
|
||||||
|
const beamDown = new THREE.Box3().setFromObject(w.scene.getObjectByName('beam_down'));
|
||||||
|
assert(beamDown.max.y < 0.55,
|
||||||
|
`beam_down tops out at y=${beamDown.max.y.toFixed(2)} — the torn beam lies on the deck`);
|
||||||
|
// Intact control: the rafters were UP, or "down" above means nothing.
|
||||||
|
const up = new THREE.Box3().setFromObject(loaded.get('pergola_01').scene.getObjectByName('rafters'));
|
||||||
|
assert(up.min.y > 2.3,
|
||||||
|
`intact rafters bottom out at y=${up.min.y.toFixed(2)} — the control is broken`);
|
||||||
|
|
||||||
|
// The wreck does not stand taller, and the DECK did not move: it is the
|
||||||
|
// footing the rating is about, and the shared helper builds it once —
|
||||||
|
// byte-drift between the twins' decks means someone forked the helper.
|
||||||
|
const hi = new THREE.Box3().setFromObject(loaded.get('pergola_01').scene);
|
||||||
|
const hw = new THREE.Box3().setFromObject(w.scene);
|
||||||
|
assert(hw.max.y <= hi.max.y + 0.02,
|
||||||
|
`the wreck stands ${hw.max.y.toFixed(2)} m vs the intact ${hi.max.y.toFixed(2)} — taller is a bug`);
|
||||||
|
const deckA = new THREE.Box3().setFromObject(loaded.get('pergola_01').scene.getObjectByName('deck'));
|
||||||
|
const deckB = new THREE.Box3().setFromObject(w.scene.getObjectByName('deck'));
|
||||||
|
assert(deckA.min.distanceTo(deckB.min) < 0.01 && deckA.max.distanceTo(deckB.max) < 0.01,
|
||||||
|
'the two decks differ — intact and wrecked no longer share the helper, and they will drift');
|
||||||
|
});
|
||||||
|
|
||||||
|
// The glasshouse's one rule, pinned from every direction: NOTHING about it
|
||||||
|
// may adopt as an anchor. Not the ridge (denied in words), not any node by
|
||||||
|
// silence (a rating_hint anywhere on either variant is a red), not via the
|
||||||
|
// enum (no node enrols an anchor_type). The ridge bar is the most
|
||||||
|
// tie-off-shaped object in the game and that is exactly why the denial is
|
||||||
|
// load-bearing rather than decorative.
|
||||||
|
t.test('the glasshouse offers NOTHING: no rated node, no typed node, and the ridge denies', () => {
|
||||||
|
for (const name of ['glasshouse_01', 'glasshouse_01_wrecked']) {
|
||||||
|
const g = loaded.get(name);
|
||||||
|
assert(g, `${name} did not load`);
|
||||||
|
const rated = [], typed = [];
|
||||||
|
g.scene.traverse((o) => {
|
||||||
|
if (typeof o.userData?.rating_hint === 'number') rated.push(o.name);
|
||||||
|
if (o.userData?.anchor_type !== undefined) typed.push(o.name);
|
||||||
|
});
|
||||||
|
assert(rated.length === 0,
|
||||||
|
`${name} carries rating_hint on [${rated.join(', ')}] — a glasshouse node with a rating `
|
||||||
|
+ 'is adoptable, and 30 kg of glass is not a thing you add load to');
|
||||||
|
assert(typed.length === 0,
|
||||||
|
`${name} enrols [${typed.join(', ')}] in the anchor enum — nothing here is an anchor of any type`);
|
||||||
|
}
|
||||||
|
const ridge = loaded.get('glasshouse_01').scene.getObjectByName('ridge_anchor');
|
||||||
|
assert(ridge, 'ridge_anchor missing — the temptation must exist to be denied');
|
||||||
|
assert(ridge.userData?.tie_off === false,
|
||||||
|
'the ridge must carry tie_off:false — the most anchor-looking member says no ON the data');
|
||||||
|
assert(typeof ridge.userData?.why === 'string' && ridge.userData.why.length > 0,
|
||||||
|
'the denial must carry its reasoning — a bare flag is a rule, a why is a lesson');
|
||||||
|
});
|
||||||
|
|
||||||
|
// The apex is a price claim, so it is pinned as one: strictly the worst
|
||||||
|
// bill in the palette, keyed, and identical on the wreck. The billing
|
||||||
|
// EVENT is arc 2's (nothing in the sim wrecks a glasshouse yet — the value
|
||||||
|
// is inert data until then, which is why pricing it is not the bike bug);
|
||||||
|
// this pin is the chain being ready the day that yard is authored.
|
||||||
|
t.test('the glasshouse is the collateral apex: priced above everything, keyed, twin-priced', () => {
|
||||||
|
const g = loaded.get('glasshouse_01')?.scene.getObjectByName('glasshouse_01');
|
||||||
|
assert(g, 'glasshouse_01 root missing');
|
||||||
|
const cost = g.userData?.collateral_value;
|
||||||
|
assert(typeof cost === 'number', 'glasshouse carries no collateral_value');
|
||||||
|
assert(g.userData?.collateral_key === 'glasshouse',
|
||||||
|
'collateral_key must be explicit — the carport was one editor click from being FREE '
|
||||||
|
+ 'because its price resolved off a coincidence of naming');
|
||||||
|
const carport = loaded.get('carport_01')?.scene.getObjectByName('carport_01')?.userData?.collateral_value;
|
||||||
|
assert(cost > carport,
|
||||||
|
`the glasshouse (${cost}) must top the carport (${carport}) by construction — fourteen panes `
|
||||||
|
+ 'of glass against one sheet of Colorbond, or "apex" is just a word');
|
||||||
|
const w = loaded.get('glasshouse_01_wrecked')?.scene.getObjectByName('glasshouse_01_wrecked');
|
||||||
|
assert(w?.userData?.collateral_value === cost,
|
||||||
|
'the wrecked glasshouse must be priced the same as the one it used to be');
|
||||||
|
assert(w?.userData?.broken_variant_of === 'glasshouse_01',
|
||||||
|
'the wreck must name its intact twin so A can pair the swap');
|
||||||
|
});
|
||||||
|
|
||||||
|
// What glass DOES, measured: the intact panes are translucent (the fragile
|
||||||
|
// read is a material fact, not a vibe — turn the glass opaque and the tell
|
||||||
|
// is deleted), and the wreck EMPTIES rather than falls (frame at full
|
||||||
|
// height, shard field flat on the ground and thrown wider than the
|
||||||
|
// building ever stood).
|
||||||
|
t.test('the glasshouse reads fragile, and its wreck empties instead of falling', () => {
|
||||||
|
const panes = loaded.get('glasshouse_01')?.scene.getObjectByName('panes');
|
||||||
|
assert(panes, 'panes node missing');
|
||||||
|
let translucent = false;
|
||||||
|
panes.traverse((o) => {
|
||||||
|
const m = o.material;
|
||||||
|
if (m && m.transparent === true && m.opacity < 0.6) translucent = true;
|
||||||
|
});
|
||||||
|
assert(translucent,
|
||||||
|
'no pane material is transparent with opacity < 0.6 — the glass has gone opaque, and '
|
||||||
|
+ 'the only see-through walls in the palette no longer read as glass');
|
||||||
|
|
||||||
|
const hi = new THREE.Box3().setFromObject(loaded.get('glasshouse_01').scene);
|
||||||
|
const wreck = loaded.get('glasshouse_01_wrecked');
|
||||||
|
const hw = new THREE.Box3().setFromObject(wreck.scene);
|
||||||
|
assert(Math.abs(hi.max.y - hw.max.y) < 0.05,
|
||||||
|
`wreck ridge at y=${hw.max.y.toFixed(2)} vs intact ${hi.max.y.toFixed(2)} — the aluminium frame `
|
||||||
|
+ 'survives; a felled glasshouse is lying about what glass does');
|
||||||
|
const shards = new THREE.Box3().setFromObject(wreck.scene.getObjectByName('shards'));
|
||||||
|
assert(shards.max.y < 0.05,
|
||||||
|
`shards top out at y=${shards.max.y.toFixed(2)} — glass on the ground lies flat`);
|
||||||
|
const spreadX = (hw.max.x - hw.min.x) - (hi.max.x - hi.min.x);
|
||||||
|
const spreadZ = (hw.max.z - hw.min.z) - (hi.max.z - hi.min.z);
|
||||||
|
assert(spreadX > 0.6 && spreadZ > 0.6,
|
||||||
|
`the shard field adds only ${spreadX.toFixed(2)}/${spreadZ.toFixed(2)} m in plan — it must `
|
||||||
|
+ 'throw glass clear of the footprint or the wreck reads as a dirty window, not a catastrophe');
|
||||||
|
assert(wreck.scene.getObjectByName('panes_left'),
|
||||||
|
'panes_left missing — a wreck with zero surviving panes reads as demolition, not storm');
|
||||||
|
});
|
||||||
|
|
||||||
|
// The pool ring: fourteen posts, fourteen explicit nos. RULE 1's regex only
|
||||||
|
// guards *_anchor names, and these posts don't match it — which is exactly
|
||||||
|
// how they'd slip through as silent 1.0s the day a site names one (the
|
||||||
|
// `?? 1` default is generous, and silence is not neutral). So the denial is
|
||||||
|
// pinned per-post, by name, with the count exact: a fifteenth post added
|
||||||
|
// without its denial goes red here, not in a shipped yard.
|
||||||
|
t.test('the pool ring is fourteen honest nos — every fence post denies, nothing rates', () => {
|
||||||
|
const g = loaded.get('pool_kit_01');
|
||||||
|
assert(g, 'pool_kit_01 did not load');
|
||||||
|
const posts = [];
|
||||||
|
g.scene.traverse((o) => { if (/^fence_post_\d+$/.test(o.name)) posts.push(o); });
|
||||||
|
assert(posts.length === 14,
|
||||||
|
`found ${posts.length} fence posts, want exactly 14 — a post this count doesn't know `
|
||||||
|
+ 'is a post whose denial nobody checked');
|
||||||
|
for (const p of posts) {
|
||||||
|
assert(p.userData?.tie_off === false,
|
||||||
|
`${p.name} does not deny being a tie-off — silence here adopts at rating 1.0, the best `
|
||||||
|
+ 'steel in the game conjured out of a missing field');
|
||||||
|
assert(p.userData?.rating_hint === undefined,
|
||||||
|
`${p.name} carries a rating_hint, which makes it adoptable — the ring's whole design is `
|
||||||
|
+ 'that its steel is legally useless');
|
||||||
|
assert(typeof p.userData?.why === 'string' && p.userData.why.length > 0,
|
||||||
|
`${p.name} denies without reasoning`);
|
||||||
|
}
|
||||||
|
// Nothing anywhere on the kit rates or enrols — same sweep as the glasshouse.
|
||||||
|
const rated = [], typed = [];
|
||||||
|
g.scene.traverse((o) => {
|
||||||
|
if (typeof o.userData?.rating_hint === 'number') rated.push(o.name);
|
||||||
|
if (o.userData?.anchor_type !== undefined) typed.push(o.name);
|
||||||
|
});
|
||||||
|
assert(rated.length === 0 && typed.length === 0,
|
||||||
|
`pool_kit_01 offers [${[...rated, ...typed].join(', ')}] — the kit must offer nothing`);
|
||||||
|
|
||||||
|
// Compliance is the flavour, so its numbers are pinned as facts: 1.2 m
|
||||||
|
// posts, a self-closing gate, and no price until something can break it.
|
||||||
|
const post1 = new THREE.Box3().setFromObject(g.scene.getObjectByName('fence_post_01'));
|
||||||
|
assert(post1.max.y >= 1.15 && post1.max.y <= 1.32,
|
||||||
|
`fence stands ${post1.max.y.toFixed(2)} m — AS 1926.1 says 1.2, and the height IS the flavour`);
|
||||||
|
const gate = g.scene.getObjectByName('pool_gate');
|
||||||
|
assert(gate?.userData?.self_closing === true, 'the gate must be self-closing — compliance is data');
|
||||||
|
const root = g.scene.getObjectByName('pool_kit_01');
|
||||||
|
assert(root?.userData?.priced === false && typeof root?.userData?.unpriced_why === 'string',
|
||||||
|
'the kit must say OUT LOUD that it is unpriced and why (the bike ruling) — an unpriced '
|
||||||
|
+ 'prop with no reason is indistinguishable from a forgotten one');
|
||||||
|
assert(root?.userData?.collateral_value === undefined,
|
||||||
|
'pool_kit_01 carries a collateral_value — pricing an event the sim cannot produce is the '
|
||||||
|
+ 'lie the invoice exists to kill; when debris can take a panel out, price it THEN');
|
||||||
|
});
|
||||||
|
|
||||||
// One GLB carries three wilt states as siblings; Lane A toggles .visible
|
// One GLB carries three wilt states as siblings; Lane A toggles .visible
|
||||||
// rather than reloading, so all three have to be present at once.
|
// rather than reloading, so all three have to be present at once.
|
||||||
t.test('garden_bed carries all 3 damage states in one GLB', () => {
|
t.test('garden_bed carries all 3 damage states in one GLB', () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user