Improve SCUM map playback and vehicle markers

This commit is contained in:
npc0-hue
2026-09-18 15:53:32 +08:00
parent 4009b7260e
commit 156392df1e
7 changed files with 432 additions and 55 deletions
@@ -5135,10 +5135,23 @@ export function scumCatalogIconUrl(icon: string): string {
export const scumCatalogByCommand: ReadonlyMap<string, ScumCatalogEntry> = new Map(scumCatalogEntries.map((entry) => [entry.command.toLowerCase(), entry]));
export const scumCatalogByCode: ReadonlyMap<string, ScumCatalogEntry> = new Map(scumCatalogEntries.map((entry) => [entry.code.toLowerCase(), entry]));
function vehicleAliasKeys(code: string): string[] {
const normalized = code.replace(/^#spawnvehicle\s+/i, "").trim();
const withoutVariant = normalized.replace(/_([a-z])$/i, "");
return [...new Set([normalized, withoutVariant, withoutVariant.replace(/_([a-z])$/i, "")].map((value) => value.toLowerCase()))];
// The live server reports spawned vehicles as "<name>_ES" (the collector's VehicleClassSuffix),
// while the catalog keys them as "BP_<name>" / "BPC_<name>". UE object paths add a package prefix.
// Every key below is derived from the same code so both sides meet on the bare vehicle name.
export function vehicleAliasKeys(code: string): string[] {
const normalized = (code.replace(/^#spawnvehicle\s+/i, "").trim().split(".").pop() ?? "").trim();
if (!normalized) return [];
const withoutSuffix = normalized.replace(/_(?:ES|C)$/i, "");
const bare = withoutSuffix.replace(/^(?:BPC_|BP_)/i, "");
const variants = new Set([normalized, withoutSuffix, bare]);
const keys = new Set<string>();
for (const value of variants) {
const base = value.replace(/^(?:bpc_|bp_)/i, "");
keys.add(value.toLowerCase());
keys.add(`bpc_${base}`.toLowerCase());
keys.add(`bp_${base}`.toLowerCase());
}
return [...keys];
}
const vehicleIconAliases: Record<string, string> = {};
@@ -5155,23 +5168,83 @@ export function scumVehicleIconPath(vehicleCode: string): string {
// Colour variants and re-releases ship no icon of their own; they render the icon of their base vehicle.
const scumVehicleFamilyIcons: ReadonlyArray<readonly [string, string]> = [
["bp_dirtbike", "items/vehicles/ico_motorcycle_01_a.webp"],
["bp_cruiser", "items/vehicles/ico_cruiser.webp"],
["bp_bicycle_mountain_bike", "items/vehicles/ico_bicycle_02_a.webp"],
["bpc_bicycle_mountain_bike", "items/vehicles/ico_bicycle_02_a.webp"],
["bpc_bicycle_citybike", "items/vehicles/ico_bicycle_01_a.webp"],
["bpc_motorboat_01", "items/vehicles/ico_motorboat_02.webp"],
["bp_kinglet_scout", "items/vehicles/ico_kinglet_duster_a.webp"],
["dirtbike", "items/vehicles/ico_motorcycle_01_a.webp"],
["sportbike", "items/vehicles/ico_motorcycle_01_a.webp"],
["cruiser", "items/vehicles/ico_cruiser.webp"],
["citybike", "items/vehicles/ico_bicycle_01_a.webp"],
["mountainbike", "items/vehicles/ico_bicycle_02_a.webp"],
["bicycle_mountain_bike", "items/vehicles/ico_bicycle_02_a.webp"],
["bicycle_citybike", "items/vehicles/ico_bicycle_01_a.webp"],
["sidecarbike", "items/vehicles/ico_sidecarbike.webp"],
["motorboat_01", "items/vehicles/ico_motorboat_02.webp"],
["barba", "items/vehicles/motoredboat_a.webp"],
["dinghy", "items/vehicles/ico_motorboat_02.webp"],
["sup", "items/vehicles/ico_sup_vicinity.webp"],
["bigraft", "items/vehicles/ico_improraftbig.webp"],
["smallraft", "items/vehicles/ico_improsmallraft.webp"],
["wheelbarrow_improvised", "items/vehicles/ico_improwheelbarrow.webp"],
["wheelbarrow_metal", "items/vehicles/ico_wheelbarrow.webp"],
["wheelbarrow", "items/vehicles/ico_wheelbarrow.webp"],
["laika", "items/vehicles/ico_laika.webp"],
["rager", "items/vehicles/ico_rager.webp"],
["ris", "items/vehicles/ico_ris.webp"],
["wolfswagen", "items/vehicles/ico_wolfswagen.webp"],
["tractor", "items/vehicles/ico_tractor_01_a.webp"],
["kinglet_scout", "items/vehicles/ico_kinglet_duster_a.webp"],
["kinglet_duster", "items/vehicles/ico_kinglet_duster_a.webp"],
["kinglet_mariner", "items/vehicles/ico_kinglet_mariner.webp"],
];
export function scumVehicleIconUrl(vehicleCode: string): string {
const direct = scumVehicleIconPath(vehicleCode);
if (direct) return scumCatalogIconUrl(direct);
const key = vehicleCode.replace(/^#spawnvehicle\s+/i, "").trim().toLowerCase();
for (const [prefix, icon] of scumVehicleFamilyIcons) { if (key.startsWith(prefix)) return scumCatalogIconUrl(icon); }
function scumVehicleFamilyIcon(vehicleCode: string): string {
const key = (vehicleCode.replace(/^#spawnvehicle\s+/i, "").trim().split(".").pop() ?? "").toLowerCase().replace(/^(bpc_|bp_)/, "");
for (const [prefix, icon] of scumVehicleFamilyIcons) { if (key === prefix || key.startsWith(`${prefix}_`)) return icon; }
return "";
}
export function scumVehicleIconPathFor(vehicleCode: string): string {
const direct = scumVehicleIconPath(vehicleCode);
if (direct) return direct;
return scumVehicleFamilyIcon(vehicleCode);
}
export function scumVehicleIconUrl(vehicleCode: string): string {
const path = scumVehicleIconPathFor(vehicleCode);
return path ? scumCatalogIconUrl(path) : "";
}
// Vehicles whose class ships no icon of its own (Pickup / Quad / SUV / Sportbike and any future
// re-release) still need a marker. These inline glyphs cost no extra request and never 404.
const scumVehicleFallbackGlyphs: ReadonlyArray<readonly [string, string]> = [
["boat", "M3 15h18l-2 5H5l-2-5Zm3 0 3-7h6l3 7M8 11V8h8v3"],
["bike", "M5 18a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm14 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM8 15l3-6h4l2 6M11 9h3"],
["motorcycle", "M5 18a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm14 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM8 15l3-5h3l2 1 3 4M11 10h4"],
["cart", "M4 6h11l2 8H6L4 6Zm2 12a1.6 1.6 0 1 0 0-3.2A1.6 1.6 0 0 0 6 18Zm10 0a1.6 1.6 0 1 0 0-3.2A1.6 1.6 0 0 0 16 18Z"],
["car", "M4 16v-3l2-5h12l2 5v3M4 16h16M6.5 16a1.5 1.5 0 1 0 0 .01M17.5 16a1.5 1.5 0 1 0 0 .01M4 13h16"],
];
export function scumVehicleFallbackIconUrl(vehicleCode: string): string {
const key = (vehicleCode.replace(/^#spawnvehicle\s+/i, "").trim().split(".").pop() ?? "").toLowerCase();
const family = scumVehicleFamilyIcon(vehicleCode);
const kind = family.includes("boat") || family.includes("raft") || family.includes("sup") || /boat|raft|dinghy|sup|barba/.test(key) ? "boat"
: family.includes("bicycle") ? "bike"
: family.includes("motorcycle") || family.includes("sidecarbike") || /bike|motorcycle/.test(key) ? "motorcycle"
: family.includes("wheelbarrow") || /wheelbarrow/.test(key) ? "cart"
: "car";
const glyph = scumVehicleFallbackGlyphs.find(([name]) => name === kind)?.[1] ?? "";
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="${glyph}"/></svg>`;
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}
export function scumVehicleEntry(vehicleCode: string): ScumCatalogEntry | undefined {
for (const key of vehicleAliasKeys(vehicleCode)) {
const icon = vehicleIconAliases[key];
if (!icon) continue;
const entry = scumCatalogEntries.find((value) => value.kind === "vehicle" && value.icon === icon);
if (entry) return entry;
}
return scumCatalogEntryFor(vehicleCode);
}
export function scumCatalogEntryFor(value: string): ScumCatalogEntry | undefined {
const key = value.trim().toLowerCase();
return scumCatalogByCommand.get(key) ?? scumCatalogByCode.get(key) ?? scumCatalogByCommand.get(`#spawnitem ${key}`) ?? scumCatalogByCommand.get(`#spawnvehicle ${key}`);