Vehicles
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user