\n `},updateDangerAlert(){const e=document.getElementById("cadDangerAlert");if(!e)return;if(!this.isDispatchMapMode())return e.textContent="",void e.classList.add("is-hidden");const t=this.getDangerGroups();if(!t.length)return e.textContent="",void e.classList.add("is-hidden");const s=t.map(e=>e.callsign||e.groupId||"Unknown Group");e.textContent=`Danger alert active: ${s.join(", ")}`,e.classList.remove("is-hidden")},updateRequestAlert(){const e=document.getElementById("cadRequestAlert");if(!e)return;if(!this.isDispatchMapMode())return e.textContent="",void e.classList.add("is-hidden");const t=this.buildSupportAlertMessage();if(!t)return e.textContent="",void e.classList.add("is-hidden");e.textContent=t,e.classList.remove("is-hidden")},clearFocusStatusSoon(e){this.focusStatusTimer&&window.clearTimeout(this.focusStatusTimer),this.focusStatusTimer=window.setTimeout(()=>{const t=document.getElementById("cadStatusMessage");t&&"info"===t.dataset.type&&t.textContent===e&&this.setStatus("","")},1800)},handleServerResponse(e,t){this.setStatus(t||(e?"CAD update succeeded.":"CAD update failed."),e?"success":"error")},acknowledgeTask(e){this.setStatus("Acknowledging contract...","info"),window.mapUI.sendEvent("cad::tasks::acknowledge",{taskID:e})},declineTask(e){this.setStatus("Declining contract...","info"),window.mapUI.sendEvent("cad::tasks::decline",{taskID:e})},updateGroupStatus(e,t){this.setStatus("Updating group status...","info"),window.mapUI.sendEvent("cad::groups::status",{groupID:e,status:t})},updateGroupRole(e,t){this.setStatus("Updating group role...","info"),window.mapUI.sendEvent("cad::groups::role",{groupID:e,role:t})},focusGroup(e){const t=this.groups.find(t=>t.groupId===e);if(!t)return void this.setStatus("Selected group is no longer available.","error");this.selectedDispatchGroupId=e,this.selectedDispatchTaskId="",this.selectedDispatchRequestId="",this.selectedRosterMemberUid="";const s=`Centering map on ${t.callsign||t.groupId||"group"}...`;this.setStatus(s,"info"),this.clearFocusStatusSoon(s),window.mapUI.sendEvent("cad::groups::focus",{groupID:e}),this.render()},focusMember(e){let t=null;if(this.groups.some(s=>this.normalizeCollection(s.members).some(s=>(s.uid||"")===e&&(t=s,!0))),!t)return void this.setStatus("Selected group member is no longer available.","error");if((Array.isArray(t.position)?t.position:[]).length<2)return void this.setStatus("Selected group member has no map position.","error");this.selectedRosterMemberUid=e,this.selectedDispatchGroupId="",this.selectedDispatchTaskId="",this.selectedDispatchRequestId="";const s=`Centering map on ${t.name||"group member"}...`;this.setStatus(s,"info"),this.clearFocusStatusSoon(s),window.mapUI.sendEvent("cad::members::focus",{uid:e}),this.render()},focusTask(e){const t=this.contracts.find(t=>(t.taskId||t.taskID||"")===e);if(!t)return void this.setStatus("Selected contract is no longer available.","error");this.selectedDispatchTaskId=e,this.selectedDispatchGroupId="",this.selectedDispatchRequestId="",this.selectedRosterMemberUid="";const s=`Centering map on ${t.title||e}...`;this.setStatus(s,"info"),this.clearFocusStatusSoon(s),window.mapUI.sendEvent("cad::tasks::focus",{taskID:e}),this.render()},focusRequest(e){const t=this.requests.find(t=>(t.requestId||"")===e);if(!t)return void this.setStatus("Selected request is no longer available.","error");if((Array.isArray(t.position)?t.position:[]).length<2)return void this.setStatus("Selected request has no map position.","error");this.selectedDispatchRequestId=e,this.selectedDispatchGroupId="",this.selectedDispatchTaskId="",this.selectedRosterMemberUid="";const s=`Centering map on ${t.title||e}...`;this.setStatus(s,"info"),this.clearFocusStatusSoon(s),window.mapUI.sendEvent("cad::requests::focus",{requestID:e}),this.render()},getPlayerGroupId(){return this.session.groupId||""},getCurrentGroup(){const e=this.getPlayerGroupId();return this.groups.find(t=>t.groupId===e)||null},normalizeCollection:e=>Array.isArray(e)?e:e&&"object"==typeof e?Object.values(e):[],canDispatch(){return!!this.session.isDispatcher},isDispatchMode(){return"dispatch"===this.mode},isDispatchMapMode(){return"dispatch"===this.mode&&"map"===this.dispatchView},isLeader(){return!!this.session.isLeader},renderContracts(){const e=document.getElementById("taskList");if(!e)return;if(this.isDispatchMapMode()){if(!this.contracts.length)return void(e.innerHTML='
${member.name || "Unknown Operator"}${lifeState}
diff --git a/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf b/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf
index f437877..0257e43 100644
--- a/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf
+++ b/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf
@@ -63,6 +63,11 @@ switch (_event) do {
GVAR(GarageActionService) call ["handleRepairRequest", [_data]];
};
};
+ case "garage::vehicle::rearm::request": {
+ if !(isNil QGVAR(GarageActionService)) then {
+ GVAR(GarageActionService) call ["handleRearmRequest", [_data]];
+ };
+ };
case "garage::refresh": {
if !(isNil QGVAR(GarageUIBridge)) then {
GVAR(GarageUIBridge) call ["refreshGarage", []];
diff --git a/arma/client/addons/garage/functions/fnc_initActionService.sqf b/arma/client/addons/garage/functions/fnc_initActionService.sqf
index 2b45657..7bca7e2 100644
--- a/arma/client/addons/garage/functions/fnc_initActionService.sqf
+++ b/arma/client/addons/garage/functions/fnc_initActionService.sqf
@@ -8,8 +8,8 @@
* Public: No
*
* Description:
- * Initializes the garage action service for retrieve, store, refuel, and
- * repair world actions.
+ * Initializes the garage action service for retrieve, store, refuel, rearm,
+ * and repair world actions.
*
* Arguments:
* None
@@ -184,6 +184,17 @@ GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [
_self call ["refreshAfterService", []];
true
}],
+ ["handleRearmRequest", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _vehicle = _self call ["resolveServiceVehicle", [_data, "rearm"]];
+ if (isNull _vehicle) exitWith { false };
+
+ [SRPC(economy,RearmService), [_vehicle, player, -1]] call CFUNC(serverEvent);
+ _self call ["sendServiceResult", ["rearm", true, "Rearm request sent. Billing result will appear as a notification."]];
+ _self call ["refreshAfterService", []];
+ true
+ }],
["handleActionResponse", compileFinal {
params [["_payload", createHashMap, [createHashMap]]];
diff --git a/arma/client/addons/garage/ui/_site/garage-ui.css b/arma/client/addons/garage/ui/_site/garage-ui.css
index 0f5a3cb..81c436a 100644
--- a/arma/client/addons/garage/ui/_site/garage-ui.css
+++ b/arma/client/addons/garage/ui/_site/garage-ui.css
@@ -1 +1 @@
-:root{--garage-shell-bg:#e4e3df;--garage-surface:#f5f3ef;--garage-surface-alt:#ece8e2;--garage-border:#4a5b6e33;--garage-border-strong:#142e4f2e;--garage-text-main:#1f2d3d;--garage-text-muted:#6a7787;--garage-text-subtle:#8792a0;--garage-accent:#12365d;--garage-accent-soft:#dbe7f3;--garage-accent-line:#12365d1f;--garage-warning:#8f5f26}*{box-sizing:border-box}html,body{width:100%;height:100%;margin:0;overflow:hidden}body{color:var(--garage-text-main);background:var(--garage-shell-bg);font-family:Segoe UI,Trebuchet MS,sans-serif}button,input{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.72}:focus-visible{outline-offset:2px;outline:2px solid #12365d59}#app{width:100%;height:100%}.garage-shell{background:var(--garage-shell-bg);flex-direction:column;width:100%;height:100%;display:flex;overflow:hidden}.garage-layout{flex:1;grid-template-columns:308px minmax(0,1fr);gap:1.25rem;width:min(100%,1613px);min-height:0;margin:0 auto;padding:1.25rem;display:grid}.garage-sidebar,.garage-main{flex-direction:column;gap:1rem;min-height:0;display:flex}.garage-main{overflow:hidden}.garage-module,.garage-panel,.garage-card{background:linear-gradient(180deg, var(--garage-surface) 0%, var(--garage-surface-alt) 100%);border:1px solid var(--garage-border);border-radius:1.35rem}.garage-module,.garage-card{padding:1rem}.garage-module{align-content:start;gap:.85rem;display:grid}.garage-panel{flex-direction:column;flex:auto;min-height:0;display:flex;overflow:hidden}.garage-panel-header,.garage-module-header,.garage-card-header{justify-content:space-between;align-items:center;gap:1rem;display:flex}.garage-panel-header{padding:1rem 1rem 0}.garage-module-header{align-items:flex-start}.garage-panel-intro{border-bottom:1px solid var(--garage-accent-line);padding:0 1rem 1rem}.garage-dashboard{flex:1;grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:stretch;gap:1rem;min-height:0;padding:1rem;display:grid}.garage-list-card,.garage-detail-card{flex-direction:column;min-height:0;display:flex}.garage-detail-card{grid-column:1/-1}.garage-scroll-body{flex:1;gap:.8rem;min-height:20rem;max-height:24rem;padding-right:.2rem;display:grid;overflow:auto}.garage-detail-body{padding-top:.95rem}.garage-detail-grid{grid-template-columns:minmax(0,1.25fr) minmax(280px,.85fr);gap:1rem;display:grid}.garage-detail-meta,.garage-summary-grid,.garage-search-actions,.garage-category-grid,.garage-action-row,.garage-inline-meters,.garage-hitpoint-grid,.garage-footer{gap:.75rem;display:grid}.garage-detail-meta{grid-template-columns:repeat(3,minmax(0,1fr));margin-bottom:1rem}.garage-summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.garage-summary-grid>:last-child{grid-column:1/-1}.garage-search-actions,.garage-action-row,.garage-category-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:.65rem}.garage-footer-bar{border-top:1px solid #12365d1a;width:100%}.garage-footer{grid-template-columns:repeat(3,minmax(0,1fr));width:min(100%,1613px);margin:0 auto;padding:.95rem 1.25rem 1.15rem}.garage-meter-stack{gap:.75rem;margin-bottom:1rem;display:grid}.garage-eyebrow,.garage-footer-title,.garage-stat-label,.garage-meter-label,.garage-hitpoint-selection{letter-spacing:.18em;text-transform:uppercase;color:var(--garage-text-subtle);font-size:.68rem;font-weight:700}.garage-title,.garage-section-title{letter-spacing:-.02em;color:var(--garage-text-main);margin:.16rem 0 0;font-weight:700}.garage-title{font-size:1.1rem}.garage-section-title{font-size:1.05rem}.garage-copy,.garage-detail-note,.garage-empty-copy,.garage-footer-copy,.garage-vehicle-meta,.garage-detail-caption{color:var(--garage-text-muted);margin:0;font-size:.92rem;line-height:1.48}.garage-pill,.garage-badge{letter-spacing:.1em;text-transform:uppercase;background:var(--garage-accent-soft);color:var(--garage-accent);border-radius:999px;justify-content:center;align-items:center;padding:.48rem .8rem;font-size:.74rem;font-weight:700;display:inline-flex}.garage-badge.is-warning{color:var(--garage-warning);background:#f6e2c1e0}.garage-search-form{gap:.75rem;display:grid}.garage-search-input{border:1px solid var(--garage-border);width:100%;height:2.9rem;color:var(--garage-text-main);background:#ffffffbf;border-radius:.8rem;padding:0 .95rem}.garage-stat-card{border:1px solid var(--garage-border);background:#ffffff7a;border-radius:.85rem;flex-direction:column;gap:.3rem;min-width:0;padding:.85rem;display:flex}.garage-stat-card.is-accent{background:linear-gradient(#edf3f9eb 0%,#dfe8f2b8 100%)}.garage-stat-card.is-danger{background:linear-gradient(#fef2f2f2 0%,#fce1e1d1 100%);border-color:#dc979761}.garage-stat-value{color:var(--garage-text-main);overflow-wrap:anywhere;word-break:break-word;font-size:1rem;font-weight:700;line-height:1.3}.garage-chip{border:1px solid var(--garage-border);min-height:2.6rem;color:var(--garage-text-muted);letter-spacing:.08em;text-transform:uppercase;background:#ffffff85;border-radius:.85rem;padding:.68rem .9rem;font-size:.8rem;font-weight:700}.garage-chip.is-active{background:var(--garage-accent-soft);color:var(--garage-accent);border-color:#12365d33}.garage-vehicle-item{border:1px solid var(--garage-border);width:100%;color:inherit;text-align:left;background:#ffffff7a;border-radius:.95rem;padding:.9rem}.garage-vehicle-item.is-selected{background:linear-gradient(#edf3f9f5 0%,#dfe8f2bd 100%);border-color:#12365d3d;box-shadow:0 16px 26px #12365d14}.garage-vehicle-item-head,.garage-meter-label-row,.garage-subsystem-header,.garage-hitpoint-row{justify-content:space-between;align-items:center;gap:.75rem;display:flex}.garage-vehicle-copy,.garage-hitpoint-copy,.garage-footer-block{flex-direction:column;gap:.18rem;min-width:0;display:flex}.garage-vehicle-title,.garage-hitpoint-name,.garage-hitpoint-value{color:var(--garage-text-main);font-size:.9rem;font-weight:700}.garage-meter{gap:.32rem;display:grid}.garage-meter-track{background:#12365d14;border-radius:999px;width:100%;height:.45rem;overflow:hidden}.garage-meter-value{color:var(--garage-text-main);font-size:.78rem;font-weight:700}.garage-meter-fill{border-radius:inherit;height:100%;display:block}.garage-meter-fill.is-health{background:linear-gradient(90deg,#2f7d5b 0%,#4eaa82 100%)}.garage-meter-fill.is-fuel{background:linear-gradient(90deg,#12365d 0%,#3c6792 100%)}.garage-btn{border:1px solid var(--garage-border-strong);letter-spacing:.08em;text-transform:uppercase;border-radius:.8rem;min-height:2.75rem;padding:.72rem 1rem;font-size:.82rem;font-weight:700}.garage-btn-primary{color:var(--garage-accent);background:#ffffffad}.garage-btn-primary:hover{background:#dbe7f3e0}.garage-btn-secondary{color:var(--garage-text-muted);background:#ffffff6b}.garage-btn-secondary:hover{color:var(--garage-text-main);background:#fff9}.garage-hitpoint-row{border:1px solid var(--garage-border);background:#ffffff85;border-radius:.85rem;padding:.72rem .78rem}.garage-detail-empty,.garage-empty-state{flex-direction:column;justify-content:center;align-items:flex-start;min-height:100%;display:flex}.garage-empty-title{color:var(--garage-text-main);margin:0 0 .35rem;font-size:1rem;font-weight:700}.garage-empty-inline{border:1px dashed var(--garage-border);color:var(--garage-text-muted);background:#ffffff5c;border-radius:.85rem;padding:.9rem}.garage-toast-stack{z-index:10;flex-direction:column;gap:.65rem;display:flex;position:fixed;top:1.2rem;right:1.5rem}.garage-toast{border:1px solid var(--garage-border);background:#fff;border-radius:.9rem;max-width:24rem;padding:.85rem 1rem;font-size:.92rem;box-shadow:0 14px 28px #10223824}.garage-toast.is-success{color:#166534;background:#ecfdf5;border-color:#bbf7d0}.garage-toast.is-error{color:#991b1b;background:#fef2f2;border-color:#fecaca}@media (width<=1440px){.garage-layout{grid-template-columns:288px minmax(0,1fr)}.garage-detail-grid{grid-template-columns:1fr}}@media (width<=1120px){.garage-layout{grid-template-columns:1fr;overflow:auto}.garage-main,.garage-sidebar{min-height:auto}.garage-dashboard{grid-template-columns:1fr}.garage-detail-card{grid-column:auto}.garage-scroll-body{min-height:16rem;max-height:none}.garage-footer{grid-template-columns:1fr}}
\ No newline at end of file
+:root{--garage-shell-bg:#e4e3df;--garage-surface:#f5f3ef;--garage-surface-alt:#ece8e2;--garage-border:#4a5b6e33;--garage-border-strong:#142e4f2e;--garage-text-main:#1f2d3d;--garage-text-muted:#6a7787;--garage-text-subtle:#8792a0;--garage-accent:#12365d;--garage-accent-soft:#dbe7f3;--garage-accent-line:#12365d1f;--garage-warning:#8f5f26}*{box-sizing:border-box}html,body{width:100%;height:100%;margin:0;overflow:hidden}body{color:var(--garage-text-main);background:var(--garage-shell-bg);font-family:Segoe UI,Trebuchet MS,sans-serif}button,input{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.72}:focus-visible{outline-offset:2px;outline:2px solid #12365d59}#app{width:100%;height:100%}.garage-shell{background:var(--garage-shell-bg);flex-direction:column;width:100%;height:100%;display:flex;overflow:hidden}.garage-layout{flex:1;grid-template-columns:308px minmax(0,1fr);gap:1.25rem;width:min(100%,1613px);min-height:0;margin:0 auto;padding:1.25rem;display:grid}.garage-sidebar,.garage-main{flex-direction:column;gap:1rem;min-height:0;display:flex}.garage-main{overflow:hidden}.garage-module,.garage-panel,.garage-card{background:linear-gradient(180deg, var(--garage-surface) 0%, var(--garage-surface-alt) 100%);border:1px solid var(--garage-border);border-radius:1.35rem}.garage-module,.garage-card{padding:1rem}.garage-module{align-content:start;gap:.85rem;display:grid}.garage-panel{flex-direction:column;flex:auto;min-height:0;display:flex;overflow:hidden}.garage-panel-header,.garage-module-header,.garage-card-header{justify-content:space-between;align-items:center;gap:1rem;display:flex}.garage-panel-header{padding:1rem 1rem 0}.garage-module-header{align-items:flex-start}.garage-panel-intro{border-bottom:1px solid var(--garage-accent-line);padding:0 1rem 1rem}.garage-dashboard{flex:1;grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:stretch;gap:1rem;min-height:0;padding:1rem;display:grid}.garage-list-card,.garage-detail-card{flex-direction:column;min-height:0;display:flex}.garage-detail-card{grid-column:1/-1}.garage-scroll-body{flex:1;gap:.8rem;min-height:20rem;max-height:24rem;padding-right:.2rem;display:grid;overflow:auto}.garage-detail-body{padding-top:.95rem}.garage-detail-grid{grid-template-columns:minmax(0,1.25fr) minmax(280px,.85fr);gap:1rem;display:grid}.garage-detail-meta,.garage-summary-grid,.garage-search-actions,.garage-category-grid,.garage-action-row,.garage-inline-meters,.garage-hitpoint-grid,.garage-footer{gap:.75rem;display:grid}.garage-detail-meta{grid-template-columns:repeat(3,minmax(0,1fr));margin-bottom:1rem}.garage-summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.garage-summary-grid>:last-child{grid-column:1/-1}.garage-search-actions,.garage-action-row,.garage-category-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:.65rem}.garage-action-refresh{grid-column:1/-1}.garage-footer-bar{border-top:1px solid #12365d1a;width:100%}.garage-footer{grid-template-columns:repeat(3,minmax(0,1fr));width:min(100%,1613px);margin:0 auto;padding:.95rem 1.25rem 1.15rem}.garage-meter-stack{gap:.75rem;margin-bottom:1rem;display:grid}.garage-eyebrow,.garage-footer-title,.garage-stat-label,.garage-meter-label,.garage-hitpoint-selection{letter-spacing:.18em;text-transform:uppercase;color:var(--garage-text-subtle);font-size:.68rem;font-weight:700}.garage-title,.garage-section-title{letter-spacing:-.02em;color:var(--garage-text-main);margin:.16rem 0 0;font-weight:700}.garage-title{font-size:1.1rem}.garage-section-title{font-size:1.05rem}.garage-copy,.garage-detail-note,.garage-empty-copy,.garage-footer-copy,.garage-vehicle-meta,.garage-detail-caption{color:var(--garage-text-muted);margin:0;font-size:.92rem;line-height:1.48}.garage-pill,.garage-badge{letter-spacing:.1em;text-transform:uppercase;background:var(--garage-accent-soft);color:var(--garage-accent);border-radius:999px;justify-content:center;align-items:center;padding:.48rem .8rem;font-size:.74rem;font-weight:700;display:inline-flex}.garage-badge.is-warning{color:var(--garage-warning);background:#f6e2c1e0}.garage-search-form{gap:.75rem;display:grid}.garage-search-input{border:1px solid var(--garage-border);width:100%;height:2.9rem;color:var(--garage-text-main);background:#ffffffbf;border-radius:.8rem;padding:0 .95rem}.garage-stat-card{border:1px solid var(--garage-border);background:#ffffff7a;border-radius:.85rem;flex-direction:column;gap:.3rem;min-width:0;padding:.85rem;display:flex}.garage-stat-card.is-accent{background:linear-gradient(#edf3f9eb 0%,#dfe8f2b8 100%)}.garage-stat-card.is-danger{background:linear-gradient(#fef2f2f2 0%,#fce1e1d1 100%);border-color:#dc979761}.garage-stat-value{color:var(--garage-text-main);overflow-wrap:anywhere;word-break:break-word;font-size:1rem;font-weight:700;line-height:1.3}.garage-chip{border:1px solid var(--garage-border);min-height:2.6rem;color:var(--garage-text-muted);letter-spacing:.08em;text-transform:uppercase;background:#ffffff85;border-radius:.85rem;padding:.68rem .9rem;font-size:.8rem;font-weight:700}.garage-chip.is-active{background:var(--garage-accent-soft);color:var(--garage-accent);border-color:#12365d33}.garage-vehicle-item{border:1px solid var(--garage-border);width:100%;color:inherit;text-align:left;background:#ffffff7a;border-radius:.95rem;padding:.9rem}.garage-vehicle-item.is-selected{background:linear-gradient(#edf3f9f5 0%,#dfe8f2bd 100%);border-color:#12365d3d;box-shadow:0 16px 26px #12365d14}.garage-vehicle-item-head,.garage-meter-label-row,.garage-subsystem-header,.garage-hitpoint-row{justify-content:space-between;align-items:center;gap:.75rem;display:flex}.garage-vehicle-copy,.garage-hitpoint-copy,.garage-footer-block{flex-direction:column;gap:.18rem;min-width:0;display:flex}.garage-vehicle-title,.garage-hitpoint-name,.garage-hitpoint-value{color:var(--garage-text-main);font-size:.9rem;font-weight:700}.garage-meter{gap:.32rem;display:grid}.garage-meter-track{background:#12365d14;border-radius:999px;width:100%;height:.45rem;overflow:hidden}.garage-meter-value{color:var(--garage-text-main);font-size:.78rem;font-weight:700}.garage-meter-fill{border-radius:inherit;height:100%;display:block}.garage-meter-fill.is-health{background:linear-gradient(90deg,#2f7d5b 0%,#4eaa82 100%)}.garage-meter-fill.is-fuel{background:linear-gradient(90deg,#12365d 0%,#3c6792 100%)}.garage-btn{border:1px solid var(--garage-border-strong);letter-spacing:.08em;text-transform:uppercase;border-radius:.8rem;min-height:2.75rem;padding:.72rem 1rem;font-size:.82rem;font-weight:700}.garage-btn-primary{color:var(--garage-accent);background:#ffffffad}.garage-btn-primary:hover{background:#dbe7f3e0}.garage-btn-secondary{color:var(--garage-text-muted);background:#ffffff6b}.garage-btn-secondary:hover{color:var(--garage-text-main);background:#fff9}.garage-hitpoint-row{border:1px solid var(--garage-border);background:#ffffff85;border-radius:.85rem;padding:.72rem .78rem}.garage-detail-empty,.garage-empty-state{flex-direction:column;justify-content:center;align-items:flex-start;min-height:100%;display:flex}.garage-empty-title{color:var(--garage-text-main);margin:0 0 .35rem;font-size:1rem;font-weight:700}.garage-empty-inline{border:1px dashed var(--garage-border);color:var(--garage-text-muted);background:#ffffff5c;border-radius:.85rem;padding:.9rem}.garage-toast-stack{z-index:10;flex-direction:column;gap:.65rem;display:flex;position:fixed;top:1.2rem;right:1.5rem}.garage-toast{border:1px solid var(--garage-border);background:#fff;border-radius:.9rem;max-width:24rem;padding:.85rem 1rem;font-size:.92rem;box-shadow:0 14px 28px #10223824}.garage-toast.is-success{color:#166534;background:#ecfdf5;border-color:#bbf7d0}.garage-toast.is-error{color:#991b1b;background:#fef2f2;border-color:#fecaca}@media (width<=1440px){.garage-layout{grid-template-columns:288px minmax(0,1fr)}.garage-detail-grid{grid-template-columns:1fr}}@media (width<=1120px){.garage-layout{grid-template-columns:1fr;overflow:auto}.garage-main,.garage-sidebar{min-height:auto}.garage-dashboard{grid-template-columns:1fr}.garage-detail-card{grid-column:auto}.garage-scroll-body{min-height:16rem;max-height:none}.garage-footer{grid-template-columns:1fr}}
\ No newline at end of file
diff --git a/arma/client/addons/garage/ui/_site/garage-ui.js b/arma/client/addons/garage/ui/_site/garage-ui.js
index 05c4afd..89e096a 100644
--- a/arma/client/addons/garage/ui/_site/garage-ui.js
+++ b/arma/client/addons/garage/ui/_site/garage-ui.js
@@ -1 +1 @@
-!function(){const e=window.ForgeWebUI;(window.GarageApp=window.GarageApp||{}).runtime=e,window.AppRuntime=e}(),function(){const e=window.GarageApp=window.GarageApp||{},a={garageName:"Vehicle Garage",capacityUsed:0,capacityMax:5,nearbyCount:0,spawnBlocked:!1,spawnStatus:"Ready"},r={vehicles:[]},t={vehicles:[]};function s(e,a){var r;Object.keys(e).forEach(a=>delete e[a]),Object.assign(e,(r=a,JSON.parse(JSON.stringify(r))))}e.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({},a),garage:Object.assign({},r),nearby:Object.assign({},t),applyHydratePayload(e){s(this.session,Object.assign({},a,e?.session||{})),s(this.garage,Object.assign({},r,e?.garage||{})),s(this.nearby,Object.assign({},t,e?.nearby||{}))}}}(),function(){const e=window.GarageApp=window.GarageApp||{},{createSignal:a}=e.runtime;e.store=new class{constructor(){[this.getSelectedKind,this.setSelectedKind]=a(""),[this.getSelectedId,this.setSelectedId]=a(""),[this.getSearchQuery,this.setSearchQuery]=a(""),[this.getCategoryFilter,this.setCategoryFilter]=a("all"),[this.getPendingAction,this.setPendingAction]=a(""),[this.getNotice,this.setNotice]=a({type:"",text:""})}getSelection(){return{id:this.getSelectedId(),kind:this.getSelectedKind()}}clearSelection(){this.setSelectedKind(""),this.setSelectedId("")}select(e,a){this.setSelectedKind(String(e||"")),this.setSelectedId(String(a||""))}startAction(e){this.setPendingAction(String(e||""))}finishAction(){this.setPendingAction("")}matchesSelection(e){if(!e||"object"!=typeof e)return!1;const a=this.getSelection();return!(!a.kind||!a.id)&&("stored"===a.kind?"stored"===e.entryKind&&String(e.plate||"")===a.id:"nearby"===a.kind&&("nearby"===e.entryKind&&String(e.netId||"")===a.id))}ensureSelection(){const a=Array.isArray(e.data?.garage?.vehicles)?e.data.garage.vehicles:[],r=Array.isArray(e.data?.nearby?.vehicles)?e.data.nearby.vehicles:[];if([...a,...r].some(e=>this.matchesSelection(e)))return;const t=a[0]||null;if(t)return void this.select("stored",t.plate||"");const s=r[0]||null;s?this.select("nearby",s.netId||""):this.clearSelection()}hydrateFromPayload(){this.finishAction(),this.ensureSelection()}}}(),function(){const e=window.GarageApp=window.GarageApp||{},a=e.store,r=window.ForgeWebUI.createBridge({closeEvent:"garage::close",globalName:"ForgeBridge",readyEvent:"garage::ready"});function t(r){e.data.applyHydratePayload(r),a.hydrateFromPayload(r)}r.on("garage::hydrate",t),r.on("garage::sync",t),r.on("garage::retrieve::success",r=>{a.finishAction(),e.actions&&e.actions.showNotice("success",r.message||"Vehicle retrieved from the garage.")}),r.on("garage::retrieve::failure",r=>{a.finishAction(),e.actions&&e.actions.showNotice("error",r.message||"Unable to retrieve vehicle.")}),r.on("garage::store::success",r=>{a.finishAction(),e.actions&&e.actions.showNotice("success",r.message||"Vehicle stored in the garage.")}),r.on("garage::store::failure",r=>{a.finishAction(),e.actions&&e.actions.showNotice("error",r.message||"Unable to store vehicle.")}),r.on("garage::service::success",r=>{a.finishAction(),e.actions&&e.actions.showNotice("success",r.message||"Service request sent.")}),r.on("garage::service::failure",r=>{a.finishAction(),e.actions&&e.actions.showNotice("error",r.message||"Unable to service vehicle.")}),e.bridge={notifyReady:function(){return r.ready({loaded:!0})},receive:r.receive,requestClose:function(){return r.close({})},requestRefresh:function(){return r.send("garage::refresh",{})},requestRefuel:function(e){return r.send("garage::vehicle::refuel::request",e)},requestRepair:function(e){return r.send("garage::vehicle::repair::request",e)},requestRetrieve:function(e){return r.send("garage::vehicle::retrieve::request",e)},requestStore:function(e){return r.send("garage::vehicle::store::request",e)},sendEvent:r.send}}(),function(){const e=window.GarageApp=window.GarageApp||{},a=e.store;let r=null;function t(){const r=a.getSelection();return"stored"===r.kind?(Array.isArray(e.data?.garage?.vehicles)?e.data.garage.vehicles:[]).find(e=>String(e.plate||"")===r.id)||null:"nearby"===r.kind&&(Array.isArray(e.data?.nearby?.vehicles)?e.data.nearby.vehicles:[]).find(e=>String(e.netId||"")===r.id)||null}function s(e,t){a.setNotice({type:e,text:t}),r&&clearTimeout(r),r=setTimeout(()=>{a.setNotice({type:"",text:""}),r=null},3200)}e.actions={showNotice:s,closeGarage:function(){const a=e.bridge;if(a&&"function"==typeof a.requestClose){if(a.requestClose())return!0}return s("error","Garage bridge is unavailable."),!1},refreshGarage:function(){const a=e.bridge;if(a&&"function"==typeof a.requestRefresh){if(a.requestRefresh())return!0}return s("error","Garage refresh bridge is unavailable."),!1},applySearchQuery:function(e){a.setSearchQuery(String(e||"").trim())},clearSearch:function(){a.setSearchQuery("")},selectCategory:function(e){a.setCategoryFilter(String(e||"all").trim()||"all")},selectEntry:function(e,r){a.select(e,r)},getSelectedEntry:t,requestRefuelSelected:function(){const r=t();if(!r||"nearby"!==r.entryKind)return s("error","Select a nearby vehicle to refuel."),!1;if(Number(r.fuel||0)>=.999)return s("error","Vehicle fuel tank is already full."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRefuel?(a.startAction("refuel"),!!i.requestRefuel({netId:r.netId||""})||(a.finishAction(),s("error","Garage refuel bridge is unavailable."),!1)):(s("error","Garage refuel bridge is unavailable."),!1)},requestRepairSelected:function(){const r=t();if(!r||"nearby"!==r.entryKind)return s("error","Select a nearby vehicle to repair."),!1;if(Number(r.health||0)>=.999)return s("error","Vehicle has no reported damage."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRepair?(a.startAction("repair"),!!i.requestRepair({netId:r.netId||""})||(a.finishAction(),s("error","Garage repair bridge is unavailable."),!1)):(s("error","Garage repair bridge is unavailable."),!1)},requestRetrieveSelected:function(){const r=t();if(!r||"stored"!==r.entryKind)return s("error","Select a stored vehicle to retrieve."),!1;if(e.data?.session?.spawnBlocked)return s("error","The garage spawn area is blocked."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRetrieve?(a.startAction("retrieve"),!!i.requestRetrieve({plate:r.plate||""})||(a.finishAction(),s("error","Garage retrieve bridge is unavailable."),!1)):(s("error","Garage retrieve bridge is unavailable."),!1)},requestStoreSelected:function(){const r=t();if(!r||"nearby"!==r.entryKind)return s("error","Select a nearby vehicle to store."),!1;if(!1===r.isEmpty)return s("error","All crew must exit the vehicle before storing it."),!1;const i=e.bridge;return i&&"function"==typeof i.requestStore?(a.startAction("store"),!!i.requestStore({netId:r.netId||""})||(a.finishAction(),s("error","Garage store bridge is unavailable."),!1)):(s("error","Garage store bridge is unavailable."),!1)}}}(),function(){const e=window.GarageApp=window.GarageApp||{},{h:a}=e.runtime,r=window.SharedUI.componentFns.WindowTitleBar,t=e.store,s=e.actions,{categories:i,garage:n,nearby:c,session:l}=e.data;function o(e){return Math.max(0,Math.min(100,Math.round(100*Number(e||0))))}function g(e){const a=i.find(a=>a.id===String(e||"other").toLowerCase());return a?a.label:"Other"}function d(e){return`${Math.round(Number(e||0))} m`}function u(e){return String(e||"").trim()||"Untracked"}function p(e,a){return(e||[]).filter(e=>("all"===a.categoryFilter||String(e.category||"").toLowerCase()===a.categoryFilter)&&function(e,a){const r=String(e||"").trim().toLowerCase();return!r||a.some(e=>String(e||"").toLowerCase().includes(r))}(a.searchQuery,[e.displayName,e.classname,e.plate,e.netId,e.category]))}function m(e,r,t=""){return a("div",{className:t?`garage-stat-card is-${t}`:"garage-stat-card"},a("span",{className:"garage-stat-label"},e),a("span",{className:"garage-stat-value"},r))}function h(e,r,t){return a("div",{className:"garage-meter"},a("div",{className:"garage-meter-label-row"},a("span",{className:"garage-meter-label"},e),a("span",{className:"garage-meter-value"},`${r}%`)),a("div",{className:"garage-meter-track"},a("span",{className:`garage-meter-fill is-${t}`,style:{width:`${r}%`}})))}function y(e,r,t,i,n){return a("section",{className:"garage-card garage-list-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},r),a("h2",{className:"garage-section-title"},e)),a("span",{className:"garage-pill"},`${i.length} ${1===i.length?"Vehicle":"Vehicles"}`)),a("div",{className:"garage-card-body garage-scroll-body","data-preserve-scroll-id":t},i.length>0?i.map(e=>function(e,r){const t="stored"===e.entryKind?String(e.plate||""):String(e.netId||""),i="nearby"===e.entryKind;return a("button",{type:"button",className:(n=e,c=r,n&&c&&String(n.entryKind||"")===String(c.entryKind||"")&&String(n.plate||"")===String(c.plate||"")&&String(n.netId||"")===String(c.netId||"")?"garage-vehicle-item is-selected":"garage-vehicle-item"),onClick:()=>s.selectEntry(e.entryKind,t)},a("div",{className:"garage-vehicle-item-head"},a("div",{className:"garage-vehicle-copy"},a("span",{className:"garage-vehicle-title"},e.displayName||e.classname||"Vehicle"),a("span",{className:"garage-vehicle-meta"},i?`Nearby ${d(e.distance)}`:`Plate ${u(e.plate)}`)),a("span",{className:i&&!1===e.isEmpty?"garage-badge is-warning":"garage-badge"},i?!1===e.isEmpty?"Crewed":"Empty":g(e.category))),a("div",{className:"garage-inline-meters"},h("Health",o(e.health),"health"),h("Fuel",o(e.fuel),"fuel")));var n,c}(e,n)):a("div",{className:"garage-empty-state"},a("h3",{className:"garage-empty-title"},"No matching vehicles"),a("p",{className:"garage-empty-copy"},"Adjust the current search or category filter to view more records."))))}function b(e){const r=(Array.isArray(e)?e:[]).slice().sort((e,a)=>Number(a.value||0)-Number(e.value||0)).slice(0,6).filter(e=>Number(e.value||0)>0);return 0===r.length?a("div",{className:"garage-empty-inline"},"No subsystem damage reported."):a("div",{className:"garage-hitpoint-grid"},r.map(e=>{return a("div",{className:"garage-hitpoint-row"},a("div",{className:"garage-hitpoint-copy"},a("span",{className:"garage-hitpoint-name"},(r=e.name,String(r||"").replace(/^Hit/i,"").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/_/g," ").trim()||"Subsystem")),e.selection?a("span",{className:"garage-hitpoint-selection"},e.selection):null),a("span",{className:"garage-hitpoint-value"},`${Math.round(100*Number(e.value||0))}%`));var r}))}e.components=e.components||{},e.components.App=function(){const e={categoryFilter:t.getCategoryFilter(),notice:t.getNotice(),pendingAction:t.getPendingAction(),searchQuery:t.getSearchQuery(),selectedId:t.getSelectedId(),selectedKind:t.getSelectedKind()},v=function(e){return"stored"===e.selectedKind?(n.vehicles||[]).find(a=>String(a.plate||"")===e.selectedId)||null:"nearby"===e.selectedKind&&(c.vehicles||[]).find(a=>String(a.netId||"")===e.selectedId)||null}(e),f=p(n.vehicles||[],e),N=p(c.vehicles||[],e),S=e.searchQuery?`Search: ${e.searchQuery}`:"Live";return a("div",{className:"garage-shell"},r({kicker:"FORGE Logistics",title:"Vehicle Garage",onClose:()=>s.closeGarage(),closeLabel:"Close garage interface"}),e.notice.text?a("div",{className:"garage-toast-stack"},a("div",{className:"error"===e.notice.type?"garage-toast is-error":"garage-toast is-success"},e.notice.text)):null,a("div",{className:"garage-layout"},a("aside",{className:"garage-sidebar"},a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Search"),a("h2",{className:"garage-section-title"},"Vehicle Records")),a("span",{className:"garage-pill"},S)),a("div",{className:"garage-search-form"},a("input",{id:"garage-search-input",type:"text",className:"garage-search-input",placeholder:"Search by name, plate, or category",value:e.searchQuery}),a("div",{className:"garage-search-actions"},a("button",{type:"button",className:"garage-btn garage-btn-primary",onClick:()=>s.applySearchQuery(document.getElementById("garage-search-input")?.value||"")},"Apply Search"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",onClick:()=>s.clearSearch()},"Clear")))),a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Filter"),a("h2",{className:"garage-section-title"},"Vehicle Categories"))),a("div",{className:"garage-category-grid"},i.map(r=>a("button",{type:"button",className:e.categoryFilter===r.id?"garage-chip is-active":"garage-chip",onClick:()=>s.selectCategory(r.id)},r.label)))),a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Status"),a("h2",{className:"garage-section-title"},"Garage Summary")),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:Boolean(e.pendingAction),onClick:()=>s.refreshGarage()},"Refresh")),a("div",{className:"garage-summary-grid"},m("Stored",`${l.capacityUsed}/${l.capacityMax}`),m("Nearby",l.nearbyCount,"accent"),m("Spawn Lane",l.spawnStatus,l.spawnBlocked?"danger":"")))),a("main",{className:"garage-main"},a("section",{className:"garage-panel"},a("div",{className:"garage-panel-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Operations Bay"),a("h1",{className:"garage-title"},l.garageName||"Vehicle Garage")),a("span",{className:"garage-pill"},`${l.capacityUsed}/${l.capacityMax} Stored`)),a("div",{className:"garage-panel-intro"},a("p",{className:"garage-copy"},"Retrieve stored vehicles into the active spawn lane or store nearby empty vehicles back into persistent ownership records.")),a("div",{className:"garage-dashboard"},y("Stored Vehicles","Persistent Records","garage-stored-list",f,v),y("Nearby Vehicles","Store Window","garage-nearby-list",N,v),function(e,r){if(!e)return a("section",{className:"garage-card garage-detail-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Selection"),a("h2",{className:"garage-section-title"},"Vehicle Detail"))),a("div",{className:"garage-card-body garage-detail-empty"},a("h3",{className:"garage-empty-title"},"Select a vehicle"),a("p",{className:"garage-empty-copy"},"Choose a stored record to retrieve or a nearby vehicle to store.")));const t="stored"===e.entryKind,i=String(r.pendingAction||""),n=Boolean(i),c=t&&!l.spawnBlocked&&!n,p=!t&&!1!==e.isEmpty&&!n,y=!t&&Number(e.fuel||0)<.999&&!n,v=!t&&Number(e.health||0)<.999&&!n;return a("section",{className:"garage-card garage-detail-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},t?"Stored Record":"Nearby Vehicle"),a("h2",{className:"garage-section-title"},e.displayName||e.classname||"Vehicle")),a("span",{className:"nearby"===e.entryKind&&!1===e.isEmpty?"garage-badge is-warning":"garage-badge"},t?`Plate ${u(e.plate)}`:!1===e.isEmpty?"Crewed":"Ready")),a("div",{className:"garage-card-body garage-detail-body"},a("div",{className:"garage-detail-grid"},a("div",{className:"garage-detail-copy"},a("div",{className:"garage-detail-meta"},m("Category",g(e.category)),m("Status",(f=e)?"stored"===f.entryKind?"Stored":!1===f.isEmpty?"Crewed":"Ready":"-","nearby"===e.entryKind&&!1===e.isEmpty?"danger":""),m(t?"Record":"Distance",t?u(e.plate):d(e.distance),t?"":"accent")),a("div",{className:"garage-meter-stack"},h("Health",o(e.health),"health"),h("Fuel",o(e.fuel),"fuel")),a("div",{className:"garage-action-row"},t?a("button",{type:"button",className:"garage-btn garage-btn-primary",disabled:!c,onClick:()=>s.requestRetrieveSelected()},"retrieve"===i?"Retrieving...":"Retrieve Vehicle"):a("button",{type:"button",className:"garage-btn garage-btn-primary",disabled:!p,onClick:()=>s.requestStoreSelected()},"store"===i?"Storing...":"Store Vehicle"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:!y,onClick:()=>s.requestRefuelSelected()},"refuel"===i?"Refueling...":"Refuel"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:!v,onClick:()=>s.requestRepairSelected()},"repair"===i?"Repairing...":"Repair"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:n,onClick:()=>s.refreshGarage()},"Refresh")),a("p",{className:"garage-detail-note"},t?l.spawnBlocked?"The garage spawn lane is currently blocked.":"Retrieve this stored vehicle into the active spawn lane before refuel or repair service.":!1===e.isEmpty?"Only empty nearby vehicles can be stored.":"Store this nearby vehicle or request organization-billed refuel and repair service.")),a("div",{className:"garage-detail-subsystems"},a("div",{className:"garage-subsystem-header"},a("span",{className:"garage-eyebrow"},"Subsystems"),a("span",{className:"garage-detail-caption"},"Highest damage first")),b(e.hitPoints)))));var f}(v,e))))),a("footer",{className:"garage-footer-bar"},a("div",{className:"garage-footer"},a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Storage Capacity"),a("span",{className:"garage-footer-copy"},`${l.capacityUsed} of ${l.capacityMax} vehicle slot(s) are currently occupied.`)),a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Retrieval Window"),a("span",{className:"garage-footer-copy"},l.spawnBlocked?"Spawn lane is blocked. Clear the bay before retrieving another vehicle.":"Spawn lane is clear. Stored vehicles can be retrieved immediately.")),a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Store Rules"),a("span",{className:"garage-footer-copy"},"Only nearby empty vehicles can be stored. Nearby count updates from the live world state.")))))}}(),function(){const e=window.ForgeWebUI,a=window.GarageApp;e.createApp({name:"garage",root:"#app",setup({root:r}){e.mount(r,()=>a.components.App(),{preserveScroll:!0}),a.bridge&&a.bridge.notifyReady()}}).start()}();
\ No newline at end of file
+!function(){const e=window.ForgeWebUI;(window.GarageApp=window.GarageApp||{}).runtime=e,window.AppRuntime=e}(),function(){const e=window.GarageApp=window.GarageApp||{},a={garageName:"Vehicle Garage",capacityUsed:0,capacityMax:5,nearbyCount:0,spawnBlocked:!1,spawnStatus:"Ready"},r={vehicles:[]},t={vehicles:[]};function s(e,a){var r;Object.keys(e).forEach(a=>delete e[a]),Object.assign(e,(r=a,JSON.parse(JSON.stringify(r))))}e.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({},a),garage:Object.assign({},r),nearby:Object.assign({},t),applyHydratePayload(e){s(this.session,Object.assign({},a,e?.session||{})),s(this.garage,Object.assign({},r,e?.garage||{})),s(this.nearby,Object.assign({},t,e?.nearby||{}))}}}(),function(){const e=window.GarageApp=window.GarageApp||{},{createSignal:a}=e.runtime;e.store=new class{constructor(){[this.getSelectedKind,this.setSelectedKind]=a(""),[this.getSelectedId,this.setSelectedId]=a(""),[this.getSearchQuery,this.setSearchQuery]=a(""),[this.getCategoryFilter,this.setCategoryFilter]=a("all"),[this.getPendingAction,this.setPendingAction]=a(""),[this.getNotice,this.setNotice]=a({type:"",text:""})}getSelection(){return{id:this.getSelectedId(),kind:this.getSelectedKind()}}clearSelection(){this.setSelectedKind(""),this.setSelectedId("")}select(e,a){this.setSelectedKind(String(e||"")),this.setSelectedId(String(a||""))}startAction(e){this.setPendingAction(String(e||""))}finishAction(){this.setPendingAction("")}matchesSelection(e){if(!e||"object"!=typeof e)return!1;const a=this.getSelection();return!(!a.kind||!a.id)&&("stored"===a.kind?"stored"===e.entryKind&&String(e.plate||"")===a.id:"nearby"===a.kind&&("nearby"===e.entryKind&&String(e.netId||"")===a.id))}ensureSelection(){const a=Array.isArray(e.data?.garage?.vehicles)?e.data.garage.vehicles:[],r=Array.isArray(e.data?.nearby?.vehicles)?e.data.nearby.vehicles:[];if([...a,...r].some(e=>this.matchesSelection(e)))return;const t=a[0]||null;if(t)return void this.select("stored",t.plate||"");const s=r[0]||null;s?this.select("nearby",s.netId||""):this.clearSelection()}hydrateFromPayload(){this.finishAction(),this.ensureSelection()}}}(),function(){const e=window.GarageApp=window.GarageApp||{},a=e.store,r=window.ForgeWebUI.createBridge({closeEvent:"garage::close",globalName:"ForgeBridge",readyEvent:"garage::ready"});function t(r){e.data.applyHydratePayload(r),a.hydrateFromPayload(r)}r.on("garage::hydrate",t),r.on("garage::sync",t),r.on("garage::retrieve::success",r=>{a.finishAction(),e.actions&&e.actions.showNotice("success",r.message||"Vehicle retrieved from the garage.")}),r.on("garage::retrieve::failure",r=>{a.finishAction(),e.actions&&e.actions.showNotice("error",r.message||"Unable to retrieve vehicle.")}),r.on("garage::store::success",r=>{a.finishAction(),e.actions&&e.actions.showNotice("success",r.message||"Vehicle stored in the garage.")}),r.on("garage::store::failure",r=>{a.finishAction(),e.actions&&e.actions.showNotice("error",r.message||"Unable to store vehicle.")}),r.on("garage::service::success",r=>{a.finishAction(),e.actions&&e.actions.showNotice("success",r.message||"Service request sent.")}),r.on("garage::service::failure",r=>{a.finishAction(),e.actions&&e.actions.showNotice("error",r.message||"Unable to service vehicle.")}),e.bridge={notifyReady:function(){return r.ready({loaded:!0})},receive:r.receive,requestClose:function(){return r.close({})},requestRefresh:function(){return r.send("garage::refresh",{})},requestRearm:function(e){return r.send("garage::vehicle::rearm::request",e)},requestRefuel:function(e){return r.send("garage::vehicle::refuel::request",e)},requestRepair:function(e){return r.send("garage::vehicle::repair::request",e)},requestRetrieve:function(e){return r.send("garage::vehicle::retrieve::request",e)},requestStore:function(e){return r.send("garage::vehicle::store::request",e)},sendEvent:r.send}}(),function(){const e=window.GarageApp=window.GarageApp||{},a=e.store;let r=null;function t(){const r=a.getSelection();return"stored"===r.kind?(Array.isArray(e.data?.garage?.vehicles)?e.data.garage.vehicles:[]).find(e=>String(e.plate||"")===r.id)||null:"nearby"===r.kind&&(Array.isArray(e.data?.nearby?.vehicles)?e.data.nearby.vehicles:[]).find(e=>String(e.netId||"")===r.id)||null}function s(e,t){a.setNotice({type:e,text:t}),r&&clearTimeout(r),r=setTimeout(()=>{a.setNotice({type:"",text:""}),r=null},3200)}e.actions={showNotice:s,closeGarage:function(){const a=e.bridge;if(a&&"function"==typeof a.requestClose){if(a.requestClose())return!0}return s("error","Garage bridge is unavailable."),!1},refreshGarage:function(){const a=e.bridge;if(a&&"function"==typeof a.requestRefresh){if(a.requestRefresh())return!0}return s("error","Garage refresh bridge is unavailable."),!1},applySearchQuery:function(e){a.setSearchQuery(String(e||"").trim())},clearSearch:function(){a.setSearchQuery("")},selectCategory:function(e){a.setCategoryFilter(String(e||"all").trim()||"all")},selectEntry:function(e,r){a.select(e,r)},getSelectedEntry:t,requestRearmSelected:function(){const r=t();if(!r||"nearby"!==r.entryKind)return s("error","Select a nearby vehicle to rearm."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRearm?(a.startAction("rearm"),!!i.requestRearm({netId:r.netId||""})||(a.finishAction(),s("error","Garage rearm bridge is unavailable."),!1)):(s("error","Garage rearm bridge is unavailable."),!1)},requestRefuelSelected:function(){const r=t();if(!r||"nearby"!==r.entryKind)return s("error","Select a nearby vehicle to refuel."),!1;if(Number(r.fuel||0)>=.999)return s("error","Vehicle fuel tank is already full."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRefuel?(a.startAction("refuel"),!!i.requestRefuel({netId:r.netId||""})||(a.finishAction(),s("error","Garage refuel bridge is unavailable."),!1)):(s("error","Garage refuel bridge is unavailable."),!1)},requestRepairSelected:function(){const r=t();if(!r||"nearby"!==r.entryKind)return s("error","Select a nearby vehicle to repair."),!1;if(Number(r.health||0)>=.999)return s("error","Vehicle has no reported damage."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRepair?(a.startAction("repair"),!!i.requestRepair({netId:r.netId||""})||(a.finishAction(),s("error","Garage repair bridge is unavailable."),!1)):(s("error","Garage repair bridge is unavailable."),!1)},requestRetrieveSelected:function(){const r=t();if(!r||"stored"!==r.entryKind)return s("error","Select a stored vehicle to retrieve."),!1;if(e.data?.session?.spawnBlocked)return s("error","The garage spawn area is blocked."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRetrieve?(a.startAction("retrieve"),!!i.requestRetrieve({plate:r.plate||""})||(a.finishAction(),s("error","Garage retrieve bridge is unavailable."),!1)):(s("error","Garage retrieve bridge is unavailable."),!1)},requestStoreSelected:function(){const r=t();if(!r||"nearby"!==r.entryKind)return s("error","Select a nearby vehicle to store."),!1;if(!1===r.isEmpty)return s("error","All crew must exit the vehicle before storing it."),!1;const i=e.bridge;return i&&"function"==typeof i.requestStore?(a.startAction("store"),!!i.requestStore({netId:r.netId||""})||(a.finishAction(),s("error","Garage store bridge is unavailable."),!1)):(s("error","Garage store bridge is unavailable."),!1)}}}(),function(){const e=window.GarageApp=window.GarageApp||{},{h:a}=e.runtime,r=window.SharedUI.componentFns.WindowTitleBar,t=e.store,s=e.actions,{categories:i,garage:n,nearby:c,session:l}=e.data;function o(e){return Math.max(0,Math.min(100,Math.round(100*Number(e||0))))}function g(e){const a=i.find(a=>a.id===String(e||"other").toLowerCase());return a?a.label:"Other"}function d(e){return`${Math.round(Number(e||0))} m`}function u(e){return String(e||"").trim()||"Untracked"}function m(e,a){return(e||[]).filter(e=>("all"===a.categoryFilter||String(e.category||"").toLowerCase()===a.categoryFilter)&&function(e,a){const r=String(e||"").trim().toLowerCase();return!r||a.some(e=>String(e||"").toLowerCase().includes(r))}(a.searchQuery,[e.displayName,e.classname,e.plate,e.netId,e.category]))}function p(e,r,t=""){return a("div",{className:t?`garage-stat-card is-${t}`:"garage-stat-card"},a("span",{className:"garage-stat-label"},e),a("span",{className:"garage-stat-value"},r))}function h(e,r,t){return a("div",{className:"garage-meter"},a("div",{className:"garage-meter-label-row"},a("span",{className:"garage-meter-label"},e),a("span",{className:"garage-meter-value"},`${r}%`)),a("div",{className:"garage-meter-track"},a("span",{className:`garage-meter-fill is-${t}`,style:{width:`${r}%`}})))}function y(e,r,t,i,n){return a("section",{className:"garage-card garage-list-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},r),a("h2",{className:"garage-section-title"},e)),a("span",{className:"garage-pill"},`${i.length} ${1===i.length?"Vehicle":"Vehicles"}`)),a("div",{className:"garage-card-body garage-scroll-body","data-preserve-scroll-id":t},i.length>0?i.map(e=>function(e,r){const t="stored"===e.entryKind?String(e.plate||""):String(e.netId||""),i="nearby"===e.entryKind;return a("button",{type:"button",className:(n=e,c=r,n&&c&&String(n.entryKind||"")===String(c.entryKind||"")&&String(n.plate||"")===String(c.plate||"")&&String(n.netId||"")===String(c.netId||"")?"garage-vehicle-item is-selected":"garage-vehicle-item"),onClick:()=>s.selectEntry(e.entryKind,t)},a("div",{className:"garage-vehicle-item-head"},a("div",{className:"garage-vehicle-copy"},a("span",{className:"garage-vehicle-title"},e.displayName||e.classname||"Vehicle"),a("span",{className:"garage-vehicle-meta"},i?`Nearby ${d(e.distance)}`:`Plate ${u(e.plate)}`)),a("span",{className:i&&!1===e.isEmpty?"garage-badge is-warning":"garage-badge"},i?!1===e.isEmpty?"Crewed":"Empty":g(e.category))),a("div",{className:"garage-inline-meters"},h("Health",o(e.health),"health"),h("Fuel",o(e.fuel),"fuel")));var n,c}(e,n)):a("div",{className:"garage-empty-state"},a("h3",{className:"garage-empty-title"},"No matching vehicles"),a("p",{className:"garage-empty-copy"},"Adjust the current search or category filter to view more records."))))}function b(e){const r=(Array.isArray(e)?e:[]).slice().sort((e,a)=>Number(a.value||0)-Number(e.value||0)).slice(0,6).filter(e=>Number(e.value||0)>0);return 0===r.length?a("div",{className:"garage-empty-inline"},"No subsystem damage reported."):a("div",{className:"garage-hitpoint-grid"},r.map(e=>{return a("div",{className:"garage-hitpoint-row"},a("div",{className:"garage-hitpoint-copy"},a("span",{className:"garage-hitpoint-name"},(r=e.name,String(r||"").replace(/^Hit/i,"").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/_/g," ").trim()||"Subsystem")),e.selection?a("span",{className:"garage-hitpoint-selection"},e.selection):null),a("span",{className:"garage-hitpoint-value"},`${Math.round(100*Number(e.value||0))}%`));var r}))}e.components=e.components||{},e.components.App=function(){const e={categoryFilter:t.getCategoryFilter(),notice:t.getNotice(),pendingAction:t.getPendingAction(),searchQuery:t.getSearchQuery(),selectedId:t.getSelectedId(),selectedKind:t.getSelectedKind()},v=function(e){return"stored"===e.selectedKind?(n.vehicles||[]).find(a=>String(a.plate||"")===e.selectedId)||null:"nearby"===e.selectedKind&&(c.vehicles||[]).find(a=>String(a.netId||"")===e.selectedId)||null}(e),f=m(n.vehicles||[],e),N=m(c.vehicles||[],e),S=e.searchQuery?`Search: ${e.searchQuery}`:"Live";return a("div",{className:"garage-shell"},r({kicker:"FORGE Logistics",title:"Vehicle Garage",onClose:()=>s.closeGarage(),closeLabel:"Close garage interface"}),e.notice.text?a("div",{className:"garage-toast-stack"},a("div",{className:"error"===e.notice.type?"garage-toast is-error":"garage-toast is-success"},e.notice.text)):null,a("div",{className:"garage-layout"},a("aside",{className:"garage-sidebar"},a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Search"),a("h2",{className:"garage-section-title"},"Vehicle Records")),a("span",{className:"garage-pill"},S)),a("div",{className:"garage-search-form"},a("input",{id:"garage-search-input",type:"text",className:"garage-search-input",placeholder:"Search by name, plate, or category",value:e.searchQuery}),a("div",{className:"garage-search-actions"},a("button",{type:"button",className:"garage-btn garage-btn-primary",onClick:()=>s.applySearchQuery(document.getElementById("garage-search-input")?.value||"")},"Apply Search"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",onClick:()=>s.clearSearch()},"Clear")))),a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Filter"),a("h2",{className:"garage-section-title"},"Vehicle Categories"))),a("div",{className:"garage-category-grid"},i.map(r=>a("button",{type:"button",className:e.categoryFilter===r.id?"garage-chip is-active":"garage-chip",onClick:()=>s.selectCategory(r.id)},r.label)))),a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Status"),a("h2",{className:"garage-section-title"},"Garage Summary")),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:Boolean(e.pendingAction),onClick:()=>s.refreshGarage()},"Refresh")),a("div",{className:"garage-summary-grid"},p("Stored",`${l.capacityUsed}/${l.capacityMax}`),p("Nearby",l.nearbyCount,"accent"),p("Spawn Lane",l.spawnStatus,l.spawnBlocked?"danger":"")))),a("main",{className:"garage-main"},a("section",{className:"garage-panel"},a("div",{className:"garage-panel-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Operations Bay"),a("h1",{className:"garage-title"},l.garageName||"Vehicle Garage")),a("span",{className:"garage-pill"},`${l.capacityUsed}/${l.capacityMax} Stored`)),a("div",{className:"garage-panel-intro"},a("p",{className:"garage-copy"},"Retrieve stored vehicles into the active spawn lane or store nearby empty vehicles back into persistent ownership records.")),a("div",{className:"garage-dashboard"},y("Stored Vehicles","Persistent Records","garage-stored-list",f,v),y("Nearby Vehicles","Store Window","garage-nearby-list",N,v),function(e,r){if(!e)return a("section",{className:"garage-card garage-detail-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Selection"),a("h2",{className:"garage-section-title"},"Vehicle Detail"))),a("div",{className:"garage-card-body garage-detail-empty"},a("h3",{className:"garage-empty-title"},"Select a vehicle"),a("p",{className:"garage-empty-copy"},"Choose a stored record to retrieve or a nearby vehicle to store.")));const t="stored"===e.entryKind,i=String(r.pendingAction||""),n=Boolean(i),c=t&&!l.spawnBlocked&&!n,m=!t&&!1!==e.isEmpty&&!n,y=!t&&Number(e.fuel||0)<.999&&!n,v=!t&&Number(e.health||0)<.999&&!n,f=!t&&!n;return a("section",{className:"garage-card garage-detail-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},t?"Stored Record":"Nearby Vehicle"),a("h2",{className:"garage-section-title"},e.displayName||e.classname||"Vehicle")),a("span",{className:"nearby"===e.entryKind&&!1===e.isEmpty?"garage-badge is-warning":"garage-badge"},t?`Plate ${u(e.plate)}`:!1===e.isEmpty?"Crewed":"Ready")),a("div",{className:"garage-card-body garage-detail-body"},a("div",{className:"garage-detail-grid"},a("div",{className:"garage-detail-copy"},a("div",{className:"garage-detail-meta"},p("Category",g(e.category)),p("Status",(N=e)?"stored"===N.entryKind?"Stored":!1===N.isEmpty?"Crewed":"Ready":"-","nearby"===e.entryKind&&!1===e.isEmpty?"danger":""),p(t?"Record":"Distance",t?u(e.plate):d(e.distance),t?"":"accent")),a("div",{className:"garage-meter-stack"},h("Health",o(e.health),"health"),h("Fuel",o(e.fuel),"fuel")),a("div",{className:"garage-action-row"},t?a("button",{type:"button",className:"garage-btn garage-btn-primary",disabled:!c,onClick:()=>s.requestRetrieveSelected()},"retrieve"===i?"Retrieving...":"Retrieve Vehicle"):a("button",{type:"button",className:"garage-btn garage-btn-primary",disabled:!m,onClick:()=>s.requestStoreSelected()},"store"===i?"Storing...":"Store Vehicle"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:!y,onClick:()=>s.requestRefuelSelected()},"refuel"===i?"Refueling...":"Refuel"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:!v,onClick:()=>s.requestRepairSelected()},"repair"===i?"Repairing...":"Repair"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:!f,onClick:()=>s.requestRearmSelected()},"rearm"===i?"Rearming...":"Rearm"),a("button",{type:"button",className:"garage-btn garage-btn-secondary garage-action-refresh",disabled:n,onClick:()=>s.refreshGarage()},"Refresh")),a("p",{className:"garage-detail-note"},t?l.spawnBlocked?"The garage spawn lane is currently blocked.":"Retrieve this stored vehicle into the active spawn lane before refuel, rearm, or repair service.":!1===e.isEmpty?"Only empty nearby vehicles can be stored.":"Store this nearby vehicle or request organization-billed refuel, rearm, and repair service.")),a("div",{className:"garage-detail-subsystems"},a("div",{className:"garage-subsystem-header"},a("span",{className:"garage-eyebrow"},"Subsystems"),a("span",{className:"garage-detail-caption"},"Highest damage first")),b(e.hitPoints)))));var N}(v,e))))),a("footer",{className:"garage-footer-bar"},a("div",{className:"garage-footer"},a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Storage Capacity"),a("span",{className:"garage-footer-copy"},`${l.capacityUsed} of ${l.capacityMax} vehicle slot(s) are currently occupied.`)),a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Retrieval Window"),a("span",{className:"garage-footer-copy"},l.spawnBlocked?"Spawn lane is blocked. Clear the bay before retrieving another vehicle.":"Spawn lane is clear. Stored vehicles can be retrieved immediately.")),a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Store Rules"),a("span",{className:"garage-footer-copy"},"Only nearby empty vehicles can be stored. Nearby count updates from the live world state.")))))}}(),function(){const e=window.ForgeWebUI,a=window.GarageApp;e.createApp({name:"garage",root:"#app",setup({root:r}){e.mount(r,()=>a.components.App(),{preserveScroll:!0}),a.bridge&&a.bridge.notifyReady()}}).start()}();
\ No newline at end of file
diff --git a/arma/client/addons/garage/ui/src/bridge.js b/arma/client/addons/garage/ui/src/bridge.js
index 0e1c9f8..7941864 100644
--- a/arma/client/addons/garage/ui/src/bridge.js
+++ b/arma/client/addons/garage/ui/src/bridge.js
@@ -31,6 +31,10 @@
return bridge.send("garage::vehicle::repair::request", payload);
}
+ function requestRearm(payload) {
+ return bridge.send("garage::vehicle::rearm::request", payload);
+ }
+
function notifyReady() {
return bridge.ready({ loaded: true });
}
@@ -108,6 +112,7 @@
receive: bridge.receive,
requestClose,
requestRefresh,
+ requestRearm,
requestRefuel,
requestRepair,
requestRetrieve,
diff --git a/arma/client/addons/garage/ui/src/components/AppShell.js b/arma/client/addons/garage/ui/src/components/AppShell.js
index ed57875..9026934 100644
--- a/arma/client/addons/garage/ui/src/components/AppShell.js
+++ b/arma/client/addons/garage/ui/src/components/AppShell.js
@@ -353,6 +353,7 @@
!isStored &&
Number(currentSelection.health || 0) < 0.999 &&
!isBusy;
+ const canRearm = !isStored && !isBusy;
return h(
"section",
@@ -500,6 +501,20 @@
type: "button",
className:
"garage-btn garage-btn-secondary",
+ disabled: !canRearm,
+ onClick: () =>
+ actions.requestRearmSelected(),
+ },
+ pendingAction === "rearm"
+ ? "Rearming..."
+ : "Rearm",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-secondary garage-action-refresh",
disabled: isBusy,
onClick: () => actions.refreshGarage(),
},
@@ -512,10 +527,10 @@
isStored
? session.spawnBlocked
? "The garage spawn lane is currently blocked."
- : "Retrieve this stored vehicle into the active spawn lane before refuel or repair service."
+ : "Retrieve this stored vehicle into the active spawn lane before refuel, rearm, or repair service."
: currentSelection.isEmpty === false
? "Only empty nearby vehicles can be stored."
- : "Store this nearby vehicle or request organization-billed refuel and repair service.",
+ : "Store this nearby vehicle or request organization-billed refuel, rearm, and repair service.",
),
),
h(
diff --git a/arma/client/addons/garage/ui/src/registry/events.js b/arma/client/addons/garage/ui/src/registry/events.js
index 07e47bc..266c44b 100644
--- a/arma/client/addons/garage/ui/src/registry/events.js
+++ b/arma/client/addons/garage/ui/src/registry/events.js
@@ -223,6 +223,33 @@
return true;
}
+ function requestRearmSelected() {
+ const selectedEntry = getSelectedEntry();
+ if (!selectedEntry || selectedEntry.entryKind !== "nearby") {
+ showNotice("error", "Select a nearby vehicle to rearm.");
+ return false;
+ }
+
+ const bridge = GarageApp.bridge;
+ if (!bridge || typeof bridge.requestRearm !== "function") {
+ showNotice("error", "Garage rearm bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("rearm");
+ const sent = bridge.requestRearm({
+ netId: selectedEntry.netId || "",
+ });
+
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Garage rearm bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
GarageApp.actions = {
showNotice,
closeGarage,
@@ -232,6 +259,7 @@
selectCategory,
selectEntry,
getSelectedEntry,
+ requestRearmSelected,
requestRefuelSelected,
requestRepairSelected,
requestRetrieveSelected,
diff --git a/arma/client/addons/garage/ui/src/styles.css b/arma/client/addons/garage/ui/src/styles.css
index b45dec7..dfac0cc 100644
--- a/arma/client/addons/garage/ui/src/styles.css
+++ b/arma/client/addons/garage/ui/src/styles.css
@@ -217,6 +217,10 @@ button:disabled {
gap: 0.65rem;
}
+.garage-action-refresh {
+ grid-column: 1 / -1;
+}
+
.garage-footer-bar {
width: 100%;
border-top: 1px solid rgb(18 54 93 / 0.1);
diff --git a/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf b/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf
index 9d6f8dc..94f1fbf 100644
--- a/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf
+++ b/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf
@@ -99,7 +99,8 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [
["uid", _memberUid],
["name", name _x],
["lifeState", _memberState],
- ["isLeader", _x isEqualTo _leader]
+ ["isLeader", _x isEqualTo _leader],
+ ["position", getPosATL _x]
]);
} forEach _members;
diff --git a/arma/server/addons/economy/README.md b/arma/server/addons/economy/README.md
index 5086506..14f7331 100644
--- a/arma/server/addons/economy/README.md
+++ b/arma/server/addons/economy/README.md
@@ -7,7 +7,7 @@ refueling sessions, medical spawn occupancy, respawn placement, and death
inventory handling.
Current stores cover fuel tracking, medical service behavior, and service
-charges such as repairs.
+charges such as repairs and rearming.
## Dependencies
- `forge_server_main`
@@ -27,8 +27,9 @@ Note: Bank and Org are runtime-only dependencies (not compile-time requiredAddon
respawn placement, death inventory handling, and body-bag transfer. Medical
charges use player bank/cash first, then organization funds with repayable
member debt only when the player cannot cover the service.
-- `fnc_initSEconomyStore.sqf` handles organization-funded service charges and
- repairs. Repairs only apply after the organization charge succeeds. The
+- `fnc_initSEconomyStore.sqf` handles organization-funded service charges,
+ repairs, and rearming. Vehicle services only apply after the organization
+ charge succeeds. The
shared org-charge helper can also record member debt for medical fallback.
## Event Surface
@@ -50,6 +51,16 @@ Repair service requests use:
`_cost` is optional. Passing `-1` uses the configured service repair cost.
+Rearm service requests use:
+
+```sqf
+[QEGVAR(economy,RearmService), [_target, _unit, _cost]] call CBA_fnc_serverEvent;
+```
+
+`_cost` is optional. Passing `-1` uses the configured service rearm cost.
+`setVehicleAmmo` has global effects, but only adds ammo to local turrets, so
+the ammo reset is broadcast after billing succeeds.
+
Garage refuel service requests use:
```sqf
@@ -70,7 +81,7 @@ Fuel and repair services are organization-funded:
`commit = true`, and member service charging enabled.
4. Send the returned organization patch to online members.
5. If the charge fails, do not complete the service. Refueling rolls the target
- back to its starting fuel level; repairs are not applied.
+ back to its starting fuel level; repairs and rearming are not applied.
Direct refuel service requests, such as those from the garage UI, calculate
the missing fuel from `fuelCapacity`, charge the organization, and fill the
diff --git a/arma/server/addons/economy/XEH_preInit.sqf b/arma/server/addons/economy/XEH_preInit.sqf
index 34e561b..f941726 100644
--- a/arma/server/addons/economy/XEH_preInit.sqf
+++ b/arma/server/addons/economy/XEH_preInit.sqf
@@ -33,6 +33,11 @@ if (isNil QGVAR(SEconomyStore)) then { call FUNC(initSEconomyStore); };
GVAR(SEconomyStore) call ["repair", [_target, _unit, _cost]];
}] call CFUNC(addEventHandler);
+[QGVAR(RearmService), {
+ params ["_target", "_unit", ["_cost", -1, [0]]];
+ GVAR(SEconomyStore) call ["rearm", [_target, _unit, _cost]];
+}] call CFUNC(addEventHandler);
+
[QGVAR(RefuelService), {
params ["_target", "_unit"];
GVAR(FEconomyStore) call ["refuel", [_target, _unit]];
diff --git a/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf
index 72bcc13..3cd86d3 100644
--- a/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf
+++ b/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf
@@ -4,7 +4,7 @@
* File: fnc_initSEconomyStore.sqf
* Author: IDSolutions
* Date: 2025-12-20
- * Last Update: 2026-05-15
+ * Last Update: 2026-05-19
* Public: No
*
* Description:
@@ -27,6 +27,7 @@ GVAR(SEconomyStore) = createHashMapObject [[
["#type", "IServiceEconomy"],
["#create", {
GVAR(ServiceRepairCost) = 500;
+ GVAR(ServiceRearmCost) = 500;
["INFO", "Service Store Initialized!", nil, nil] call EFUNC(common,log);
}],
["notify", {
@@ -158,6 +159,22 @@ GVAR(SEconomyStore) = createHashMapObject [[
_self call ["notify", [_unit, "info", "Repair", format ["Repair complete. Organization charged $%1.", [_repairCost] call EFUNC(common,formatNumber)]]];
true
}],
+ ["rearm", {
+ params [["_target", objNull, [objNull]], ["_unit", objNull, [objNull]], ["_cost", -1, [0]]];
+
+ if (isNull _target || { isNull _unit }) exitWith { false };
+
+ private _rearmCost = [_cost, GVAR(ServiceRearmCost)] select (_cost < 0);
+ private _charge = _self call ["chargeOrg", [_unit, _rearmCost, "Rearm"]];
+ if !(_charge getOrDefault ["success", false]) exitWith {
+ _self call ["notify", [_unit, "danger", "Rearm", _charge getOrDefault ["message", "Organization funds cannot cover this rearm."]]];
+ false
+ };
+
+ [_target, 1] remoteExecCall ["setVehicleAmmo", 0];
+ _self call ["notify", [_unit, "info", "Rearm", format ["Rearm complete. Organization charged $%1.", [_rearmCost] call EFUNC(common,formatNumber)]]];
+ true
+ }],
["init", {}]
]];
diff --git a/docs/CLIENT_CAD_USAGE_GUIDE.md b/docs/CLIENT_CAD_USAGE_GUIDE.md
index 654f23b..1af8a96 100644
--- a/docs/CLIENT_CAD_USAGE_GUIDE.md
+++ b/docs/CLIENT_CAD_USAGE_GUIDE.md
@@ -37,6 +37,16 @@ assignments.
- group status, role, and profile requests
- map focus actions
+## Map Focus Behavior
+
+CAD list entries can drive the native map position without duplicating map
+logic in the browser UI. In operations mode, assigned or accepted task cards,
+roster member cards, and support request cards send focus events. In dispatch
+map mode, group, contract, and support request cards use the same focus path.
+
+Task and support request focus uses the stored record position. Roster member
+focus uses the member position included in the hydrated group roster.
+
## Browser Events
| Event | Client behavior |
@@ -58,6 +68,7 @@ assignments.
| `cad::groups::role` | Update group role. |
| `cad::groups::profile` | Update status and role together. |
| `cad::groups::focus` | Center map on a group. |
+| `cad::members::focus` | Center map on a group member. |
| `cad::tasks::focus` | Center map on a task. |
| `cad::requests::focus` | Center map on a support request. |
| `map::zoomIn` | Zoom native map in. |
diff --git a/docs/CLIENT_GARAGE_USAGE_GUIDE.md b/docs/CLIENT_GARAGE_USAGE_GUIDE.md
index cf116bb..f076ac1 100644
--- a/docs/CLIENT_GARAGE_USAGE_GUIDE.md
+++ b/docs/CLIENT_GARAGE_USAGE_GUIDE.md
@@ -56,21 +56,21 @@ is finalized and spawned onto the resolved lane.
| --- | --- |
| `garage::hydrate` | Initial vehicle and session payload. |
| `garage::sync` | Refreshed vehicle payload. |
-| `garage::service::success` | Browser notice for accepted refuel/repair requests. |
-| `garage::service::failure` | Browser notice for rejected refuel/repair requests. |
+| `garage::service::success` | Browser notice for accepted refuel/rearm/repair requests. |
+| `garage::service::failure` | Browser notice for rejected refuel/rearm/repair requests. |
Server action responses are handled by the action service and notification
flow.
## Vehicle Service
-The selected vehicle detail panel includes refuel and repair actions for nearby
+The selected vehicle detail panel includes refuel, rearm, and repair actions for nearby
world vehicles. Stored records must be retrieved first because server economy
services operate on live vehicle objects, not stored garage records.
-Refuel requests use the server economy `RefuelService` event. Repair requests
-use the server economy `RepairService` event. Both services are billed by the
-server economy addon through organization funds.
+Refuel requests use the server economy `RefuelService` event. Rearm requests
+use `RearmService`. Repair requests use `RepairService`. These services are
+billed by the server economy addon through organization funds.
## Mission Setup
diff --git a/docs/ECONOMY_USAGE_GUIDE.md b/docs/ECONOMY_USAGE_GUIDE.md
index 88b6181..eefa8cc 100644
--- a/docs/ECONOMY_USAGE_GUIDE.md
+++ b/docs/ECONOMY_USAGE_GUIDE.md
@@ -45,6 +45,24 @@ The target is only repaired after the organization charge succeeds.
The client garage UI forwards selected nearby vehicle repair requests through
the same event.
+## Rearm
+
+Rearm is organization-funded.
+
+Use the rearm service event:
+
+```sqf
+[QEGVAR(economy,RearmService), [_target, _unit, _cost]] call CBA_fnc_serverEvent;
+```
+
+`_cost` is optional. Passing `-1` uses the configured service rearm cost.
+The target is only rearmed after the organization charge succeeds.
+`setVehicleAmmo` has global effects, but the ammo is only added to local
+turrets, so the service broadcasts the ammo reset after billing succeeds.
+
+The client garage UI forwards selected nearby vehicle rearm requests through
+the same event.
+
## Medical
Medical is player-funded first.
diff --git a/docs/PLAYER_GUIDE.md b/docs/PLAYER_GUIDE.md
new file mode 100644
index 0000000..2f5663b
--- /dev/null
+++ b/docs/PLAYER_GUIDE.md
@@ -0,0 +1,321 @@
+# Player Guide
+
+Use this guide as the player-facing overview for Forge systems. It explains
+what players interact with during normal missions, how task assignment works,
+and what persistent storage limits apply.
+
+Player-guide screenshots are stored as JPG files under
+`docus/public/images/player`.
+
+## Opening Forge Interactions
+
+Most Forge actions are opened from the actor interaction menu while standing
+near a configured mission object.
+
+
+
+Press `Tab` by default to open the custom interaction menu. Server settings or
+local keybind changes may use a different key.
+
+Known current behavior: after closing the custom interaction menu, players may
+need to press `Tab` twice before it opens again. Treat this as a temporary
+workaround until the interaction menu focus behavior is investigated further.
+
+Players usually need to be within 5 meters of an interaction object such as a
+bank terminal, ATM, store counter, garage terminal, or locker.
+
+## CAD and Tasks
+
+CAD is the main task and dispatch system. It is used for mission contracts,
+group status, support requests, dispatch orders, and task assignment.
+
+
+
+Player workflow:
+
+1. Open CAD from the available interaction path.
+2. Review available or assigned tasks.
+3. If a dispatcher assigns a task to your group, the group leader must
+ acknowledge or decline it.
+4. Once acknowledged, the task becomes active for the assigned group.
+5. Complete the task objective shown by CAD, map task state, and mission
+ instructions.
+
+Map focus behavior:
+
+- Click an assigned or accepted task in the operations task board to center the
+ map on that task.
+- Click a roster member to center the map on that player.
+- Click a support request to center the map on the request location.
+- Dispatch map mode supports the same focus behavior for groups, contracts,
+ and support requests.
+
+Dispatch workflow:
+
+
+
+1. Open CAD with a dispatcher-enabled slot or permission.
+2. Use dispatch mode to review groups, open contracts, assigned contracts, and
+ support requests.
+3. Assign available contracts to active groups.
+4. Send dispatch orders or close completed orders as needed.
+5. Track group status and recent CAD activity.
+
+Dispatch access:
+
+- The CEO slot can administer the default organization and use CAD dispatch
+ permissions.
+- The Dispatch slot grants CAD dispatch permissions without default
+ organization administration rights.
+- Players who are the CEO or owner of their own organization also receive CAD
+ dispatch permissions.
+
+Important task behavior:
+
+- CAD assignment reserves a task for a group.
+- The task starts after the assigned group leader acknowledges it.
+- If the leader declines, the task returns to the open contract board.
+- Some task timers wait for group-leader acknowledgment before counting down.
+
+## Phone
+
+The phone provides contacts, messages, email, and local utility apps.
+
+
+
+### Contacts
+
+Use Contacts to keep track of other players by phone number or email address.
+Adding contacts makes it easier to start messages and emails without manually
+entering recipient details every time.
+
+
+
+### Messages
+
+Messages are short player-to-player conversations.
+
+
+
+Use Messages to:
+
+- start or continue a conversation with a contact
+- read incoming messages
+- mark messages as read
+- delete messages you no longer need
+
+### Email
+
+Email is used for longer player-to-player communication.
+
+
+
+Use Email to:
+
+- send a subject and body to another player
+- read incoming mail
+- mark email as read
+- delete old email
+
+### Local Phone Apps
+
+Notes, calendar events, clocks, alarms, and theme preferences are local utility
+features. They are saved for the local player profile and should not be treated
+as shared multiplayer data.
+
+## Bank and ATM
+
+Bank and ATM access are separate.
+
+Use a bank object for full banking:
+
+
+
+- view account information
+- transfer funds
+- deposit earnings
+- change PIN
+
+Use an ATM for limited account access:
+
+
+
+
+
+- PIN-gated account actions
+- ATM banking workflows
+- no PIN changes
+
+If a PIN prompt appears, enter the correct PIN before attempting account
+actions.
+
+## Organizations
+
+Players start in the default organization. A player can create a player-owned
+organization only if they have `$50,000` available for the registration fee.
+Organization access depends on the player's role.
+
+
+
+
+
+Default organization:
+
+- The `ceo` slot can administer the default organization.
+- The `dispatch` slot receives CAD dispatch permissions, but does not receive
+ default organization administration rights.
+
+Player-owned organizations:
+
+
+
+
+
+- The player who created the organization is its owner or CEO.
+- The owner can administer the organization, including treasury and roster
+ actions exposed by the organization interface.
+- Organization owners can invite players, manage members, assign credit lines,
+ transfer funds or run payroll when funds are available, and disband the
+ organization.
+- Organization owners can use organization funds for supported store purchases.
+- Members may receive assigned credit lines, accept or decline organization
+ invites, and leave the organization.
+- The organization CEO or owner cannot leave their own organization directly.
+ They must disband the organization if they want to leave it.
+
+Organization actions are server-authoritative. If an organization action fails,
+check that the player has the correct role, the player or organization has
+enough funds, and the target player is eligible for the action.
+
+## Store
+
+Stores sell unlocks and equipment through the configured server-side catalog.
+
+
+
+Store purchases may grant:
+
+- items or equipment added to the locker
+- matching gear unlocks in the virtual arsenal
+- vehicle unlocks in the virtual garage
+- other mission-configured rewards
+
+Store purchases are server-authoritative. If a purchase succeeds, the relevant
+bank, locker, virtual arsenal, virtual garage, or organization state updates
+from the server.
+
+
+
+Vehicle purchases unlock the vehicle in the virtual garage. They do not place a
+physical vehicle into the player's 5-slot garage. Use the virtual garage to
+spawn an unlocked vehicle, and use the garage to store or retrieve live world
+vehicles.
+
+## Locker and Virtual Arsenal
+
+The locker is personal item storage.
+
+
+
+Locker rules:
+
+- Up to 25 items can be stored.
+- The locker saves when the locker container is closed.
+- Over-capacity storage can warn or fail depending on server handling.
+
+The virtual arsenal is locked down. Players only see gear they have been
+granted or have unlocked through systems such as the store. The virtual arsenal
+is not intended to expose the full unrestricted Arma arsenal.
+
+
+
+## Garage and Virtual Garage
+
+The garage stores physical player vehicles that have been saved from the world.
+
+
+
+Garage rules:
+
+- Up to 5 vehicles can be stored.
+- Stored vehicles can be retrieved from a garage interaction point.
+- Retrieved vehicles become live world vehicles again.
+- Vehicle service actions operate on live nearby vehicles, not vehicles that
+ are still stored.
+
+The virtual garage is locked down. Players only see vehicles they have been
+granted or have unlocked through systems such as the store. Virtual garage
+unlocks are separate from the 5 physical vehicle slots in the garage. The
+virtual garage uses mission-configured spawn lanes, and spawning may be blocked
+if the spawn position is occupied.
+
+
+
+## Economy Services
+
+Economy services are server-controlled. Charges must succeed before the world
+effect is applied.
+
+
+
+### Medical
+
+Medical services are player-funded first.
+
+
+
+Billing order:
+
+1. Player bank balance.
+2. Player cash.
+3. Organization funds, when allowed by the server.
+4. Organization credit-line debt for the player when organization fallback is
+ used.
+
+Medical respawn placement uses mission-configured medical spawn objects.
+
+### Refuel
+
+Refuel service is organization-funded. If the organization cannot cover the
+cost, the vehicle is not refueled or the fuel level is rolled back.
+
+Refuel is available from the garage app dashboard shown above.
+
+### Repair
+
+Repair service is organization-funded. The repair is only applied after the
+organization charge succeeds.
+
+Repair is available from the garage app dashboard shown above.
+
+### Rearm
+
+If the mission exposes rearm service through the economy or support workflow,
+expect it to follow the same server-authoritative pattern: the service request
+must be accepted and billed before equipment or vehicle state changes are
+applied.
+
+Rearm is available from the garage app dashboard shown above.
+
+## Common Player Checks
+
+If a system does not appear or does not work:
+
+- Move closer to the interaction object.
+- Confirm you are using the correct object type, such as ATM vs bank.
+- Confirm your group leader has acknowledged an assigned CAD task.
+- Confirm the needed store unlock has been purchased before checking VA or VG.
+- Confirm the garage spawn point is clear before using the virtual garage.
+- Confirm your player, cash, bank, or organization funds can cover the service.
+
+## Related Guides
+
+- [Mission Designer Guide](./MISSION_DESIGNER_GUIDE.md)
+- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md)
+- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md)
+- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md)
+- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md)
+- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md)
+- [Organization Usage Guide](./ORG_USAGE_GUIDE.md)
+- [Store Usage Guide](./STORE_USAGE_GUIDE.md)
+- [Economy Usage Guide](./ECONOMY_USAGE_GUIDE.md)
diff --git a/docs/README.md b/docs/README.md
index 0b560b5..37f5281 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -30,6 +30,8 @@ See [SurrealDB Setup](./surrealdb-setup.md) for the full setup path.
- [Mission Designer Guide](./MISSION_DESIGNER_GUIDE.md): how to place Eden
objects, garage markers, and CAD-compatible task modules for playable
missions.
+- [Player Guide](./PLAYER_GUIDE.md): how players use CAD, phone, bank, store,
+ locker, garage, and economy services during missions.
- [SurrealDB Setup](./surrealdb-setup.md): where to get SurrealDB or
Surrealist and how to connect Forge to it for local or live use.
diff --git a/docus/content/1.getting-started/0.index.md b/docus/content/1.getting-started/0.index.md
index ae32302..9dcbe74 100644
--- a/docus/content/1.getting-started/0.index.md
+++ b/docus/content/1.getting-started/0.index.md
@@ -70,6 +70,16 @@ npm run build:webui
playable missions.
:::
+ :::u-page-card
+ ---
+ icon: i-lucide-user-round-check
+ title: Player Guide
+ to: /getting-started/player-guide
+ ---
+ Learn the player-facing CAD, phone, bank, store, locker, garage, and economy
+ workflows.
+ :::
+
:::u-page-card
---
icon: i-lucide-database
diff --git a/docus/content/1.getting-started/5.player-guide.md b/docus/content/1.getting-started/5.player-guide.md
new file mode 100644
index 0000000..803a90a
--- /dev/null
+++ b/docus/content/1.getting-started/5.player-guide.md
@@ -0,0 +1,320 @@
+---
+title: "Player Guide"
+description: "Use this guide as the player-facing overview for Forge systems. It explains what players interact with during normal missions, how task assignment works, and what persistent storage limits apply."
+---
+
+Player-guide screenshots are stored as JPG files under
+`docus/public/images/player`.
+
+## Opening Forge Interactions
+
+Most Forge actions are opened from the actor interaction menu while standing
+near a configured mission object.
+
+
+
+Press `Tab` by default to open the custom interaction menu. Server settings or
+local keybind changes may use a different key.
+
+Known current behavior: after closing the custom interaction menu, players may
+need to press `Tab` twice before it opens again. Treat this as a temporary
+workaround until the interaction menu focus behavior is investigated further.
+
+Players usually need to be within 5 meters of an interaction object such as a
+bank terminal, ATM, store counter, garage terminal, or locker.
+
+## CAD and Tasks
+
+CAD is the main task and dispatch system. It is used for mission contracts,
+group status, support requests, dispatch orders, and task assignment.
+
+
+
+Player workflow:
+
+1. Open CAD from the available interaction path.
+2. Review available or assigned tasks.
+3. If a dispatcher assigns a task to your group, the group leader must
+ acknowledge or decline it.
+4. Once acknowledged, the task becomes active for the assigned group.
+5. Complete the task objective shown by CAD, map task state, and mission
+ instructions.
+
+Map focus behavior:
+
+- Click an assigned or accepted task in the operations task board to center the
+ map on that task.
+- Click a roster member to center the map on that player.
+- Click a support request to center the map on the request location.
+- Dispatch map mode supports the same focus behavior for groups, contracts,
+ and support requests.
+
+Dispatch workflow:
+
+
+
+1. Open CAD with a dispatcher-enabled slot or permission.
+2. Use dispatch mode to review groups, open contracts, assigned contracts, and
+ support requests.
+3. Assign available contracts to active groups.
+4. Send dispatch orders or close completed orders as needed.
+5. Track group status and recent CAD activity.
+
+Dispatch access:
+
+- The CEO slot can administer the default organization and use CAD dispatch
+ permissions.
+- The Dispatch slot grants CAD dispatch permissions without default
+ organization administration rights.
+- Players who are the CEO or owner of their own organization also receive CAD
+ dispatch permissions.
+
+Important task behavior:
+
+- CAD assignment reserves a task for a group.
+- The task starts after the assigned group leader acknowledges it.
+- If the leader declines, the task returns to the open contract board.
+- Some task timers wait for group-leader acknowledgment before counting down.
+
+## Phone
+
+The phone provides contacts, messages, email, and local utility apps.
+
+
+
+### Contacts
+
+Use Contacts to keep track of other players by phone number or email address.
+Adding contacts makes it easier to start messages and emails without manually
+entering recipient details every time.
+
+
+
+### Messages
+
+Messages are short player-to-player conversations.
+
+
+
+Use Messages to:
+
+- start or continue a conversation with a contact
+- read incoming messages
+- mark messages as read
+- delete messages you no longer need
+
+### Email
+
+Email is used for longer player-to-player communication.
+
+
+
+Use Email to:
+
+- send a subject and body to another player
+- read incoming mail
+- mark email as read
+- delete old email
+
+### Local Phone Apps
+
+Notes, calendar events, clocks, alarms, and theme preferences are local utility
+features. They are saved for the local player profile and should not be treated
+as shared multiplayer data.
+
+## Bank and ATM
+
+Bank and ATM access are separate.
+
+Use a bank object for full banking:
+
+
+
+- view account information
+- transfer funds
+- deposit earnings
+- change PIN
+
+Use an ATM for limited account access:
+
+
+
+
+
+- PIN-gated account actions
+- ATM banking workflows
+- no PIN changes
+
+If a PIN prompt appears, enter the correct PIN before attempting account
+actions.
+
+## Organizations
+
+Players start in the default organization. A player can create a player-owned
+organization only if they have `$50,000` available for the registration fee.
+Organization access depends on the player's role.
+
+
+
+
+
+Default organization:
+
+- The `ceo` slot can administer the default organization.
+- The `dispatch` slot receives CAD dispatch permissions, but does not receive
+ default organization administration rights.
+
+Player-owned organizations:
+
+
+
+
+
+- The player who created the organization is its owner or CEO.
+- The owner can administer the organization, including treasury and roster
+ actions exposed by the organization interface.
+- Organization owners can invite players, manage members, assign credit lines,
+ transfer funds or run payroll when funds are available, and disband the
+ organization.
+- Organization owners can use organization funds for supported store purchases.
+- Members may receive assigned credit lines, accept or decline organization
+ invites, and leave the organization.
+- The organization CEO or owner cannot leave their own organization directly.
+ They must disband the organization if they want to leave it.
+
+Organization actions are server-authoritative. If an organization action fails,
+check that the player has the correct role, the player or organization has
+enough funds, and the target player is eligible for the action.
+
+## Store
+
+Stores sell unlocks and equipment through the configured server-side catalog.
+
+
+
+Store purchases may grant:
+
+- items or equipment added to the locker
+- matching gear unlocks in the virtual arsenal
+- vehicle unlocks in the virtual garage
+- other mission-configured rewards
+
+Store purchases are server-authoritative. If a purchase succeeds, the relevant
+bank, locker, virtual arsenal, virtual garage, or organization state updates
+from the server.
+
+
+
+Vehicle purchases unlock the vehicle in the virtual garage. They do not place a
+physical vehicle into the player's 5-slot garage. Use the virtual garage to
+spawn an unlocked vehicle, and use the garage to store or retrieve live world
+vehicles.
+
+## Locker and Virtual Arsenal
+
+The locker is personal item storage.
+
+
+
+Locker rules:
+
+- Up to 25 items can be stored.
+- The locker saves when the locker container is closed.
+- Over-capacity storage can warn or fail depending on server handling.
+
+The virtual arsenal is locked down. Players only see gear they have been
+granted or have unlocked through systems such as the store. The virtual arsenal
+is not intended to expose the full unrestricted Arma arsenal.
+
+
+
+## Garage and Virtual Garage
+
+The garage stores physical player vehicles that have been saved from the world.
+
+
+
+Garage rules:
+
+- Up to 5 vehicles can be stored.
+- Stored vehicles can be retrieved from a garage interaction point.
+- Retrieved vehicles become live world vehicles again.
+- Vehicle service actions operate on live nearby vehicles, not vehicles that
+ are still stored.
+
+The virtual garage is locked down. Players only see vehicles they have been
+granted or have unlocked through systems such as the store. Virtual garage
+unlocks are separate from the 5 physical vehicle slots in the garage. The
+virtual garage uses mission-configured spawn lanes, and spawning may be blocked
+if the spawn position is occupied.
+
+
+
+## Economy Services
+
+Economy services are server-controlled. Charges must succeed before the world
+effect is applied.
+
+
+
+### Medical
+
+Medical services are player-funded first.
+
+
+
+Billing order:
+
+1. Player bank balance.
+2. Player cash.
+3. Organization funds, when allowed by the server.
+4. Organization credit-line debt for the player when organization fallback is
+ used.
+
+Medical respawn placement uses mission-configured medical spawn objects.
+
+### Refuel
+
+Refuel service is organization-funded. If the organization cannot cover the
+cost, the vehicle is not refueled or the fuel level is rolled back.
+
+Refuel is available from the garage app dashboard shown above.
+
+### Repair
+
+Repair service is organization-funded. The repair is only applied after the
+organization charge succeeds.
+
+Repair is available from the garage app dashboard shown above.
+
+### Rearm
+
+If the mission exposes rearm service through the economy or support workflow,
+expect it to follow the same server-authoritative pattern: the service request
+must be accepted and billed before equipment or vehicle state changes are
+applied.
+
+Rearm is available from the garage app dashboard shown above.
+
+## Common Player Checks
+
+If a system does not appear or does not work:
+
+- Move closer to the interaction object.
+- Confirm you are using the correct object type, such as ATM vs bank.
+- Confirm your group leader has acknowledged an assigned CAD task.
+- Confirm the needed store unlock has been purchased before checking VA or VG.
+- Confirm the garage spawn point is clear before using the virtual garage.
+- Confirm your player, cash, bank, or organization funds can cover the service.
+
+## Related Guides
+
+- [Mission Designer Guide](/getting-started/mission-designer)
+- [Client CAD Usage Guide](/client-addons/cad)
+- [Client Phone Usage Guide](/client-addons/phone)
+- [Client Bank Usage Guide](/client-addons/bank)
+- [Client Garage Usage Guide](/client-addons/garage)
+- [Client Locker Usage Guide](/client-addons/locker)
+- [Organization Usage Guide](/server-modules/organization)
+- [Store Usage Guide](/server-modules/store)
+- [Economy Usage Guide](/server-modules/economy)
diff --git a/docus/content/1.getting-started/5.surrealdb-setup.md b/docus/content/1.getting-started/6.surrealdb-setup.md
similarity index 100%
rename from docus/content/1.getting-started/5.surrealdb-setup.md
rename to docus/content/1.getting-started/6.surrealdb-setup.md
diff --git a/docus/content/3.server-modules/4.economy.md b/docus/content/3.server-modules/4.economy.md
index b7e1a42..6a3df06 100644
--- a/docus/content/3.server-modules/4.economy.md
+++ b/docus/content/3.server-modules/4.economy.md
@@ -43,6 +43,24 @@ The target is only repaired after the organization charge succeeds.
The client garage UI forwards selected nearby vehicle repair requests through
the same event.
+## Rearm
+
+Rearm is organization-funded.
+
+Use the rearm service event:
+
+```sqf
+[QEGVAR(economy,RearmService), [_target, _unit, _cost]] call CBA_fnc_serverEvent;
+```
+
+`_cost` is optional. Passing `-1` uses the configured service rearm cost.
+The target is only rearmed after the organization charge succeeds.
+`setVehicleAmmo` has global effects, but the ammo is only added to local
+turrets, so the service broadcasts the ammo reset after billing succeeds.
+
+The client garage UI forwards selected nearby vehicle rearm requests through
+the same event.
+
## Medical
Medical is player-funded first.
diff --git a/docus/content/4.client-addons/5.cad.md b/docus/content/4.client-addons/5.cad.md
index 458eeac..57bc7b2 100644
--- a/docus/content/4.client-addons/5.cad.md
+++ b/docus/content/4.client-addons/5.cad.md
@@ -36,6 +36,16 @@ assignments.
- group status, role, and profile requests
- map focus actions
+## Map Focus Behavior
+
+CAD list entries can drive the native map position without duplicating map
+logic in the browser UI. In operations mode, assigned or accepted task cards,
+roster member cards, and support request cards send focus events. In dispatch
+map mode, group, contract, and support request cards use the same focus path.
+
+Task and support request focus uses the stored record position. Roster member
+focus uses the member position included in the hydrated group roster.
+
## Browser Events
| Event | Client behavior |
@@ -57,6 +67,7 @@ assignments.
| `cad::groups::role` | Update group role. |
| `cad::groups::profile` | Update status and role together. |
| `cad::groups::focus` | Center map on a group. |
+| `cad::members::focus` | Center map on a group member. |
| `cad::tasks::focus` | Center map on a task. |
| `cad::requests::focus` | Center map on a support request. |
| `map::zoomIn` | Zoom native map in. |
diff --git a/docus/content/4.client-addons/6.garage.md b/docus/content/4.client-addons/6.garage.md
index 67671e3..492dfda 100644
--- a/docus/content/4.client-addons/6.garage.md
+++ b/docus/content/4.client-addons/6.garage.md
@@ -55,21 +55,21 @@ is finalized and spawned onto the resolved lane.
| --- | --- |
| `garage::hydrate` | Initial vehicle and session payload. |
| `garage::sync` | Refreshed vehicle payload. |
-| `garage::service::success` | Browser notice for accepted refuel/repair requests. |
-| `garage::service::failure` | Browser notice for rejected refuel/repair requests. |
+| `garage::service::success` | Browser notice for accepted refuel/rearm/repair requests. |
+| `garage::service::failure` | Browser notice for rejected refuel/rearm/repair requests. |
Server action responses are handled by the action service and notification
flow.
## Vehicle Service
-The selected vehicle detail panel includes refuel and repair actions for nearby
+The selected vehicle detail panel includes refuel, rearm, and repair actions for nearby
world vehicles. Stored records must be retrieved first because server economy
services operate on live vehicle objects, not stored garage records.
-Refuel requests use the server economy `RefuelService` event. Repair requests
-use the server economy `RepairService` event. Both services are billed by the
-server economy addon through organization funds.
+Refuel requests use the server economy `RefuelService` event. Rearm requests
+use `RearmService`. Repair requests use `RepairService`. These services are
+billed by the server economy addon through organization funds.
## Mission Setup
diff --git a/docus/public/images/player/atm_app_home.jpg b/docus/public/images/player/atm_app_home.jpg
new file mode 100644
index 0000000..074d6d4
Binary files /dev/null and b/docus/public/images/player/atm_app_home.jpg differ
diff --git a/docus/public/images/player/atm_app_pin.jpg b/docus/public/images/player/atm_app_pin.jpg
new file mode 100644
index 0000000..518740f
Binary files /dev/null and b/docus/public/images/player/atm_app_pin.jpg differ
diff --git a/docus/public/images/player/bank_app.jpg b/docus/public/images/player/bank_app.jpg
new file mode 100644
index 0000000..a0a1e15
Binary files /dev/null and b/docus/public/images/player/bank_app.jpg differ
diff --git a/docus/public/images/player/cad_dispatch_board.jpg b/docus/public/images/player/cad_dispatch_board.jpg
new file mode 100644
index 0000000..f8dd719
Binary files /dev/null and b/docus/public/images/player/cad_dispatch_board.jpg differ
diff --git a/docus/public/images/player/cad_ops_board.jpg b/docus/public/images/player/cad_ops_board.jpg
new file mode 100644
index 0000000..9ea0970
Binary files /dev/null and b/docus/public/images/player/cad_ops_board.jpg differ
diff --git a/docus/public/images/player/garage.jpg b/docus/public/images/player/garage.jpg
new file mode 100644
index 0000000..bf8cf63
Binary files /dev/null and b/docus/public/images/player/garage.jpg differ
diff --git a/docus/public/images/player/interaction_menu.jpg b/docus/public/images/player/interaction_menu.jpg
new file mode 100644
index 0000000..7ff3dba
Binary files /dev/null and b/docus/public/images/player/interaction_menu.jpg differ
diff --git a/docus/public/images/player/locker.jpg b/docus/public/images/player/locker.jpg
new file mode 100644
index 0000000..59cd0f6
Binary files /dev/null and b/docus/public/images/player/locker.jpg differ
diff --git a/docus/public/images/player/medical_respawn.jpg b/docus/public/images/player/medical_respawn.jpg
new file mode 100644
index 0000000..c211af5
Binary files /dev/null and b/docus/public/images/player/medical_respawn.jpg differ
diff --git a/docus/public/images/player/org_dashboard.jpg b/docus/public/images/player/org_dashboard.jpg
new file mode 100644
index 0000000..be15976
Binary files /dev/null and b/docus/public/images/player/org_dashboard.jpg differ
diff --git a/docus/public/images/player/org_home.jpg b/docus/public/images/player/org_home.jpg
new file mode 100644
index 0000000..a176145
Binary files /dev/null and b/docus/public/images/player/org_home.jpg differ
diff --git a/docus/public/images/player/org_registration.jpg b/docus/public/images/player/org_registration.jpg
new file mode 100644
index 0000000..9475fc9
Binary files /dev/null and b/docus/public/images/player/org_registration.jpg differ
diff --git a/docus/public/images/player/org_treasury.jpg b/docus/public/images/player/org_treasury.jpg
new file mode 100644
index 0000000..a338bae
Binary files /dev/null and b/docus/public/images/player/org_treasury.jpg differ
diff --git a/docus/public/images/player/phone_contacts.jpg b/docus/public/images/player/phone_contacts.jpg
new file mode 100644
index 0000000..cea4a26
Binary files /dev/null and b/docus/public/images/player/phone_contacts.jpg differ
diff --git a/docus/public/images/player/phone_email.jpg b/docus/public/images/player/phone_email.jpg
new file mode 100644
index 0000000..b45fbba
Binary files /dev/null and b/docus/public/images/player/phone_email.jpg differ
diff --git a/docus/public/images/player/phone_home.jpg b/docus/public/images/player/phone_home.jpg
new file mode 100644
index 0000000..a1038be
Binary files /dev/null and b/docus/public/images/player/phone_home.jpg differ
diff --git a/docus/public/images/player/phone_messages.jpg b/docus/public/images/player/phone_messages.jpg
new file mode 100644
index 0000000..91b155d
Binary files /dev/null and b/docus/public/images/player/phone_messages.jpg differ
diff --git a/docus/public/images/player/store_catalog.jpg b/docus/public/images/player/store_catalog.jpg
new file mode 100644
index 0000000..82e3d08
Binary files /dev/null and b/docus/public/images/player/store_catalog.jpg differ
diff --git a/docus/public/images/player/store_checkout.jpg b/docus/public/images/player/store_checkout.jpg
new file mode 100644
index 0000000..f01d7ac
Binary files /dev/null and b/docus/public/images/player/store_checkout.jpg differ
diff --git a/docus/public/images/player/virtual_arsenal.jpg b/docus/public/images/player/virtual_arsenal.jpg
new file mode 100644
index 0000000..42c2fc5
Binary files /dev/null and b/docus/public/images/player/virtual_arsenal.jpg differ
diff --git a/docus/public/images/player/virtual_garage.jpg b/docus/public/images/player/virtual_garage.jpg
new file mode 100644
index 0000000..cc42860
Binary files /dev/null and b/docus/public/images/player/virtual_garage.jpg differ
diff --git a/tools/sync-docus-docs.mjs b/tools/sync-docus-docs.mjs
index 46a5953..c6c5228 100644
--- a/tools/sync-docus-docs.mjs
+++ b/tools/sync-docus-docs.mjs
@@ -23,9 +23,13 @@ const generatedPages = [
source: 'docs/MISSION_DESIGNER_GUIDE.md',
target: '1.getting-started/4.mission-designer.md'
},
+ {
+ source: 'docs/PLAYER_GUIDE.md',
+ target: '1.getting-started/5.player-guide.md'
+ },
{
source: 'docs/surrealdb-setup.md',
- target: '1.getting-started/5.surrealdb-setup.md'
+ target: '1.getting-started/6.surrealdb-setup.md'
},
{
source: 'arma/server/docs/README.md',
@@ -435,6 +439,16 @@ npm run build:webui
playable missions.
:::
+ :::u-page-card
+ ---
+ icon: i-lucide-user-round-check
+ title: Player Guide
+ to: /getting-started/player-guide
+ ---
+ Learn the player-facing CAD, phone, bank, store, locker, garage, and economy
+ workflows.
+ :::
+
:::u-page-card
---
icon: i-lucide-database