- replace placeholder garage interaction with real UI open flow - add catalog/session/UI bridge services for hydrate, sync, store, and retrieve actions - migrate garage web UI bundle to new app shell/runtime structure - align org/store function naming with shared init and UI bridge patterns
1292 lines
44 KiB
JavaScript
1292 lines
44 KiB
JavaScript
/* Generated by tools/build-webui.mjs for Garage UI app. Do not edit directly. */
|
|
(function () {
|
|
const runtime = window.ForgeWebUI;
|
|
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
|
|
|
GarageApp.runtime = runtime;
|
|
window.AppRuntime = runtime;
|
|
})();
|
|
|
|
(function () {
|
|
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
|
|
|
const defaultSession = {
|
|
garageName: "Vehicle Garage",
|
|
capacityUsed: 0,
|
|
capacityMax: 5,
|
|
nearbyCount: 0,
|
|
spawnBlocked: false,
|
|
spawnStatus: "Ready",
|
|
};
|
|
|
|
const defaultGarage = {
|
|
vehicles: [],
|
|
};
|
|
|
|
const defaultNearby = {
|
|
vehicles: [],
|
|
};
|
|
|
|
function cloneValue(value) {
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function replaceObject(target, source) {
|
|
Object.keys(target).forEach((key) => delete target[key]);
|
|
Object.assign(target, cloneValue(source));
|
|
}
|
|
|
|
GarageApp.data = {
|
|
categories: [
|
|
{ id: "all", label: "All" },
|
|
{ id: "car", label: "Cars" },
|
|
{ id: "armor", label: "Armor" },
|
|
{ id: "air", label: "Air" },
|
|
{ id: "naval", label: "Naval" },
|
|
{ id: "other", label: "Other" },
|
|
],
|
|
session: Object.assign({}, defaultSession),
|
|
garage: Object.assign({}, defaultGarage),
|
|
nearby: Object.assign({}, defaultNearby),
|
|
applyHydratePayload(payload) {
|
|
replaceObject(
|
|
this.session,
|
|
Object.assign({}, defaultSession, payload?.session || {}),
|
|
);
|
|
replaceObject(
|
|
this.garage,
|
|
Object.assign({}, defaultGarage, payload?.garage || {}),
|
|
);
|
|
replaceObject(
|
|
this.nearby,
|
|
Object.assign({}, defaultNearby, payload?.nearby || {}),
|
|
);
|
|
},
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
|
const { createSignal } = GarageApp.runtime;
|
|
|
|
class GarageStore {
|
|
constructor() {
|
|
[this.getSelectedKind, this.setSelectedKind] = createSignal("");
|
|
[this.getSelectedId, this.setSelectedId] = createSignal("");
|
|
[this.getSearchQuery, this.setSearchQuery] = createSignal("");
|
|
[this.getCategoryFilter, this.setCategoryFilter] =
|
|
createSignal("all");
|
|
[this.getPendingAction, this.setPendingAction] = createSignal("");
|
|
[this.getNotice, this.setNotice] = createSignal({
|
|
type: "",
|
|
text: "",
|
|
});
|
|
}
|
|
|
|
getSelection() {
|
|
return {
|
|
id: this.getSelectedId(),
|
|
kind: this.getSelectedKind(),
|
|
};
|
|
}
|
|
|
|
clearSelection() {
|
|
this.setSelectedKind("");
|
|
this.setSelectedId("");
|
|
}
|
|
|
|
select(kind, id) {
|
|
this.setSelectedKind(String(kind || ""));
|
|
this.setSelectedId(String(id || ""));
|
|
}
|
|
|
|
startAction(action) {
|
|
this.setPendingAction(String(action || ""));
|
|
}
|
|
|
|
finishAction() {
|
|
this.setPendingAction("");
|
|
}
|
|
|
|
matchesSelection(entry) {
|
|
if (!entry || typeof entry !== "object") {
|
|
return false;
|
|
}
|
|
|
|
const selection = this.getSelection();
|
|
if (!selection.kind || !selection.id) {
|
|
return false;
|
|
}
|
|
|
|
if (selection.kind === "stored") {
|
|
return (
|
|
entry.entryKind === "stored" &&
|
|
String(entry.plate || "") === selection.id
|
|
);
|
|
}
|
|
|
|
if (selection.kind === "nearby") {
|
|
return (
|
|
entry.entryKind === "nearby" &&
|
|
String(entry.netId || "") === selection.id
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
ensureSelection() {
|
|
const garageVehicles = Array.isArray(
|
|
GarageApp.data?.garage?.vehicles,
|
|
)
|
|
? GarageApp.data.garage.vehicles
|
|
: [];
|
|
const nearbyVehicles = Array.isArray(
|
|
GarageApp.data?.nearby?.vehicles,
|
|
)
|
|
? GarageApp.data.nearby.vehicles
|
|
: [];
|
|
const hasCurrentSelection = [
|
|
...garageVehicles,
|
|
...nearbyVehicles,
|
|
].some((entry) => this.matchesSelection(entry));
|
|
|
|
if (hasCurrentSelection) {
|
|
return;
|
|
}
|
|
|
|
const firstStored = garageVehicles[0] || null;
|
|
if (firstStored) {
|
|
this.select("stored", firstStored.plate || "");
|
|
return;
|
|
}
|
|
|
|
const firstNearby = nearbyVehicles[0] || null;
|
|
if (firstNearby) {
|
|
this.select("nearby", firstNearby.netId || "");
|
|
return;
|
|
}
|
|
|
|
this.clearSelection();
|
|
}
|
|
|
|
hydrateFromPayload() {
|
|
this.finishAction();
|
|
this.ensureSelection();
|
|
}
|
|
}
|
|
|
|
GarageApp.store = new GarageStore();
|
|
})();
|
|
|
|
(function () {
|
|
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
|
const store = GarageApp.store;
|
|
const bridge = window.ForgeWebUI.createBridge({
|
|
closeEvent: "garage::close",
|
|
globalName: "ForgeBridge",
|
|
readyEvent: "garage::ready",
|
|
});
|
|
|
|
function requestClose() {
|
|
return bridge.close({});
|
|
}
|
|
|
|
function requestRefresh() {
|
|
return bridge.send("garage::refresh", {});
|
|
}
|
|
|
|
function requestRetrieve(payload) {
|
|
return bridge.send("garage::vehicle::retrieve::request", payload);
|
|
}
|
|
|
|
function requestStore(payload) {
|
|
return bridge.send("garage::vehicle::store::request", payload);
|
|
}
|
|
|
|
function notifyReady() {
|
|
return bridge.ready({ loaded: true });
|
|
}
|
|
|
|
function hydrate(payloadData) {
|
|
GarageApp.data.applyHydratePayload(payloadData);
|
|
store.hydrateFromPayload(payloadData);
|
|
}
|
|
|
|
bridge.on("garage::hydrate", hydrate);
|
|
bridge.on("garage::sync", hydrate);
|
|
|
|
bridge.on("garage::retrieve::success", (payloadData) => {
|
|
store.finishAction();
|
|
if (GarageApp.actions) {
|
|
GarageApp.actions.showNotice(
|
|
"success",
|
|
payloadData.message || "Vehicle retrieved from the garage.",
|
|
);
|
|
}
|
|
});
|
|
|
|
bridge.on("garage::retrieve::failure", (payloadData) => {
|
|
store.finishAction();
|
|
if (GarageApp.actions) {
|
|
GarageApp.actions.showNotice(
|
|
"error",
|
|
payloadData.message || "Unable to retrieve vehicle.",
|
|
);
|
|
}
|
|
});
|
|
|
|
bridge.on("garage::store::success", (payloadData) => {
|
|
store.finishAction();
|
|
if (GarageApp.actions) {
|
|
GarageApp.actions.showNotice(
|
|
"success",
|
|
payloadData.message || "Vehicle stored in the garage.",
|
|
);
|
|
}
|
|
});
|
|
|
|
bridge.on("garage::store::failure", (payloadData) => {
|
|
store.finishAction();
|
|
if (GarageApp.actions) {
|
|
GarageApp.actions.showNotice(
|
|
"error",
|
|
payloadData.message || "Unable to store vehicle.",
|
|
);
|
|
}
|
|
});
|
|
|
|
GarageApp.bridge = {
|
|
notifyReady,
|
|
receive: bridge.receive,
|
|
requestClose,
|
|
requestRefresh,
|
|
requestRetrieve,
|
|
requestStore,
|
|
sendEvent: bridge.send,
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
|
const store = GarageApp.store;
|
|
|
|
let noticeTimer = null;
|
|
|
|
function getStoredVehicles() {
|
|
return Array.isArray(GarageApp.data?.garage?.vehicles)
|
|
? GarageApp.data.garage.vehicles
|
|
: [];
|
|
}
|
|
|
|
function getNearbyVehicles() {
|
|
return Array.isArray(GarageApp.data?.nearby?.vehicles)
|
|
? GarageApp.data.nearby.vehicles
|
|
: [];
|
|
}
|
|
|
|
function getSelectedEntry() {
|
|
const selection = store.getSelection();
|
|
if (selection.kind === "stored") {
|
|
return (
|
|
getStoredVehicles().find(
|
|
(vehicle) => String(vehicle.plate || "") === selection.id,
|
|
) || null
|
|
);
|
|
}
|
|
|
|
if (selection.kind === "nearby") {
|
|
return (
|
|
getNearbyVehicles().find(
|
|
(vehicle) => String(vehicle.netId || "") === selection.id,
|
|
) || null
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function showNotice(type, text) {
|
|
store.setNotice({ type, text });
|
|
|
|
if (noticeTimer) {
|
|
clearTimeout(noticeTimer);
|
|
}
|
|
|
|
noticeTimer = setTimeout(() => {
|
|
store.setNotice({ type: "", text: "" });
|
|
noticeTimer = null;
|
|
}, 3200);
|
|
}
|
|
|
|
function closeGarage() {
|
|
const bridge = GarageApp.bridge;
|
|
if (bridge && typeof bridge.requestClose === "function") {
|
|
const sent = bridge.requestClose();
|
|
if (sent) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
showNotice("error", "Garage bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
function refreshGarage() {
|
|
const bridge = GarageApp.bridge;
|
|
if (bridge && typeof bridge.requestRefresh === "function") {
|
|
const sent = bridge.requestRefresh();
|
|
if (sent) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
showNotice("error", "Garage refresh bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
function applySearchQuery(value) {
|
|
store.setSearchQuery(String(value || "").trim());
|
|
}
|
|
|
|
function clearSearch() {
|
|
store.setSearchQuery("");
|
|
}
|
|
|
|
function selectCategory(categoryId) {
|
|
store.setCategoryFilter(String(categoryId || "all").trim() || "all");
|
|
}
|
|
|
|
function selectEntry(kind, id) {
|
|
store.select(kind, id);
|
|
}
|
|
|
|
function requestRetrieveSelected() {
|
|
const selectedEntry = getSelectedEntry();
|
|
if (!selectedEntry || selectedEntry.entryKind !== "stored") {
|
|
showNotice("error", "Select a stored vehicle to retrieve.");
|
|
return false;
|
|
}
|
|
|
|
if (GarageApp.data?.session?.spawnBlocked) {
|
|
showNotice("error", "The garage spawn area is blocked.");
|
|
return false;
|
|
}
|
|
|
|
const bridge = GarageApp.bridge;
|
|
if (!bridge || typeof bridge.requestRetrieve !== "function") {
|
|
showNotice("error", "Garage retrieve bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
store.startAction("retrieve");
|
|
const sent = bridge.requestRetrieve({
|
|
plate: selectedEntry.plate || "",
|
|
});
|
|
|
|
if (!sent) {
|
|
store.finishAction();
|
|
showNotice("error", "Garage retrieve bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function requestStoreSelected() {
|
|
const selectedEntry = getSelectedEntry();
|
|
if (!selectedEntry || selectedEntry.entryKind !== "nearby") {
|
|
showNotice("error", "Select a nearby vehicle to store.");
|
|
return false;
|
|
}
|
|
|
|
if (selectedEntry.isEmpty === false) {
|
|
showNotice(
|
|
"error",
|
|
"All crew must exit the vehicle before storing it.",
|
|
);
|
|
return false;
|
|
}
|
|
|
|
const bridge = GarageApp.bridge;
|
|
if (!bridge || typeof bridge.requestStore !== "function") {
|
|
showNotice("error", "Garage store bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
store.startAction("store");
|
|
const sent = bridge.requestStore({
|
|
netId: selectedEntry.netId || "",
|
|
});
|
|
|
|
if (!sent) {
|
|
store.finishAction();
|
|
showNotice("error", "Garage store bridge is unavailable.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
GarageApp.actions = {
|
|
showNotice,
|
|
closeGarage,
|
|
refreshGarage,
|
|
applySearchQuery,
|
|
clearSearch,
|
|
selectCategory,
|
|
selectEntry,
|
|
getSelectedEntry,
|
|
requestRetrieveSelected,
|
|
requestStoreSelected,
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
|
const { h } = GarageApp.runtime;
|
|
const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar;
|
|
const store = GarageApp.store;
|
|
const actions = GarageApp.actions;
|
|
const { categories, garage, nearby, session } = GarageApp.data;
|
|
|
|
function q(query, values) {
|
|
const needle = String(query || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
if (!needle) {
|
|
return true;
|
|
}
|
|
|
|
return values.some((value) =>
|
|
String(value || "")
|
|
.toLowerCase()
|
|
.includes(needle),
|
|
);
|
|
}
|
|
|
|
function pct(value) {
|
|
return Math.max(0, Math.min(100, Math.round(Number(value || 0) * 100)));
|
|
}
|
|
|
|
function categoryLabel(category) {
|
|
const match = categories.find(
|
|
(entry) => entry.id === String(category || "other").toLowerCase(),
|
|
);
|
|
return match ? match.label : "Other";
|
|
}
|
|
|
|
function distanceLabel(value) {
|
|
return `${Math.round(Number(value || 0))} m`;
|
|
}
|
|
|
|
function plateLabel(value) {
|
|
return String(value || "").trim() || "Untracked";
|
|
}
|
|
|
|
function statusLabel(vehicle) {
|
|
if (!vehicle) {
|
|
return "-";
|
|
}
|
|
|
|
if (vehicle.entryKind === "stored") {
|
|
return "Stored";
|
|
}
|
|
|
|
return vehicle.isEmpty === false ? "Crewed" : "Ready";
|
|
}
|
|
|
|
function normalizeHitPointLabel(value) {
|
|
return String(value || "")
|
|
.replace(/^Hit/i, "")
|
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
.replace(/_/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
function sameEntry(left, right) {
|
|
if (!left || !right) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
String(left.entryKind || "") === String(right.entryKind || "") &&
|
|
String(left.plate || "") === String(right.plate || "") &&
|
|
String(left.netId || "") === String(right.netId || "")
|
|
);
|
|
}
|
|
|
|
function selectedEntry(state) {
|
|
if (state.selectedKind === "stored") {
|
|
return (
|
|
(garage.vehicles || []).find(
|
|
(vehicle) =>
|
|
String(vehicle.plate || "") === state.selectedId,
|
|
) || null
|
|
);
|
|
}
|
|
|
|
if (state.selectedKind === "nearby") {
|
|
return (
|
|
(nearby.vehicles || []).find(
|
|
(vehicle) =>
|
|
String(vehicle.netId || "") === state.selectedId,
|
|
) || null
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function visibleVehicles(vehicles, state) {
|
|
return (vehicles || []).filter((vehicle) => {
|
|
if (
|
|
state.categoryFilter !== "all" &&
|
|
String(vehicle.category || "").toLowerCase() !==
|
|
state.categoryFilter
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return q(state.searchQuery, [
|
|
vehicle.displayName,
|
|
vehicle.classname,
|
|
vehicle.plate,
|
|
vehicle.netId,
|
|
vehicle.category,
|
|
]);
|
|
});
|
|
}
|
|
|
|
function stat(label, value, tone = "") {
|
|
return h(
|
|
"div",
|
|
{
|
|
className: tone
|
|
? `garage-stat-card is-${tone}`
|
|
: "garage-stat-card",
|
|
},
|
|
h("span", { className: "garage-stat-label" }, label),
|
|
h("span", { className: "garage-stat-value" }, value),
|
|
);
|
|
}
|
|
|
|
function meter(label, percent, tone) {
|
|
return h(
|
|
"div",
|
|
{ className: "garage-meter" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-meter-label-row" },
|
|
h("span", { className: "garage-meter-label" }, label),
|
|
h("span", { className: "garage-meter-value" }, `${percent}%`),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-meter-track" },
|
|
h("span", {
|
|
className: `garage-meter-fill is-${tone}`,
|
|
style: { width: `${percent}%` },
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
function vehicleItem(vehicle, currentSelection) {
|
|
const id =
|
|
vehicle.entryKind === "stored"
|
|
? String(vehicle.plate || "")
|
|
: String(vehicle.netId || "");
|
|
const isNearby = vehicle.entryKind === "nearby";
|
|
|
|
return h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: sameEntry(vehicle, currentSelection)
|
|
? "garage-vehicle-item is-selected"
|
|
: "garage-vehicle-item",
|
|
onClick: () => actions.selectEntry(vehicle.entryKind, id),
|
|
},
|
|
h(
|
|
"div",
|
|
{ className: "garage-vehicle-item-head" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-vehicle-copy" },
|
|
h(
|
|
"span",
|
|
{ className: "garage-vehicle-title" },
|
|
vehicle.displayName || vehicle.classname || "Vehicle",
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "garage-vehicle-meta" },
|
|
isNearby
|
|
? `Nearby ${distanceLabel(vehicle.distance)}`
|
|
: `Plate ${plateLabel(vehicle.plate)}`,
|
|
),
|
|
),
|
|
h(
|
|
"span",
|
|
{
|
|
className:
|
|
isNearby && vehicle.isEmpty === false
|
|
? "garage-badge is-warning"
|
|
: "garage-badge",
|
|
},
|
|
isNearby
|
|
? vehicle.isEmpty === false
|
|
? "Crewed"
|
|
: "Empty"
|
|
: categoryLabel(vehicle.category),
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-inline-meters" },
|
|
meter("Health", pct(vehicle.health), "health"),
|
|
meter("Fuel", pct(vehicle.fuel), "fuel"),
|
|
),
|
|
);
|
|
}
|
|
|
|
function vehicleList(title, eyebrow, scrollId, vehicles, currentSelection) {
|
|
return h(
|
|
"section",
|
|
{ className: "garage-card garage-list-card" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-card-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "garage-eyebrow" }, eyebrow),
|
|
h("h2", { className: "garage-section-title" }, title),
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "garage-pill" },
|
|
`${vehicles.length} ${vehicles.length === 1 ? "Vehicle" : "Vehicles"}`,
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{
|
|
className: "garage-card-body garage-scroll-body",
|
|
"data-preserve-scroll-id": scrollId,
|
|
},
|
|
vehicles.length > 0
|
|
? vehicles.map((vehicle) =>
|
|
vehicleItem(vehicle, currentSelection),
|
|
)
|
|
: h(
|
|
"div",
|
|
{ className: "garage-empty-state" },
|
|
h(
|
|
"h3",
|
|
{ className: "garage-empty-title" },
|
|
"No matching vehicles",
|
|
),
|
|
h(
|
|
"p",
|
|
{ className: "garage-empty-copy" },
|
|
"Adjust the current search or category filter to view more records.",
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function hitPointRows(hitPoints) {
|
|
const rows = (Array.isArray(hitPoints) ? hitPoints : [])
|
|
.slice()
|
|
.sort(
|
|
(left, right) =>
|
|
Number(right.value || 0) - Number(left.value || 0),
|
|
)
|
|
.slice(0, 6)
|
|
.filter((row) => Number(row.value || 0) > 0);
|
|
|
|
if (rows.length === 0) {
|
|
return h(
|
|
"div",
|
|
{ className: "garage-empty-inline" },
|
|
"No subsystem damage reported.",
|
|
);
|
|
}
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "garage-hitpoint-grid" },
|
|
rows.map((row) =>
|
|
h(
|
|
"div",
|
|
{ className: "garage-hitpoint-row" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-hitpoint-copy" },
|
|
h(
|
|
"span",
|
|
{ className: "garage-hitpoint-name" },
|
|
normalizeHitPointLabel(row.name) || "Subsystem",
|
|
),
|
|
row.selection
|
|
? h(
|
|
"span",
|
|
{ className: "garage-hitpoint-selection" },
|
|
row.selection,
|
|
)
|
|
: null,
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "garage-hitpoint-value" },
|
|
`${Math.round(Number(row.value || 0) * 100)}%`,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function detailPanel(currentSelection, state) {
|
|
if (!currentSelection) {
|
|
return h(
|
|
"section",
|
|
{ className: "garage-card garage-detail-card" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-card-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("span", { className: "garage-eyebrow" }, "Selection"),
|
|
h(
|
|
"h2",
|
|
{ className: "garage-section-title" },
|
|
"Vehicle Detail",
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-card-body garage-detail-empty" },
|
|
h(
|
|
"h3",
|
|
{ className: "garage-empty-title" },
|
|
"Select a vehicle",
|
|
),
|
|
h(
|
|
"p",
|
|
{ className: "garage-empty-copy" },
|
|
"Choose a stored record to retrieve or a nearby vehicle to store.",
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
const isStored = currentSelection.entryKind === "stored";
|
|
const pendingAction = String(state.pendingAction || "");
|
|
const isBusy =
|
|
pendingAction === "retrieve" || pendingAction === "store";
|
|
const canRetrieve = isStored && !session.spawnBlocked && !isBusy;
|
|
const canStore =
|
|
!isStored && currentSelection.isEmpty !== false && !isBusy;
|
|
|
|
return h(
|
|
"section",
|
|
{ className: "garage-card garage-detail-card" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-card-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h(
|
|
"span",
|
|
{ className: "garage-eyebrow" },
|
|
isStored ? "Stored Record" : "Nearby Vehicle",
|
|
),
|
|
h(
|
|
"h2",
|
|
{ className: "garage-section-title" },
|
|
currentSelection.displayName ||
|
|
currentSelection.classname ||
|
|
"Vehicle",
|
|
),
|
|
),
|
|
h(
|
|
"span",
|
|
{
|
|
className:
|
|
currentSelection.entryKind === "nearby" &&
|
|
currentSelection.isEmpty === false
|
|
? "garage-badge is-warning"
|
|
: "garage-badge",
|
|
},
|
|
isStored
|
|
? `Plate ${plateLabel(currentSelection.plate)}`
|
|
: currentSelection.isEmpty === false
|
|
? "Crewed"
|
|
: "Ready",
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-card-body garage-detail-body" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-detail-grid" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-detail-copy" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-detail-meta" },
|
|
stat(
|
|
"Category",
|
|
categoryLabel(currentSelection.category),
|
|
),
|
|
stat(
|
|
"Status",
|
|
statusLabel(currentSelection),
|
|
currentSelection.entryKind === "nearby" &&
|
|
currentSelection.isEmpty === false
|
|
? "danger"
|
|
: "",
|
|
),
|
|
stat(
|
|
isStored ? "Record" : "Distance",
|
|
isStored
|
|
? plateLabel(currentSelection.plate)
|
|
: distanceLabel(currentSelection.distance),
|
|
isStored ? "" : "accent",
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-meter-stack" },
|
|
meter(
|
|
"Health",
|
|
pct(currentSelection.health),
|
|
"health",
|
|
),
|
|
meter("Fuel", pct(currentSelection.fuel), "fuel"),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-action-row" },
|
|
isStored
|
|
? h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className:
|
|
"garage-btn garage-btn-primary",
|
|
disabled: !canRetrieve,
|
|
onClick: () =>
|
|
actions.requestRetrieveSelected(),
|
|
},
|
|
pendingAction === "retrieve"
|
|
? "Retrieving..."
|
|
: "Retrieve Vehicle",
|
|
)
|
|
: h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className:
|
|
"garage-btn garage-btn-primary",
|
|
disabled: !canStore,
|
|
onClick: () =>
|
|
actions.requestStoreSelected(),
|
|
},
|
|
pendingAction === "store"
|
|
? "Storing..."
|
|
: "Store Vehicle",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className:
|
|
"garage-btn garage-btn-secondary",
|
|
disabled: isBusy,
|
|
onClick: () => actions.refreshGarage(),
|
|
},
|
|
"Refresh",
|
|
),
|
|
),
|
|
h(
|
|
"p",
|
|
{ className: "garage-detail-note" },
|
|
isStored
|
|
? session.spawnBlocked
|
|
? "The garage spawn lane is currently blocked."
|
|
: "Retrieve this stored vehicle into the active spawn lane."
|
|
: currentSelection.isEmpty === false
|
|
? "Only empty nearby vehicles can be stored."
|
|
: "Store this nearby vehicle back into persistent garage storage.",
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-detail-subsystems" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-subsystem-header" },
|
|
h(
|
|
"span",
|
|
{ className: "garage-eyebrow" },
|
|
"Subsystems",
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "garage-detail-caption" },
|
|
"Highest damage first",
|
|
),
|
|
),
|
|
hitPointRows(currentSelection.hitPoints),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
GarageApp.components = GarageApp.components || {};
|
|
GarageApp.components.App = function App() {
|
|
const state = {
|
|
categoryFilter: store.getCategoryFilter(),
|
|
notice: store.getNotice(),
|
|
pendingAction: store.getPendingAction(),
|
|
searchQuery: store.getSearchQuery(),
|
|
selectedId: store.getSelectedId(),
|
|
selectedKind: store.getSelectedKind(),
|
|
};
|
|
const currentSelection = selectedEntry(state);
|
|
const storedVehicles = visibleVehicles(garage.vehicles || [], state);
|
|
const nearbyVehicles = visibleVehicles(nearby.vehicles || [], state);
|
|
const searchLabel = state.searchQuery
|
|
? `Search: ${state.searchQuery}`
|
|
: "Live";
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "garage-shell" },
|
|
WindowTitleBar({
|
|
kicker: "FORGE Logistics",
|
|
title: "Vehicle Garage",
|
|
onClose: () => actions.closeGarage(),
|
|
closeLabel: "Close garage interface",
|
|
}),
|
|
state.notice.text
|
|
? h(
|
|
"div",
|
|
{ className: "garage-toast-stack" },
|
|
h(
|
|
"div",
|
|
{
|
|
className:
|
|
state.notice.type === "error"
|
|
? "garage-toast is-error"
|
|
: "garage-toast is-success",
|
|
},
|
|
state.notice.text,
|
|
),
|
|
)
|
|
: null,
|
|
h(
|
|
"div",
|
|
{ className: "garage-layout" },
|
|
h(
|
|
"aside",
|
|
{ className: "garage-sidebar" },
|
|
h(
|
|
"section",
|
|
{ className: "garage-module" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-module-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h(
|
|
"span",
|
|
{ className: "garage-eyebrow" },
|
|
"Search",
|
|
),
|
|
h(
|
|
"h2",
|
|
{ className: "garage-section-title" },
|
|
"Vehicle Records",
|
|
),
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "garage-pill" },
|
|
searchLabel,
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-search-form" },
|
|
h("input", {
|
|
id: "garage-search-input",
|
|
type: "text",
|
|
className: "garage-search-input",
|
|
placeholder:
|
|
"Search by name, plate, or category",
|
|
value: state.searchQuery,
|
|
}),
|
|
h(
|
|
"div",
|
|
{ className: "garage-search-actions" },
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className:
|
|
"garage-btn garage-btn-primary",
|
|
onClick: () =>
|
|
actions.applySearchQuery(
|
|
document.getElementById(
|
|
"garage-search-input",
|
|
)?.value || "",
|
|
),
|
|
},
|
|
"Apply Search",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className:
|
|
"garage-btn garage-btn-secondary",
|
|
onClick: () => actions.clearSearch(),
|
|
},
|
|
"Clear",
|
|
),
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"section",
|
|
{ className: "garage-module" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-module-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h(
|
|
"span",
|
|
{ className: "garage-eyebrow" },
|
|
"Filter",
|
|
),
|
|
h(
|
|
"h2",
|
|
{ className: "garage-section-title" },
|
|
"Vehicle Categories",
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-category-grid" },
|
|
categories.map((category) =>
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className:
|
|
state.categoryFilter === category.id
|
|
? "garage-chip is-active"
|
|
: "garage-chip",
|
|
onClick: () =>
|
|
actions.selectCategory(category.id),
|
|
},
|
|
category.label,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"section",
|
|
{ className: "garage-module" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-module-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h(
|
|
"span",
|
|
{ className: "garage-eyebrow" },
|
|
"Status",
|
|
),
|
|
h(
|
|
"h2",
|
|
{ className: "garage-section-title" },
|
|
"Garage Summary",
|
|
),
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className:
|
|
"garage-btn garage-btn-secondary",
|
|
disabled: Boolean(state.pendingAction),
|
|
onClick: () => actions.refreshGarage(),
|
|
},
|
|
"Refresh",
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-summary-grid" },
|
|
stat(
|
|
"Stored",
|
|
`${session.capacityUsed}/${session.capacityMax}`,
|
|
),
|
|
stat("Nearby", session.nearbyCount, "accent"),
|
|
stat(
|
|
"Spawn Lane",
|
|
session.spawnStatus,
|
|
session.spawnBlocked ? "danger" : "",
|
|
),
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"main",
|
|
{ className: "garage-main" },
|
|
h(
|
|
"section",
|
|
{ className: "garage-panel" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-panel-header" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h(
|
|
"span",
|
|
{ className: "garage-eyebrow" },
|
|
"Operations Bay",
|
|
),
|
|
h(
|
|
"h1",
|
|
{ className: "garage-title" },
|
|
session.garageName || "Vehicle Garage",
|
|
),
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "garage-pill" },
|
|
`${session.capacityUsed}/${session.capacityMax} Stored`,
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-panel-intro" },
|
|
h(
|
|
"p",
|
|
{ className: "garage-copy" },
|
|
"Retrieve stored vehicles into the active spawn lane or store nearby empty vehicles back into persistent ownership records.",
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-dashboard" },
|
|
vehicleList(
|
|
"Stored Vehicles",
|
|
"Persistent Records",
|
|
"garage-stored-list",
|
|
storedVehicles,
|
|
currentSelection,
|
|
),
|
|
vehicleList(
|
|
"Nearby Vehicles",
|
|
"Store Window",
|
|
"garage-nearby-list",
|
|
nearbyVehicles,
|
|
currentSelection,
|
|
),
|
|
detailPanel(currentSelection, state),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"footer",
|
|
{ className: "garage-footer" },
|
|
h(
|
|
"div",
|
|
{ className: "garage-footer-block" },
|
|
h(
|
|
"span",
|
|
{ className: "garage-footer-title" },
|
|
"Storage Capacity",
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "garage-footer-copy" },
|
|
`${session.capacityUsed} of ${session.capacityMax} vehicle slot(s) are currently occupied.`,
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-footer-block" },
|
|
h(
|
|
"span",
|
|
{ className: "garage-footer-title" },
|
|
"Retrieval Window",
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "garage-footer-copy" },
|
|
session.spawnBlocked
|
|
? "Spawn lane is blocked. Clear the bay before retrieving another vehicle."
|
|
: "Spawn lane is clear. Stored vehicles can be retrieved immediately.",
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "garage-footer-block" },
|
|
h(
|
|
"span",
|
|
{ className: "garage-footer-title" },
|
|
"Store Rules",
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "garage-footer-copy" },
|
|
"Only nearby empty vehicles can be stored. Nearby count updates from the live world state.",
|
|
),
|
|
),
|
|
),
|
|
);
|
|
};
|
|
})();
|
|
|
|
(function () {
|
|
const ForgeWebUI = window.ForgeWebUI;
|
|
const GarageApp = window.GarageApp;
|
|
const app = ForgeWebUI.createApp({
|
|
name: "garage",
|
|
root: "#app",
|
|
setup({ root }) {
|
|
ForgeWebUI.mount(root, () => GarageApp.components.App(), {
|
|
preserveScroll: true,
|
|
});
|
|
|
|
if (GarageApp.bridge) {
|
|
GarageApp.bridge.notifyReady();
|
|
}
|
|
},
|
|
});
|
|
|
|
app.start();
|
|
})();
|