--[[ diag_shellparts.luau -- dump the actual part inventory of hatch_junk and hyper_x. diag_nosecue2 found no headlight/taillight/seat on either, so the naming convention must differ. Read-only: list what IS there, plus a purely geometric asymmetry cue that needs no naming at all. Geometric cue (naming-independent): on a car, the passenger cabin / windscreen sits BEHIND the nose, so the mass distribution along the longitudinal axis is asymmetric. Cars are also taller at the cabin than at the nose. Reporting mean height per Z-half lets the shape itself indicate which end is the bonnet -- the tall half is the cabin/rear-biased end. ]] local RS = game:GetService("ReplicatedStorage") local SSS = game:GetService("ServerScriptService") local SS = game:GetService("ServerStorage") local TARGETS = { hatch_junk = true, hyper_x = true, rx7_fc = true } local out = {} local function log(s) table.insert(out, s) end local shells = {} for _, root in ipairs({ RS, SSS, SS, workspace }) do for _, d in ipairs(root:GetDescendants()) do if d:IsA("Model") and TARGETS[d.Name] and d:FindFirstChild("Wheels") and not shells[d.Name] then shells[d.Name] = d end end end for _, name in ipairs({ "rx7_fc", "hatch_junk", "hyper_x" }) do local shell = shells[name] if not shell then log(name .. ": NOT FOUND") continue end local pivotInv = shell:GetPivot():Inverse() log("=== " .. name .. " ===") -- Part names outside the Wheels folder, with local Z, sorted front-to-back. local rows = {} for _, d in ipairs(shell:GetDescendants()) do if d:IsA("BasePart") and not d:IsDescendantOf(shell.Wheels) then local p = pivotInv * d.Position table.insert(rows, { name = d.Name, z = p.Z, y = p.Y, cls = d.ClassName }) end end table.sort(rows, function(a, b) return a.z < b.z end) log(string.format(" %d non-wheel parts (sorted by local Z, -Z first):", #rows)) local shown = 0 for _, r in ipairs(rows) do if shown < 24 then log(string.format(" z=%+7.2f y=%+6.2f %-28s %s", r.z, r.y, r.name, r.cls)) shown += 1 end end if #rows > 24 then log(string.format(" ... %d more", #rows - 24)) end -- Naming-independent shape cue: mean height of parts in each Z half. local negY, negN, posY, posN = 0, 0, 0, 0 local minZ, maxZ for _, r in ipairs(rows) do minZ = minZ and math.min(minZ, r.z) or r.z maxZ = maxZ and math.max(maxZ, r.z) or r.z end local midZ = (minZ and maxZ) and ((minZ + maxZ) / 2) or 0 for _, r in ipairs(rows) do if r.z < midZ then negY, negN = negY + r.y, negN + 1 else posY, posN = posY + r.y, posN + 1 end end if negN > 0 and posN > 0 then local negMean, posMean = negY / negN, posY / posN log( string.format( " shape cue: mean height -Z half = %+.2f (n=%d) +Z half = %+.2f (n=%d)", negMean, negN, posMean, posN ) ) local tallEnd = (negMean > posMean) and "-Z" or "+Z" log( string.format( " taller (cabin) end = %s => nose is likely %s", tallEnd, (tallEnd == "-Z") and "+Z" or "-Z" ) ) else log(" shape cue: unavailable") end log("") end return table.concat(out, "\n")