diff --git a/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf b/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf index 6357e9d..8f78075 100644 --- a/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf @@ -172,6 +172,14 @@ switch (_event) do { GVAR(CADUIBridge) call ["focusGroup", [_groupID]]; }; + case "cad::members::focus": { + private _uid = ""; + if (_data isEqualType createHashMap) then { + _uid = _data getOrDefault ["uid", ""]; + }; + + GVAR(CADUIBridge) call ["focusMember", [_uid]]; + }; case "cad::tasks::focus": { private _taskID = ""; if (_data isEqualType createHashMap) then { diff --git a/arma/client/addons/cad/functions/fnc_initUIBridge.sqf b/arma/client/addons/cad/functions/fnc_initUIBridge.sqf index 8e27f70..b72aa4a 100644 --- a/arma/client/addons/cad/functions/fnc_initUIBridge.sqf +++ b/arma/client/addons/cad/functions/fnc_initUIBridge.sqf @@ -319,6 +319,33 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [ ctrlMapAnimCommit _mapCtrl; true }], + ["focusMember", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { false }; + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _groups = GVAR(CADRepository) getOrDefault ["groups", []]; + private _position = []; + { + private _members = _x getOrDefault ["members", []]; + private _memberIndex = _members findIf { (_x getOrDefault ["uid", ""]) isEqualTo _uid }; + if (_memberIndex >= 0) exitWith { + _position = (_members # _memberIndex) getOrDefault ["position", []]; + }; + } forEach _groups; + + if !(_position isEqualType []) exitWith { false }; + if ((count _position) < 2) exitWith { false }; + + private _mapCtrl = _self call ["getMapControl", []]; + if (isNull _mapCtrl) exitWith { false }; + + private _targetPosition = [_position # 0, _position # 1, 0]; + _mapCtrl ctrlMapAnimAdd [0.35, ctrlMapScale _mapCtrl, _targetPosition]; + ctrlMapAnimCommit _mapCtrl; + true + }], ["focusTask", compileFinal { params [["_taskID", "", [""]]]; diff --git a/arma/client/addons/cad/ui/_site/cad-sidepanel.js b/arma/client/addons/cad/ui/_site/cad-sidepanel.js index abba8a2..6ff06af 100644 --- a/arma/client/addons/cad/ui/_site/cad-sidepanel.js +++ b/arma/client/addons/cad/ui/_site/cad-sidepanel.js @@ -1 +1 @@ -window.cadTasks={contracts:[],requests:[],groups:[],activity:[],session:{},mode:"operations",dispatchView:"board",activeTab:"contracts",selectedDispatchGroupId:"",selectedDispatchTaskId:"",selectedDispatchRequestId:"",focusStatusTimer:null,requestModalType:"",statuses:["available","en_route","on_task","holding","danger","unavailable"],roles:["infantry","recon","armor","air","logistics","support"],requestTypes:[{id:"medevac_9line",label:"9-Line MEDEVAC",defaultPriority:"emergency",fields:[{id:"pickup_location",label:"Line 1 Pickup Location",type:"text",defaultFromGroupPosition:!0},{id:"radio_freq",label:"Line 2 Radio / Call Sign",type:"text"},{id:"precedence",label:"Line 3 Precedence",type:"select",options:["urgent","urgent_surgical","priority","routine","convenience"]},{id:"special_equipment",label:"Line 4 Special Equipment",type:"select",options:["none","hoist","extraction","ventilator"]},{id:"patient_type",label:"Line 5 Patient Type",type:"select",options:["litter","ambulatory","mixed"]},{id:"security",label:"Line 6 Security",type:"select",options:["secure","possible_enemy","enemy_in_area","hot"]},{id:"marking",label:"Line 7 Marking",type:"select",options:["panels","smoke","ir","none","other"]},{id:"patient_nationality",label:"Line 8 Patient Nationality",type:"select",options:["coalition","civilian","enemy","epw","mixed"]},{id:"terrain",label:"Line 9 Terrain",type:"select",options:["flat","restricted","slope","rooftop","wooded"]}]},{id:"ace_lace",label:"ACE/LACE",defaultPriority:"routine",fields:[{id:"ammo",label:"Ammo",type:"textarea"},{id:"casualties",label:"Casualties",type:"textarea"},{id:"equipment",label:"Equipment",type:"textarea"},{id:"notes",label:"Notes",type:"textarea"}]},{id:"fire_support",label:"Fire Support",defaultPriority:"priority",fields:[{id:"target_location",label:"Target Location",type:"text",defaultFromGroupPosition:!0},{id:"target_description",label:"Target Description",type:"textarea"},{id:"requested_effect",label:"Requested Effect",type:"select",options:["suppress","destroy","illum","smoke","screen"]},{id:"ordnance",label:"Requested Ordnance",type:"text"},{id:"danger_close",label:"Danger Close",type:"select",options:["no","yes"]},{id:"remarks",label:"Remarks",type:"textarea"}]},{id:"air_support",label:"Air Support",defaultPriority:"priority",fields:[{id:"target_location",label:"Target Location",type:"text",defaultFromGroupPosition:!0},{id:"target_description",label:"Target Description",type:"textarea"},{id:"target_marking",label:"Target Marking",type:"select",options:["smoke","ir","laser","grid","visual"]},{id:"requested_effect",label:"Requested Effect",type:"select",options:["show_of_force","escort","suppress","destroy","recon"]},{id:"remarks",label:"Remarks",type:"textarea"}]},{id:"logreq",label:"LOGREQ",defaultPriority:"priority",fields:[{id:"category",label:"Category",type:"select",options:["ammo","medical","fuel","repair","vehicle","equipment","weapons","mixed"]},{id:"delivery_method",label:"Delivery Method",type:"select",options:["ground","airdrop","pickup","dispatch_discretion"]},{id:"delivery_location",label:"Delivery Location",type:"text",defaultFromGroupPosition:!0},{id:"requested_items",label:"Requested Items",type:"textarea"},{id:"quantity",label:"Quantity / Package",type:"text"},{id:"remarks",label:"Remarks",type:"textarea"}]}],init(){document.querySelectorAll(".cad-tab").forEach(e=>{e.addEventListener("click",()=>{this.setActiveTab(e.dataset.tab||"contracts")})}),document.getElementById("cadRequestModalCloseBtn").addEventListener("click",()=>{this.closeRequestModal()}),document.getElementById("cadRequestModalSaveBtn").addEventListener("click",()=>{this.submitSupportRequest()}),document.querySelector("#cadRequestModal .cad-modal-backdrop").addEventListener("click",()=>{this.closeRequestModal()}),window.ForgeBridge.on("cad::hydrate",e=>{this.setHydratePayload(e||{})}),window.ForgeBridge.on("cad::assignment::response",e=>{this.handleServerResponse(!!e.success,e.message||"")}),window.ForgeBridge.on("cad::group::response",e=>{this.handleServerResponse(!!e.success,e.message||"")}),window.ForgeBridge.on("cad::request::response",e=>{this.handleServerResponse(!!e.success,e.message||"")}),window.ForgeBridge.ready({loaded:!0})},setActiveTab(e){this.activeTab=e||"contracts",document.querySelectorAll(".cad-tab").forEach(e=>{e.classList.toggle("is-active",e.dataset.tab===this.activeTab)}),document.querySelectorAll("[data-panel]").forEach(e=>{e.classList.toggle("is-active",e.dataset.panel===this.activeTab)})},syncLayoutState(){const e=document.querySelector(".cad-tabs"),t=document.getElementById("tabContractsBtn"),s=document.getElementById("tabRosterBtn"),a=document.getElementById("tabRequestsBtn"),n=document.getElementById("tabActivityBtn"),i=document.getElementById("contractsPanel"),r=document.getElementById("rosterPanel"),o=document.getElementById("requestsPanel"),d=document.getElementById("activityPanel"),c=i?.querySelector(".cad-section-header"),l=r?.querySelector(".cad-section-header");if(this.isDispatchMapMode())return e&&(e.style.display="",e.classList.remove("is-two-col"),e.classList.add("is-three-col")),t&&(t.style.display=""),s&&(s.textContent="Groups"),n&&(n.style.display="none"),a&&(a.style.display=""),d&&(d.classList.remove("is-active"),d.style.display="none"),o&&(o.style.display=""),r&&(r.style.display=""),l&&(l.textContent="Active Groups"),i&&(i.style.display=""),c&&(c.textContent="Contracts"),void(["contracts","roster","requests"].includes(this.activeTab)||(this.activeTab="contracts"));e&&(e.style.display="",e.classList.remove("is-three-col"),e.classList.remove("is-two-col")),t&&(t.style.display=""),s&&(s.textContent="Roster"),n&&(n.style.display=""),a&&(a.style.display=""),i&&(i.style.display=""),d&&(d.style.display=""),o&&(o.style.display=""),r&&(r.style.display=""),l&&(l.textContent="Roster"),c&&(c.textContent="Contracts")},setHydratePayload(e){this.contracts=Array.isArray(e.contracts)?e.contracts:[],this.requests=Array.isArray(e.requests)?e.requests:[],this.groups=Array.isArray(e.groups)?e.groups:[],this.activity=Array.isArray(e.activity)?e.activity:[],this.session=e.session&&"object"==typeof e.session?e.session:{},this.mode=e&&"string"==typeof e.mode?e.mode:"operations",this.dispatchView=e&&"string"==typeof e.dispatchView?e.dispatchView:"board";const t=document.getElementById("cadStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.selectedDispatchGroupId&&!this.groups.some(e=>e.groupId===this.selectedDispatchGroupId)&&(this.selectedDispatchGroupId=""),this.selectedDispatchTaskId&&!this.contracts.some(e=>(e.taskId||e.taskID||"")===this.selectedDispatchTaskId)&&(this.selectedDispatchTaskId=""),this.selectedDispatchRequestId&&!this.requests.some(e=>(e.requestId||"")===this.selectedDispatchRequestId)&&(this.selectedDispatchRequestId=""),"dispatch"!==this.mode||"map"!==this.dispatchView||["contracts","roster","requests"].includes(this.activeTab)||(this.activeTab="contracts"),this.render()},setStatus(e,t){const s=document.getElementById("cadStatusMessage");s&&(s.textContent=e||"",s.dataset.type=t||"")},getDangerGroups(){return this.groups.filter(e=>"danger"===(e.status||""))},getSupportAlertRequests(){return this.requests.filter(e=>["medevac_9line","fire_support","air_support"].includes(e.type||""))},buildSupportAlertMessage(){const e=this.getSupportAlertRequests();if(!e.length)return"";return`Support request alert: ${e.map(e=>`${e.groupCallsign||e.groupId||"Unknown Group"} ${this.getRequestTypeLabel(e.type||"request")}`).join(", ")}`},getCurrentGroupCoordinates(){const e=this.getCurrentGroup(),t=Array.isArray(e?.position)?e.position:[0,0,0];return window.mapUI.formatPosition(t)},getSortedGroups(){return this.groups.slice().sort((e,t)=>{const s="danger"===(e.status||"")?0:1,a="danger"===(t.status||"")?0:1;if(s!==a)return s-a;const n=e.callsign||e.groupId||"",i=t.callsign||t.groupId||"";return n.localeCompare(i)})},isDispatchOrder:e=>!!e.isDispatchOrder||"dispatch_order"===(e.type||""),formatTypeLabel(e){const t=(e.type||"task").replaceAll("_"," ");return this.isDispatchOrder(e)?"dispatch order":t},getRequestDefinition(e){return this.requestTypes.find(t=>t.id===e)||null},getRequestTypeLabel(e){return this.getRequestDefinition(e)?.label||e},canSubmitSupportRequest(){return"operations"===this.mode&&this.isLeader()},openRequestModal(e){const t=this.getRequestDefinition(e);t&&(this.requestModalType=e,document.getElementById("cadRequestModalTitle").textContent=t.label,document.getElementById("cadRequestPrioritySelect").value=t.defaultPriority||"priority",this.renderRequestFields(t),document.getElementById("cadRequestModal").classList.remove("is-hidden"))},closeRequestModal(){this.requestModalType="",document.getElementById("cadRequestFields").innerHTML="",document.getElementById("cadRequestModal").classList.add("is-hidden")},renderRequestFields(e){const t=document.getElementById("cadRequestFields");if(!t||!e)return;const s=this.getCurrentGroupCoordinates();t.innerHTML=e.fields.map(e=>{const t=e.defaultFromGroupPosition?s:"";return"select"===e.type?`\n \n `:"textarea"===e.type?`\n \n `:`\n \n `}).join("")},submitSupportRequest(){const e=this.getRequestDefinition(this.requestModalType);if(!e)return;const t={};e.fields.forEach(e=>{const s=document.getElementById(`cadRequestField_${e.id}`);t[e.id]=s?String(s.value||"").trim():""});const s=document.getElementById("cadRequestPrioritySelect").value;this.setStatus("Submitting support request...","info"),window.mapUI.sendEvent("cad::supportRequest::submit",{type:e.id,fields:t,priority:s}),this.closeRequestModal()},closeSupportRequest(e){e&&(this.setStatus(this.isDispatchMode()?"Closing support request...":"Cancelling support request...","info"),window.mapUI.sendEvent("cad::supportRequest::close",{requestID:e}))},renderRequests(){const e=document.getElementById("requestList");if(!e)return;if(this.isDispatchMapMode()){const t=this.requests.slice().sort((e,t)=>{const s=e.title||e.requestId||"",a=t.title||t.requestId||"";return s.localeCompare(a)});return t.length?void(e.innerHTML=t.map(e=>{const t=e.requestId||"",s=Array.isArray(e.position)?e.position:[0,0,0];return`\n \n
\n ${e.title||t||"Support Request"}\n ${this.getRequestTypeLabel(e.type||"request")}\n
\n

${e.summary||""}

\n
\n Group: ${e.groupCallsign||e.groupId||"Unknown"}\n ${(e.priority||"priority").replaceAll("_"," ")}\n
\n
\n ${window.mapUI.formatPosition(s)}\n ${t||"request"}\n
\n \n `}).join("")):void(e.innerHTML='

No support requests are currently active.

')}const t=this.canSubmitSupportRequest()?`\n
\n ${this.requestTypes.map(e=>`\n \n ${e.label}\n \n `).join("")}\n
\n `:"";this.requests.length?e.innerHTML=`\n ${t}\n ${this.requests.map(e=>{const t=this.isLeader()&&(e.groupId||"")===this.getPlayerGroupId(),s=this.canDispatch()||t,a=this.isDispatchMode()?"Close":"Cancel";return`\n
\n
\n ${e.title||this.getRequestTypeLabel(e.type||"")}\n ${(e.priority||"priority").replaceAll("_"," ")}\n
\n

${e.summary||""}

\n
\n Group: ${e.groupCallsign||e.groupId||"Unknown"}\n ${this.getRequestTypeLabel(e.type||"")}\n
\n ${s?`
\n \n
`:""}\n
\n `}).join("")}\n `:e.innerHTML=`\n ${t}\n

No support requests are currently active.

\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="";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()},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="";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="";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='

No contracts are currently available.

');const t=this.contracts.slice().sort((e,t)=>{const s="unassigned"===(e.assignmentState||"unassigned")?0:1,a="unassigned"===(t.assignmentState||"unassigned")?0:1;if(s!==a)return s-a;const n=e.taskId||e.taskID||"",i=t.taskId||t.taskID||"";return n.localeCompare(i)});return void(e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",s=Array.isArray(e.position)?e.position:[0,0,0],a=e.assignedGroupId||"",n=e.assignmentState||"unassigned",i=this.groups.find(e=>e.groupId===a),r=t===this.selectedDispatchTaskId,o="unassigned"===n?"Unassigned":`${n}: ${i?i.callsign:a||"Unknown"}`;return`\n \n
\n ${e.title||t}\n ${this.formatTypeLabel(e)}\n
\n

${e.description||""}

\n
\n ${o}\n ${window.mapUI.formatPosition(s)}\n
\n \n `}).join(""))}const t=this.getPlayerGroupId(),s=this.contracts.filter(e=>(e.assignedGroupId||"")===t);s.length?e.innerHTML=s.map(e=>{const s=e.taskId||e.taskID||"",a=Array.isArray(e.position)?e.position:[0,0,0],n=e.assignedGroupId||"",i=e.assignmentState||"unassigned",r=this.groups.find(e=>e.groupId===n),o=this.isLeader()&&n===t;return`\n
\n
\n ${e.title||s}\n ${this.formatTypeLabel(e)}\n
\n

${e.description||""}

\n
\n ${"unassigned"===i?"Available":`${i}: ${r?r.callsign:n}`}\n ${window.mapUI.formatPosition(a)}\n
\n ${o&&"assigned"===i?`
\n \n \n
`:""}\n
\n `}).join(""):e.innerHTML='

No contract is currently assigned to your group.

'},renderRoster(){const e=document.getElementById("rosterList");if(!e)return;if(this.isDispatchMapMode())return this.groups.length?void(e.innerHTML=this.getSortedGroups().map(e=>{const t=(e.groupId||"")===this.selectedDispatchGroupId,s="danger"===(e.status||"");return`\n \n
\n ${e.callsign||e.groupId||"Unknown Group"}\n ${e.role||"group"}\n ${s?'Danger':""}\n
\n
\n Leader: ${e.leaderName||"Unknown"}\n Status: ${e.status||"unknown"}\n
\n
\n Members: ${this.normalizeCollection(e.members).length}\n Task: ${e.currentTaskId||"None"}\n
\n \n `}).join("")):void(e.innerHTML='

No active groups are currently available.

');const t=this.getCurrentGroup();if(!t)return void(e.innerHTML='

Your group is not currently available.

');const s=this.normalizeCollection(t.members),a="danger"===(t.status||"");s.length?e.innerHTML=`\n
\n
\n ${t.callsign||t.groupId||"Current Group"}\n ${s.length} member${1===s.length?"":"s"}\n ${a?'Danger':""}\n
\n
\n Leader: ${t.leaderName||"Unknown"}\n Status: ${t.status||"unknown"}\n
\n
\n Role: ${t.role||"unassigned"}\n Task: ${t.currentTaskId||"None"}\n
\n
\n ${s.map(e=>{const t=(e.lifeState||"unknown").replaceAll("_"," "),s=e.isLeader?'Leader':"";return`\n
\n
\n ${e.name||"Unknown Operator"}\n ${t}\n
\n
\n ${e.uid||"No UID"}\n ${s}\n
\n
\n `}).join("")}\n `:e.innerHTML='

No roster members are currently available.

'},renderActivity(){const e=document.getElementById("activityList");e&&(this.activity.length?e.innerHTML=this.activity.slice().reverse().slice(0,8).map(e=>`\n
\n
\n ${e.type||"activity"}\n ${Math.round(e.timestamp||0)}s\n
\n

${e.message||""}

\n
\n `).join(""):e.innerHTML='

No recent activity.

')},render(){this.updateDangerAlert(),this.updateRequestAlert(),this.syncLayoutState(),this.renderContracts(),this.renderRoster(),this.renderRequests(),this.renderActivity(),this.setActiveTab(this.activeTab)}},window.cadTasks.init(); \ No newline at end of file +window.cadTasks={contracts:[],requests:[],groups:[],activity:[],session:{},mode:"operations",dispatchView:"board",activeTab:"contracts",selectedDispatchGroupId:"",selectedDispatchTaskId:"",selectedDispatchRequestId:"",selectedRosterMemberUid:"",focusStatusTimer:null,requestModalType:"",statuses:["available","en_route","on_task","holding","danger","unavailable"],roles:["infantry","recon","armor","air","logistics","support"],requestTypes:[{id:"medevac_9line",label:"9-Line MEDEVAC",defaultPriority:"emergency",fields:[{id:"pickup_location",label:"Line 1 Pickup Location",type:"text",defaultFromGroupPosition:!0},{id:"radio_freq",label:"Line 2 Radio / Call Sign",type:"text"},{id:"precedence",label:"Line 3 Precedence",type:"select",options:["urgent","urgent_surgical","priority","routine","convenience"]},{id:"special_equipment",label:"Line 4 Special Equipment",type:"select",options:["none","hoist","extraction","ventilator"]},{id:"patient_type",label:"Line 5 Patient Type",type:"select",options:["litter","ambulatory","mixed"]},{id:"security",label:"Line 6 Security",type:"select",options:["secure","possible_enemy","enemy_in_area","hot"]},{id:"marking",label:"Line 7 Marking",type:"select",options:["panels","smoke","ir","none","other"]},{id:"patient_nationality",label:"Line 8 Patient Nationality",type:"select",options:["coalition","civilian","enemy","epw","mixed"]},{id:"terrain",label:"Line 9 Terrain",type:"select",options:["flat","restricted","slope","rooftop","wooded"]}]},{id:"ace_lace",label:"ACE/LACE",defaultPriority:"routine",fields:[{id:"ammo",label:"Ammo",type:"textarea"},{id:"casualties",label:"Casualties",type:"textarea"},{id:"equipment",label:"Equipment",type:"textarea"},{id:"notes",label:"Notes",type:"textarea"}]},{id:"fire_support",label:"Fire Support",defaultPriority:"priority",fields:[{id:"target_location",label:"Target Location",type:"text",defaultFromGroupPosition:!0},{id:"target_description",label:"Target Description",type:"textarea"},{id:"requested_effect",label:"Requested Effect",type:"select",options:["suppress","destroy","illum","smoke","screen"]},{id:"ordnance",label:"Requested Ordnance",type:"text"},{id:"danger_close",label:"Danger Close",type:"select",options:["no","yes"]},{id:"remarks",label:"Remarks",type:"textarea"}]},{id:"air_support",label:"Air Support",defaultPriority:"priority",fields:[{id:"target_location",label:"Target Location",type:"text",defaultFromGroupPosition:!0},{id:"target_description",label:"Target Description",type:"textarea"},{id:"target_marking",label:"Target Marking",type:"select",options:["smoke","ir","laser","grid","visual"]},{id:"requested_effect",label:"Requested Effect",type:"select",options:["show_of_force","escort","suppress","destroy","recon"]},{id:"remarks",label:"Remarks",type:"textarea"}]},{id:"logreq",label:"LOGREQ",defaultPriority:"priority",fields:[{id:"category",label:"Category",type:"select",options:["ammo","medical","fuel","repair","vehicle","equipment","weapons","mixed"]},{id:"delivery_method",label:"Delivery Method",type:"select",options:["ground","airdrop","pickup","dispatch_discretion"]},{id:"delivery_location",label:"Delivery Location",type:"text",defaultFromGroupPosition:!0},{id:"requested_items",label:"Requested Items",type:"textarea"},{id:"quantity",label:"Quantity / Package",type:"text"},{id:"remarks",label:"Remarks",type:"textarea"}]}],init(){document.querySelectorAll(".cad-tab").forEach(e=>{e.addEventListener("click",()=>{this.setActiveTab(e.dataset.tab||"contracts")})}),document.getElementById("cadRequestModalCloseBtn").addEventListener("click",()=>{this.closeRequestModal()}),document.getElementById("cadRequestModalSaveBtn").addEventListener("click",()=>{this.submitSupportRequest()}),document.querySelector("#cadRequestModal .cad-modal-backdrop").addEventListener("click",()=>{this.closeRequestModal()}),window.ForgeBridge.on("cad::hydrate",e=>{this.setHydratePayload(e||{})}),window.ForgeBridge.on("cad::assignment::response",e=>{this.handleServerResponse(!!e.success,e.message||"")}),window.ForgeBridge.on("cad::group::response",e=>{this.handleServerResponse(!!e.success,e.message||"")}),window.ForgeBridge.on("cad::request::response",e=>{this.handleServerResponse(!!e.success,e.message||"")}),window.ForgeBridge.ready({loaded:!0})},setActiveTab(e){this.activeTab=e||"contracts",document.querySelectorAll(".cad-tab").forEach(e=>{e.classList.toggle("is-active",e.dataset.tab===this.activeTab)}),document.querySelectorAll("[data-panel]").forEach(e=>{e.classList.toggle("is-active",e.dataset.panel===this.activeTab)})},syncLayoutState(){const e=document.querySelector(".cad-tabs"),t=document.getElementById("tabContractsBtn"),s=document.getElementById("tabRosterBtn"),a=document.getElementById("tabRequestsBtn"),n=document.getElementById("tabActivityBtn"),i=document.getElementById("contractsPanel"),r=document.getElementById("rosterPanel"),o=document.getElementById("requestsPanel"),d=document.getElementById("activityPanel"),c=i?.querySelector(".cad-section-header"),l=r?.querySelector(".cad-section-header");if(this.isDispatchMapMode())return e&&(e.style.display="",e.classList.remove("is-two-col"),e.classList.add("is-three-col")),t&&(t.style.display=""),s&&(s.textContent="Groups"),n&&(n.style.display="none"),a&&(a.style.display=""),d&&(d.classList.remove("is-active"),d.style.display="none"),o&&(o.style.display=""),r&&(r.style.display=""),l&&(l.textContent="Active Groups"),i&&(i.style.display=""),c&&(c.textContent="Contracts"),void(["contracts","roster","requests"].includes(this.activeTab)||(this.activeTab="contracts"));e&&(e.style.display="",e.classList.remove("is-three-col"),e.classList.remove("is-two-col")),t&&(t.style.display=""),s&&(s.textContent="Roster"),n&&(n.style.display=""),a&&(a.style.display=""),i&&(i.style.display=""),d&&(d.style.display=""),o&&(o.style.display=""),r&&(r.style.display=""),l&&(l.textContent="Roster"),c&&(c.textContent="Contracts")},setHydratePayload(e){this.contracts=Array.isArray(e.contracts)?e.contracts:[],this.requests=Array.isArray(e.requests)?e.requests:[],this.groups=Array.isArray(e.groups)?e.groups:[],this.activity=Array.isArray(e.activity)?e.activity:[],this.session=e.session&&"object"==typeof e.session?e.session:{},this.mode=e&&"string"==typeof e.mode?e.mode:"operations",this.dispatchView=e&&"string"==typeof e.dispatchView?e.dispatchView:"board";const t=document.getElementById("cadStatusMessage");if(!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.selectedDispatchGroupId&&!this.groups.some(e=>e.groupId===this.selectedDispatchGroupId)&&(this.selectedDispatchGroupId=""),this.selectedRosterMemberUid){this.groups.some(e=>this.normalizeCollection(e.members).some(e=>(e.uid||"")===this.selectedRosterMemberUid))||(this.selectedRosterMemberUid="")}this.selectedDispatchTaskId&&!this.contracts.some(e=>(e.taskId||e.taskID||"")===this.selectedDispatchTaskId)&&(this.selectedDispatchTaskId=""),this.selectedDispatchRequestId&&!this.requests.some(e=>(e.requestId||"")===this.selectedDispatchRequestId)&&(this.selectedDispatchRequestId=""),"dispatch"!==this.mode||"map"!==this.dispatchView||["contracts","roster","requests"].includes(this.activeTab)||(this.activeTab="contracts"),this.render()},setStatus(e,t){const s=document.getElementById("cadStatusMessage");s&&(s.textContent=e||"",s.dataset.type=t||"")},getDangerGroups(){return this.groups.filter(e=>"danger"===(e.status||""))},getSupportAlertRequests(){return this.requests.filter(e=>["medevac_9line","fire_support","air_support"].includes(e.type||""))},buildSupportAlertMessage(){const e=this.getSupportAlertRequests();if(!e.length)return"";return`Support request alert: ${e.map(e=>`${e.groupCallsign||e.groupId||"Unknown Group"} ${this.getRequestTypeLabel(e.type||"request")}`).join(", ")}`},getCurrentGroupCoordinates(){const e=this.getCurrentGroup(),t=Array.isArray(e?.position)?e.position:[0,0,0];return window.mapUI.formatPosition(t)},getSortedGroups(){return this.groups.slice().sort((e,t)=>{const s="danger"===(e.status||"")?0:1,a="danger"===(t.status||"")?0:1;if(s!==a)return s-a;const n=e.callsign||e.groupId||"",i=t.callsign||t.groupId||"";return n.localeCompare(i)})},isDispatchOrder:e=>!!e.isDispatchOrder||"dispatch_order"===(e.type||""),formatTypeLabel(e){const t=(e.type||"task").replaceAll("_"," ");return this.isDispatchOrder(e)?"dispatch order":t},getRequestDefinition(e){return this.requestTypes.find(t=>t.id===e)||null},getRequestTypeLabel(e){return this.getRequestDefinition(e)?.label||e},canSubmitSupportRequest(){return"operations"===this.mode&&this.isLeader()},openRequestModal(e){const t=this.getRequestDefinition(e);t&&(this.requestModalType=e,document.getElementById("cadRequestModalTitle").textContent=t.label,document.getElementById("cadRequestPrioritySelect").value=t.defaultPriority||"priority",this.renderRequestFields(t),document.getElementById("cadRequestModal").classList.remove("is-hidden"))},closeRequestModal(){this.requestModalType="",document.getElementById("cadRequestFields").innerHTML="",document.getElementById("cadRequestModal").classList.add("is-hidden")},renderRequestFields(e){const t=document.getElementById("cadRequestFields");if(!t||!e)return;const s=this.getCurrentGroupCoordinates();t.innerHTML=e.fields.map(e=>{const t=e.defaultFromGroupPosition?s:"";return"select"===e.type?`\n \n `:"textarea"===e.type?`\n \n `:`\n \n `}).join("")},submitSupportRequest(){const e=this.getRequestDefinition(this.requestModalType);if(!e)return;const t={};e.fields.forEach(e=>{const s=document.getElementById(`cadRequestField_${e.id}`);t[e.id]=s?String(s.value||"").trim():""});const s=document.getElementById("cadRequestPrioritySelect").value;this.setStatus("Submitting support request...","info"),window.mapUI.sendEvent("cad::supportRequest::submit",{type:e.id,fields:t,priority:s}),this.closeRequestModal()},closeSupportRequest(e){e&&(this.setStatus(this.isDispatchMode()?"Closing support request...":"Cancelling support request...","info"),window.mapUI.sendEvent("cad::supportRequest::close",{requestID:e}))},renderRequests(){const e=document.getElementById("requestList");if(!e)return;if(this.isDispatchMapMode()){const t=this.requests.slice().sort((e,t)=>{const s=e.title||e.requestId||"",a=t.title||t.requestId||"";return s.localeCompare(a)});return t.length?void(e.innerHTML=t.map(e=>{const t=e.requestId||"",s=Array.isArray(e.position)?e.position:[0,0,0];return`\n \n
\n ${e.title||t||"Support Request"}\n ${this.getRequestTypeLabel(e.type||"request")}\n
\n

${e.summary||""}

\n
\n Group: ${e.groupCallsign||e.groupId||"Unknown"}\n ${(e.priority||"priority").replaceAll("_"," ")}\n
\n
\n ${window.mapUI.formatPosition(s)}\n ${t||"request"}\n
\n \n `}).join("")):void(e.innerHTML='

No support requests are currently active.

')}const t=this.canSubmitSupportRequest()?`\n
\n ${this.requestTypes.map(e=>`\n \n ${e.label}\n \n `).join("")}\n
\n `:"";this.requests.length?e.innerHTML=`\n ${t}\n ${this.requests.map(e=>{const t=this.isLeader()&&(e.groupId||"")===this.getPlayerGroupId(),s=this.canDispatch()||t,a=this.isDispatchMode()?"Close":"Cancel",n=e.requestId||"";return`\n \n
\n ${e.title||this.getRequestTypeLabel(e.type||"")}\n ${(e.priority||"priority").replaceAll("_"," ")}\n
\n

${e.summary||""}

\n
\n Group: ${e.groupCallsign||e.groupId||"Unknown"}\n ${this.getRequestTypeLabel(e.type||"")}\n
\n ${s?`
\n \n
`:""}\n \n `}).join("")}\n `:e.innerHTML=`\n ${t}\n

No support requests are currently active.

\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='

No contracts are currently available.

');const t=this.contracts.slice().sort((e,t)=>{const s="unassigned"===(e.assignmentState||"unassigned")?0:1,a="unassigned"===(t.assignmentState||"unassigned")?0:1;if(s!==a)return s-a;const n=e.taskId||e.taskID||"",i=t.taskId||t.taskID||"";return n.localeCompare(i)});return void(e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",s=Array.isArray(e.position)?e.position:[0,0,0],a=e.assignedGroupId||"",n=e.assignmentState||"unassigned",i=this.groups.find(e=>e.groupId===a),r=t===this.selectedDispatchTaskId,o="unassigned"===n?"Unassigned":`${n}: ${i?i.callsign:a||"Unknown"}`;return`\n \n
\n ${e.title||t}\n ${this.formatTypeLabel(e)}\n
\n

${e.description||""}

\n
\n ${o}\n ${window.mapUI.formatPosition(s)}\n
\n \n `}).join(""))}const t=this.getPlayerGroupId(),s=this.contracts.filter(e=>(e.assignedGroupId||"")===t);s.length?e.innerHTML=s.map(e=>{const s=e.taskId||e.taskID||"",a=Array.isArray(e.position)?e.position:[0,0,0],n=e.assignedGroupId||"",i=e.assignmentState||"unassigned",r=this.groups.find(e=>e.groupId===n),o=this.isLeader()&&n===t;return`\n \n
\n ${e.title||s}\n ${this.formatTypeLabel(e)}\n
\n

${e.description||""}

\n
\n ${"unassigned"===i?"Available":`${i}: ${r?r.callsign:n}`}\n ${window.mapUI.formatPosition(a)}\n
\n ${o&&"assigned"===i?`
\n \n \n
`:""}\n \n `}).join(""):e.innerHTML='

No contract is currently assigned to your group.

'},renderRoster(){const e=document.getElementById("rosterList");if(!e)return;if(this.isDispatchMapMode())return this.groups.length?void(e.innerHTML=this.getSortedGroups().map(e=>{const t=(e.groupId||"")===this.selectedDispatchGroupId,s="danger"===(e.status||"");return`\n \n
\n ${e.callsign||e.groupId||"Unknown Group"}\n ${e.role||"group"}\n ${s?'Danger':""}\n
\n
\n Leader: ${e.leaderName||"Unknown"}\n Status: ${e.status||"unknown"}\n
\n
\n Members: ${this.normalizeCollection(e.members).length}\n Task: ${e.currentTaskId||"None"}\n
\n \n `}).join("")):void(e.innerHTML='

No active groups are currently available.

');const t=this.getCurrentGroup();if(!t)return void(e.innerHTML='

Your group is not currently available.

');const s=this.normalizeCollection(t.members),a="danger"===(t.status||"");s.length?e.innerHTML=`\n
\n
\n ${t.callsign||t.groupId||"Current Group"}\n ${s.length} member${1===s.length?"":"s"}\n ${a?'Danger':""}\n
\n
\n Leader: ${t.leaderName||"Unknown"}\n Status: ${t.status||"unknown"}\n
\n
\n Role: ${t.role||"unassigned"}\n Task: ${t.currentTaskId||"None"}\n
\n
\n ${s.map(e=>{const t=(e.lifeState||"unknown").replaceAll("_"," "),s=e.isLeader?'Leader':"",a=e.uid||"";return`\n \n
\n ${e.name||"Unknown Operator"}\n ${t}\n
\n
\n ${e.uid||"No UID"}\n ${s}\n
\n \n `}).join("")}\n `:e.innerHTML='

No roster members are currently available.

'},renderActivity(){const e=document.getElementById("activityList");e&&(this.activity.length?e.innerHTML=this.activity.slice().reverse().slice(0,8).map(e=>`\n
\n
\n ${e.type||"activity"}\n ${Math.round(e.timestamp||0)}s\n
\n

${e.message||""}

\n
\n `).join(""):e.innerHTML='

No recent activity.

')},render(){this.updateDangerAlert(),this.updateRequestAlert(),this.syncLayoutState(),this.renderContracts(),this.renderRoster(),this.renderRequests(),this.renderActivity(),this.setActiveTab(this.activeTab)}},window.cadTasks.init(); \ No newline at end of file diff --git a/arma/client/addons/cad/ui/src/sidepanel.js b/arma/client/addons/cad/ui/src/sidepanel.js index d9febcf..a247a3b 100644 --- a/arma/client/addons/cad/ui/src/sidepanel.js +++ b/arma/client/addons/cad/ui/src/sidepanel.js @@ -10,6 +10,7 @@ window.cadTasks = { selectedDispatchGroupId: "", selectedDispatchTaskId: "", selectedDispatchRequestId: "", + selectedRosterMemberUid: "", focusStatusTimer: null, requestModalType: "", statuses: [ @@ -431,6 +432,19 @@ window.cadTasks = { this.selectedDispatchGroupId = ""; } + if (this.selectedRosterMemberUid) { + const memberExists = this.groups.some((group) => + this.normalizeCollection(group.members).some( + (member) => + (member.uid || "") === this.selectedRosterMemberUid, + ), + ); + + if (!memberExists) { + this.selectedRosterMemberUid = ""; + } + } + if ( this.selectedDispatchTaskId && !this.contracts.some((task) => { @@ -746,8 +760,18 @@ window.cadTasks = { const requestActionLabel = this.isDispatchMode() ? "Close" : "Cancel"; + const requestID = request.requestId || ""; + const isSelected = + requestID === this.selectedDispatchRequestId; return ` -
+
${request.title || this.getRequestTypeLabel(request.type || "")} ${(request.priority || "priority").replaceAll("_", " ")} @@ -760,7 +784,7 @@ window.cadTasks = { ${ canClose ? `
- +
` : "" } @@ -875,6 +899,7 @@ window.cadTasks = { this.selectedDispatchGroupId = groupID; this.selectedDispatchTaskId = ""; this.selectedDispatchRequestId = ""; + this.selectedRosterMemberUid = ""; const statusMessage = `Centering map on ${group.callsign || group.groupId || "group"}...`; this.setStatus(statusMessage, "info"); this.clearFocusStatusSoon(statusMessage); @@ -883,6 +908,51 @@ window.cadTasks = { }); this.render(); }, + focusMember(uid) { + let selectedMember = null; + + this.groups.some((group) => + this.normalizeCollection(group.members).some((member) => { + if ((member.uid || "") !== uid) { + return false; + } + + selectedMember = member; + return true; + }), + ); + + if (!selectedMember) { + this.setStatus( + "Selected group member is no longer available.", + "error", + ); + return; + } + + const position = Array.isArray(selectedMember.position) + ? selectedMember.position + : []; + if (position.length < 2) { + this.setStatus( + "Selected group member has no map position.", + "error", + ); + return; + } + + this.selectedRosterMemberUid = uid; + this.selectedDispatchGroupId = ""; + this.selectedDispatchTaskId = ""; + this.selectedDispatchRequestId = ""; + const statusMessage = `Centering map on ${selectedMember.name || "group member"}...`; + this.setStatus(statusMessage, "info"); + this.clearFocusStatusSoon(statusMessage); + window.mapUI.sendEvent("cad::members::focus", { + uid: uid, + }); + this.render(); + }, focusTask(taskID) { const task = this.contracts.find((entry) => { const entryTaskID = entry.taskId || entry.taskID || ""; @@ -899,6 +969,7 @@ window.cadTasks = { this.selectedDispatchTaskId = taskID; this.selectedDispatchGroupId = ""; this.selectedDispatchRequestId = ""; + this.selectedRosterMemberUid = ""; const statusMessage = `Centering map on ${task.title || taskID}...`; this.setStatus(statusMessage, "info"); this.clearFocusStatusSoon(statusMessage); @@ -927,6 +998,7 @@ window.cadTasks = { this.selectedDispatchRequestId = requestID; this.selectedDispatchGroupId = ""; this.selectedDispatchTaskId = ""; + this.selectedRosterMemberUid = ""; const statusMessage = `Centering map on ${request.title || requestID}...`; this.setStatus(statusMessage, "info"); this.clearFocusStatusSoon(statusMessage); @@ -1067,9 +1139,17 @@ window.cadTasks = { ); const isAssignedToLeader = this.isLeader() && assignedGroupId === currentGroupId; + const isSelected = taskId === this.selectedDispatchTaskId; return ` -
+
${task.title || taskId} ${this.formatTypeLabel(task)} @@ -1082,8 +1162,8 @@ window.cadTasks = { ${ isAssignedToLeader && assignmentState === "assigned" ? `
- - + +
` : "" } @@ -1177,9 +1257,19 @@ window.cadTasks = { const leaderBadge = member.isLeader ? 'Leader' : ""; + const memberUid = member.uid || ""; + const isSelected = + memberUid && memberUid === this.selectedRosterMemberUid; return ` -
+
${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. + +![Custom interaction menu](images/player/interaction_menu.jpg) + +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. + +![CAD operations task board](images/player/cad_ops_board.jpg) + +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: + +![CAD dispatch board](images/player/cad_dispatch_board.jpg) + +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. + +![Phone home screen](images/player/phone_home.jpg) + +### 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. + +![Phone contacts screen](images/player/phone_contacts.jpg) + +### Messages + +Messages are short player-to-player conversations. + +![Phone messages screen](images/player/phone_messages.jpg) + +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. + +![Phone email screen](images/player/phone_email.jpg) + +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: + +![Bank app](images/player/bank_app.jpg) + +- view account information +- transfer funds +- deposit earnings +- change PIN + +Use an ATM for limited account access: + +![ATM PIN screen](images/player/atm_app_pin.jpg) + +![ATM home screen](images/player/atm_app_home.jpg) + +- 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. + +![Organization home screen](images/player/org_home.jpg) + +![Organization registration screen](images/player/org_registration.jpg) + +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: + +![Organization dashboard](images/player/org_dashboard.jpg) + +![Organization treasury screen](images/player/org_treasury.jpg) + +- 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 catalog](images/player/store_catalog.jpg) + +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. + +![Store checkout result](images/player/store_checkout.jpg) + +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 storage](images/player/locker.jpg) + +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. + +![Virtual arsenal unlocks](images/player/virtual_arsenal.jpg) + +## Garage and Virtual Garage + +The garage stores physical player vehicles that have been saved from the world. + +![Garage dashboard](images/player/garage.jpg) + +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. + +![Virtual garage unlocks](images/player/virtual_garage.jpg) + +## Economy Services + +Economy services are server-controlled. Charges must succeed before the world +effect is applied. + +![Garage service controls](images/player/garage.jpg) + +### Medical + +Medical services are player-funded first. + +![Medical respawn screen](images/player/medical_respawn.jpg) + +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. + +![Custom interaction menu](images/player/interaction_menu.jpg) + +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. + +![CAD operations task board](images/player/cad_ops_board.jpg) + +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: + +![CAD dispatch board](images/player/cad_dispatch_board.jpg) + +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. + +![Phone home screen](images/player/phone_home.jpg) + +### 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. + +![Phone contacts screen](images/player/phone_contacts.jpg) + +### Messages + +Messages are short player-to-player conversations. + +![Phone messages screen](images/player/phone_messages.jpg) + +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. + +![Phone email screen](images/player/phone_email.jpg) + +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: + +![Bank app](images/player/bank_app.jpg) + +- view account information +- transfer funds +- deposit earnings +- change PIN + +Use an ATM for limited account access: + +![ATM PIN screen](images/player/atm_app_pin.jpg) + +![ATM home screen](images/player/atm_app_home.jpg) + +- 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. + +![Organization home screen](images/player/org_home.jpg) + +![Organization registration screen](images/player/org_registration.jpg) + +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: + +![Organization dashboard](images/player/org_dashboard.jpg) + +![Organization treasury screen](images/player/org_treasury.jpg) + +- 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 catalog](images/player/store_catalog.jpg) + +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. + +![Store checkout result](images/player/store_checkout.jpg) + +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 storage](images/player/locker.jpg) + +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. + +![Virtual arsenal unlocks](images/player/virtual_arsenal.jpg) + +## Garage and Virtual Garage + +The garage stores physical player vehicles that have been saved from the world. + +![Garage dashboard](images/player/garage.jpg) + +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. + +![Virtual garage unlocks](images/player/virtual_garage.jpg) + +## Economy Services + +Economy services are server-controlled. Charges must succeed before the world +effect is applied. + +![Garage service controls](images/player/garage.jpg) + +### Medical + +Medical services are player-funded first. + +![Medical respawn screen](images/player/medical_respawn.jpg) + +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