// Vehicle specific rules for turning a clump into a scene. // // A car dff is not just "every atomic, drawn at its frame". It also carries the // crash damage variants, a low detail chassis and a single wheel that the game // instances four (or six) times. Rendering it literally gives you a z-fighting // mess with one wheel on the front right. // Paint is encoded in the material colour. The game looks for these exact // values and swaps in the carcols entry for the vehicle's colour slot. // Verified against the retail models: every car uses 60,255,0 for the primary, // two thirds also use 255,0,175 for the secondary, and the car4 vehicles // (camper, cement, squalo) add 0,255,255 for the third. Body kit parts carry // the same masks, so they paint along with the car. const PAINT_MASKS = [ [60, 255, 0], [255, 0, 175], [0, 255, 255], [255, 0, 255], ]; // Near enough every vehicle also carries one material each in 255,175,0 / // 185,255,0 / 0,255,200 / 255,60,0. Those mark light panels, not paint, and are // deliberately absent from the list above so they keep their own colour. export const DEFAULT_PAINT = [200, 200, 200]; // Material colours come straight from bytes, so the match is exact. export function paintSlot(color) { const r = Math.round(color[0] * 255); const g = Math.round(color[1] * 255); const b = Math.round(color[2] * 255); for (let i = 0; i < PAINT_MASKS.length; i++) { const mask = PAINT_MASKS[i]; if (mask[0] === r && mask[1] === g && mask[2] === b) return i; } return -1; } // wheel_lf_dummy, wheel_rb_dummy, and the middle axle a six wheeler adds. const WHEEL_DUMMY = /^wheel_([lr])[fmb]_dummy$/i; // An upgrade dff is a single atomic on a single root frame - it carries no // attachment information at all. What holds it on is a dummy frame in the // vehicle, and the prefix of the part's model name picks which. Longer // prefixes come first so bntl_ is not eaten by bnt_. const UPGRADE_SLOTS = [ [/^bntl_/, 'ug_bonnet_left'], [/^bntr_/, 'ug_bonnet_right'], [/^bnt_/, 'ug_bonnet'], [/^spl_/, 'ug_spoiler'], [/^rf_/, 'ug_roof'], [/^wg_l_/, 'ug_wing_left'], [/^wg_r_/, 'ug_wing_right'], [/^nto_/, 'ug_nitro'], [/^lgt_/, 'ug_lights'], // Not the 'exhaust' frame - that one is the smoke emitter and sits about a // metre behind the car. The stock exhaust component is the mount, and the // upgrade takes its place: measured across the 31 vehicles that accept // exh_b_l, hanging it there puts the pipe end within a few cm of the rear of // the body every time. [/^exh_/, 'exhaust_ok', /^exhaust/], [/^misc_a_/, 'misc_a'], [/^misc_c_/, 'misc_c'], // The slamvan is the only vehicle with bull bar mounts, and they sit in // front of the bumper rather than replacing it. [/^fbb_/, 'ug_frontbullbar'], [/^bbb_/, 'ug_backbullbar'], // Bumpers take the stock one's place rather than sitting on top of it. [/^fbmp_/, 'bump_front_dummy', /^bump_front/], [/^rbmp_/, 'bump_rear_dummy', /^bump_rear/], ]; // The atomics of an upgrade clump worth drawing. // // Fitting a part re-frames its atomic onto the vehicle's dummy, which drops the // part's own root transform - so callers place the geometry at the dummy and // ignore the clump's frame. The bumpers are where this shows: fbmp_a_l's root // frame is a metre off centre and its geometry cancels that out, exactly like // the stock bumper does against bump_front_dummy. // // Parts carry their own crash variants too, so the _dam twin is skipped the // same way the vehicle's own are. export function upgradeAtomics(dff) { return dff.atomics.filter( (atomic) => !((dff.frames[atomic.frame].name || '').toLowerCase().endsWith('_dam')) ); } // null means the part has nothing to draw - stereo and hydralics are upgrades // with a model id but no visible geometry. export function upgradeSlot(name) { const model = String(name).toLowerCase(); if (/^wheel_/.test(model)) return { wheel: true }; for (const [prefix, frame, replaces] of UPGRADE_SLOTS) { if (prefix.test(model)) return { frame, replaces }; } return null; } // upgrades is a list of { name, dff } - parts already fetched and parsed. export function planVehicle(dff, upgrades = []) { const hide = new Set(); const wheels = []; const parts = []; const replaced = []; let wheelModel = null; const frameName = (index) => ((dff.frames[index] && dff.frames[index].name) || '').toLowerCase(); for (const upgrade of upgrades) { const slot = upgradeSlot(upgrade.name); if (!slot) continue; if (slot.wheel) { wheelModel = upgrade; continue; } // A vehicle simply has no frame for a part it was never meant to take. const frame = dff.frames.findIndex((f) => f.name.toLowerCase() === slot.frame); if (frame < 0) continue; parts.push({ upgrade, frame }); if (slot.replaces) replaced.push(slot.replaces); } dff.atomics.forEach((atomic, i) => { const name = frameName(atomic.frame); // bump_front_dam sits on top of bump_front_ok, chassis_vlo inside chassis. if (name.endsWith('_dam') || name.endsWith('_vlo')) hide.add(i); if (replaced.some((prefix) => prefix.test(name))) hide.add(i); }); // The lone 'wheel' atomic hangs off whichever dummy the modeller happened to // parent it to, and gets cloned to the rest. Left hand wheels are mirrored, // because every dummy shares the same orientation and only differs in // position - so without the flip the tyre wall faces inwards. const wheel = dff.atomics.findIndex((atomic) => frameName(atomic.frame) === 'wheel'); if (wheel >= 0) { hide.add(wheel); const modelled = new Set(dff.atomics.map((atomic) => atomic.frame)); dff.frames.forEach((frame, i) => { const match = WHEEL_DUMMY.exec(frame.name); if (!match) return; if (modelled.has(i)) return; // this corner models its own wheel wheels.push({ atomic: wheel, localFrame: dff.atomics[wheel].frame, frame: i, mirror: match[1].toLowerCase() === 'l', }); }); } return { hide, wheels, parts, wheelModel }; }