Add player guide and roster member map focus

- add player-facing docs and synced screenshots
- let CAD roster entries center the map on a member
- refresh garage and economy UI bridges and docs
This commit is contained in:
Jacob Schmidt 2026-05-19 18:55:51 -05:00
parent 65b828eeda
commit 9d80e32918
49 changed files with 984 additions and 32 deletions

View File

@ -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 {

View File

@ -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", "", [""]]];

File diff suppressed because one or more lines are too long

View File

@ -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 `
<div class="task-card cad-request-card">
<div
class="task-card cad-request-card dispatch-map-card ${isSelected ? "is-selected" : ""}"
data-request-id="${requestID}"
role="button"
tabindex="0"
onclick="window.cadTasks.focusRequest('${requestID}')"
onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); window.cadTasks.focusRequest('${requestID}'); }"
>
<div class="task-card-header">
<strong>${request.title || this.getRequestTypeLabel(request.type || "")}</strong>
<span class="task-type">${(request.priority || "priority").replaceAll("_", " ")}</span>
@ -760,7 +784,7 @@ window.cadTasks = {
${
canClose
? `<div class="task-action-row">
<button type="button" class="task-secondary-btn" onclick="window.cadTasks.closeSupportRequest('${request.requestId || ""}')">${requestActionLabel}</button>
<button type="button" class="task-secondary-btn" onclick="event.stopPropagation(); window.cadTasks.closeSupportRequest('${requestID}')">${requestActionLabel}</button>
</div>`
: ""
}
@ -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 `
<div class="task-card" data-task-id="${taskId}">
<div
class="task-card dispatch-map-card ${isSelected ? "is-selected" : ""}"
data-task-id="${taskId}"
role="button"
tabindex="0"
onclick="window.cadTasks.focusTask('${taskId}')"
onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); window.cadTasks.focusTask('${taskId}'); }"
>
<div class="task-card-header">
<strong>${task.title || taskId}</strong>
<span class="task-type">${this.formatTypeLabel(task)}</span>
@ -1082,8 +1162,8 @@ window.cadTasks = {
${
isAssignedToLeader && assignmentState === "assigned"
? `<div class="task-action-row">
<button type="button" class="task-accept-btn" onclick="window.cadTasks.acknowledgeTask('${taskId}')">Acknowledge</button>
<button type="button" class="task-secondary-btn" onclick="window.cadTasks.declineTask('${taskId}')">Decline</button>
<button type="button" class="task-accept-btn" onclick="event.stopPropagation(); window.cadTasks.acknowledgeTask('${taskId}')">Acknowledge</button>
<button type="button" class="task-secondary-btn" onclick="event.stopPropagation(); window.cadTasks.declineTask('${taskId}')">Decline</button>
</div>`
: ""
}
@ -1177,9 +1257,19 @@ window.cadTasks = {
const leaderBadge = member.isLeader
? '<span class="roster-leader-badge">Leader</span>'
: "";
const memberUid = member.uid || "";
const isSelected =
memberUid && memberUid === this.selectedRosterMemberUid;
return `
<div class="task-card roster-member-card" data-member-id="${member.uid || ""}">
<div
class="task-card roster-member-card dispatch-map-group-card ${isSelected ? "is-selected" : ""}"
data-member-id="${memberUid}"
role="button"
tabindex="0"
onclick="window.cadTasks.focusMember('${memberUid}')"
onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); window.cadTasks.focusMember('${memberUid}'); }"
>
<div class="task-card-header">
<strong>${member.name || "Unknown Operator"}</strong>
<span class="task-type">${lifeState}</span>

View File

@ -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", []];

View File

@ -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]]];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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,

View File

@ -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(

View File

@ -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,

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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]];

View File

@ -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", {}]
]];

View File

@ -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. |

View File

@ -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

View File

@ -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.

321
docs/PLAYER_GUIDE.md Normal file
View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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. |

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 KiB

View File

@ -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