# :necktie: Mannequin
Documentation relating to the **jo_clothingstore_mannequin** add-on for [Clothing store script](clothing-store).

:::: tabs
::: tab BUY
[Buy the add-on](https://jumpon-studios.com/redm/mannequin)
:::

::: tab PREVIEW
<iframe width="560" height="315" src="https://streamable.com/e/agu2uh" title="Streamable video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
:::
::::

## 1. Installation
::: warning
[Clothing store script](clothing-store) is required to use this add-on.
:::

To install jo_clothingstore_mannequin:
- Drag and drop the resource into your resources folder: `jo_clothingstore_mannequin`
- Add this ensure in your server.cfg after `ensure jo_clothingstore`:
  - `ensure jo_clothingstore_mannequin`

Congratulations, the **Mannequin** add-on is ready to be used!

## 2. Usage

The Mannequin add-on lets players sell outfits through persistent mannequins placed in the world. It is an add-on of `jo_clothingstore`: before a seller can sell an outfit from a mannequin, they must first create and save at least one male or female outfit from the clothing store or wardrobe outfit menu. Mannequins can also be saved without an outfit.

The [Premade Outfits add-on](clothing-store-outfits) can help sellers create outfits faster by buying ready-made outfits and saving them into their clothing-store outfit list before using them on mannequins.

:::: tabs
::: tab 🧵 Seller setup
1. Open the clothing store or a wardrobe.
2. Go to **Outfits** and create a saved outfit.
3. Choose the outfit sex before creating it. Mannequins can display `male` or `female` outfits, and the mannequin sex must match the selected outfit.
4. To prepare stock faster, use [Premade Outfits](clothing-store-outfits) to purchase ready-made outfits.
5. Save the outfit in your clothing-store outfit list.
:::

::: tab 🧍 Create a mannequin
1. Use `/mannequinMenu` to open the mannequin creation menu.
2. Select the mannequin sex: **Male** or **Female**.
3. Select one of your saved outfits, or select **None** to create a mannequin without an outfit. The list only shows outfits compatible with the selected mannequin sex.
4. Set the mannequin name. By default, selecting an outfit also fills the mannequin name with the outfit name.
5. If an outfit is selected, set the sale price. This opens an in-game NUI price modal where you can define a simple price, a combo of several costs paid together, or several payment options for the buyer to choose from.
   If you add several payment options, use the **Switch price** prompt to preview them before saving the mannequin.
6. Select a pose.
7. Choose **Move mannequin**, place and rotate it with the gizmo, then confirm the placement.
8. Choose **Save mannequin** to persist it.

The mannequin is saved, synchronized to every player, and will respawn automatically when players are close enough.
:::

::: tab 💰 Manage sales
When the owner approaches one of their mannequins, owner prompts are displayed:
- **Edit mannequin** - Reopen the mannequin menu to change its sex, outfit, name, price, pose, or position.
- **Delete mannequin** - Remove the mannequin from the world and from persistence. If the mannequin has sales to collect, they must be collected before deleting it.
- **Collect sales** - Recover money, gold, ROL, or items stored from previous purchases when the mannequin has sales to collect.

Editing a mannequin keeps the existing world mannequin while the menu is open. If you cancel the edit, unsaved changes are reverted.
:::

::: tab 🛍️ Buyer interactions
When another player approaches a mannequin with an outfit, buyer prompts are displayed:
- **Switch price** - Cycle through the available payment options when the mannequin has more than one price.
- **Try outfit** - Temporarily apply the displayed outfit to the player. The preview is removed when the player moves away from the mannequin.
- **Buy outfit** - Purchase the outfit with the selected payment option.

Mannequins without outfits do not show try or buy prompts. Trying an outfit only works when the mannequin sex matches the buyer's current character sex. Buying can still save an outfit of either sex in the buyer's clothing-store outfit list. If the main clothing-store outfit-item mode is enabled, the purchase gives an outfit item instead.
:::
::::

## 3. Configuration
The configuration files are located in the `config` folder within the resource. This folder contains two subfolders:

- **`_default.lock/`** - Contains the default configuration files. **Do not edit these files directly** as they may be overwritten during updates.
- **`custom/`** - This is where you place your custom configuration files to override the defaults.

### How to customize the configuration

1. Navigate to `jo_clothingstore_mannequin/config/_default.lock/`
2. Copy the file you want to modify (e.g., `prices.lua`)
3. Paste it into `jo_clothingstore_mannequin/config/custom/`
4. Edit the copied file in the `custom` folder

The script automatically loads files from `custom/` first, and only uses `_default.lock/` as a fallback. This system ensures your customizations are preserved when updating the script.

### Default configuration files

`jo_clothingstore_mannequin/config/_default.lock/commands.lua`

```lua
Config.commands = {
  openMannequinMenu = "mannequinMenu"
}
```

`jo_clothingstore_mannequin/config/_default.lock/global.lua`

```lua
-- Purchase behavior depends on the outfit-in-item mode from the main jo_clothingstore resource.
--
-- Main jo_clothingstore outfit-in-item mode enabled:
--   The buyer always receives the configured outfit item with outfit, name, and sexe metadata.
--   saveOutfitWhenGivenAsItem controls whether the outfit is also saved in jo_clothingstore_outfits.
--   equipOutfitAfterPurchase is ignored because item-mode purchases are never equipped immediately.
--
-- Main jo_clothingstore outfit-in-item mode disabled:
--   The outfit is always saved in jo_clothingstore_outfits and no inventory item is given.
--   equipOutfitAfterPurchase controls immediate equipment when the outfit sex matches the buyer.
--   saveOutfitWhenGivenAsItem is ignored because the outfit is already saved by the non-item flow.

Config.saveOutfitWhenGivenAsItem = false
Config.equipOutfitAfterPurchase = true

Config.removeOutfitFromMannequinOnPurchase = false -- Remove the displayed outfit from the mannequin after each purchase.
Config.sendSellerNotificationOnSale = true         -- Notify the seller when an outfit is sold while they are online.

Config.maxMannequinsPerPlayer = 10                 -- Maximum number of mannequins each tailor character can own.

Config.mannequinsSpawnDistance = 100.0             -- Distance at which mannequin entities spawn/despawn around players.
Config.mannequinPromptsDistance = 2.0              -- Distance at which mannequin interaction prompts are displayed.
```

`jo_clothingstore_mannequin/config/_default.lock/keys.lua`

```lua
Config.keys = {
    enter = "ENTER",
    tryOutfit = "E",
    editMannequin = "E",
    deleteMannequin = "X",
    collectSales = "R",
    switchPrice = "B",
}
```

`jo_clothingstore_mannequin/config/_default.lock/lang.lua`

```lua
-------------------------------------------
-- DON'T EDIT THIS FILE
-- TO TRANSLATE THE SCRIPT, USE overwriteLang.lua file instead
-------------------------------------------

function __(name) return Lang[name] or ("#" .. name) end

-- * ======================================
-- *  GENERAL
-- * ======================================

Lang.createMannequinMenuTitle = "New Mannequin"
Lang.editMannequinMenuTitle = "Edit Mannequin"
Lang.createMannequinMenuDesc = "Create a new mannequin"
Lang.editMannequinMenuDesc = "Edit an existing mannequin"
Lang.selectOutfitItemTitle = "Outfit"
Lang.none = "None"
Lang.noneSelected = "None selected"
Lang.noOutfits = "No outfits"
Lang.selectOutfitItemDesc = "Select the outfit to apply"
Lang.setlectOutfit = "Select this outfit"
Lang.setMannequinNameTitle = "Name"
Lang.setMannequinNameDesc = "Give a name to this mannequin"
Lang.setMannequinPriceTitle = "Price"
Lang.setMannequinPriceDesc = "Give a price to this mannequin"
Lang.setMannequinPriceModalTitle = "Set mannequin price"
Lang.setMannequinPriceModalDesc = "Choose how much buyers must pay to purchase this mannequin outfit"
Lang.setMannequinPriceConfirm = "Confirm"
Lang.setMannequinPriceCancel = "Cancel"
Lang.setMannequinSexeTitle = "Sex"
Lang.setMannequinSexeDesc = "Set the mannequin sex"
Lang.mannequinMale = "Male"
Lang.mannequinFemale = "Female"
Lang.setMannequinPoseTitle = "Pose"
Lang.setMannequinPoseDesc = "Change the pose of this mannequin"
Lang.placeMannequin = "Move mannequin"
Lang.saveMannequin = "Save mannequin"
Lang.saveChanges = "Save changes"
Lang.outfitItemDesc = "Sexe: %s - Name: %s"

-- * ======================================
-- *  PROMPTS
-- * ======================================
Lang.chooseOutfitPrompt = "Select outfit"
Lang.setPrice = "Set price"
Lang.setNamePrompt = "Set name"
Lang.setPosePrompt = "Change pose"
Lang.positionMannequinPrompt = "Position mannequin"
Lang.mannequinPromptGroupName = "Mannequin - %s"
Lang.tryOutfit = "Try outfit"
Lang.buyOutfit = "Buy for "
Lang.editMannequin = "Edit mannequin"
Lang.deleteMannequin = "Delete mannequin"
Lang.collectSales = "Collect sales"
Lang.switchPrice = "Switch price"

-- * ======================================
-- *  NOTIFICATIONS
-- * ======================================
Lang.wrongNameFormat = "Wrong name format"
Lang.wrongPriceFormat = "Wrong price format"
Lang.mannequinPlaced = "Mannequin placed"
Lang.mannequinUpdated = "Mannequin updated"
Lang.mannequinMissingName = "Please set a name for the mannequin"
Lang.mannequinMissingOutfit = "Please select an outfit for the mannequin"
Lang.mannequinMissingPrice = "Please set a price for the mannequin"
Lang.mannequinDeleted = "Mannequin deleted"
Lang.salesCollected = "Sales collected"
Lang.noSalesToCollect = "No sales to collect"
Lang.collectSalesBeforeDelete = "Collect sales before deleting this mannequin"
Lang.cannotPaywith = "You don't have enough to buy this outfit"
Lang.outfitBoughtItem = "Outfit purchased! Check your inventory"
Lang.outfitBoughtEquipped = "Outfit purchased and equipped"
Lang.outfitPreviewStarted = "Move away from the mannequin to remove the outfit preview"
Lang.noOutfitToBuy = "No outfit to buy on this mannequin"
Lang.maxMannequinsReached = "You have reached the maximum number of mannequins"
Lang.notAllowed = "You are not allowed to do this"
Lang.invalidPaymentMethod = "Invalid payment method"
Lang.invalidPrice = "Invalid price"
Lang.wrongSexe = "This outfit is not for your character"
Lang.invalidMannequinSexe = "Invalid mannequin sex"
Lang.outfitBoughtSaved = "Outfit purchased and saved"
Lang.outfitSold = "Your outfit %s has been sold"
```

`jo_clothingstore_mannequin/config/_default.lock/mannequinPoses.lua`

```lua
Config.mannequinPoses = {
    -- Keep this key "default" unchanged: existing mannequins and fallback pose logic use "default". You can edit the values inside, but not the key.
    default =
    {
        label = "Pose 1",                                        -- Displayed in the mannequin pose selector.
        dict = "amb_wander@code_human_broom_wander@male_a@base", -- Animation dictionary to load.
        anim = "base"                                            -- Animation name inside the dictionary.
    },
    pose_2 =
    {
        label = "Pose 2",
        dict = "amb_rest_lean@world_human_lean@post@left@female_a@base",
        anim = "base"
    },
    pose_3 =
    {
        label = "Pose 3",
        dict = "amb_misc@world_human_stare_stoic@female_a@idle_a",
        anim = "idle_b"
    },
    pose_4 =
    {
        label = "Pose 4",
        dict = "amb_camp@world_camp_lenny_guard@crouch_tracks@male_a@base",
        anim = "base"
    },
    pose_5 =
    {
        label = "Pose 5",
        dict = "amb_rest_lean@world_human_lean@post@right@female_a@base",
        anim = "base"
    }
}
```

`jo_clothingstore_mannequin/config/_default.lock/prices.lua`

```lua
-- These options control the in-game NUI price modal used when a seller sets a mannequin price.
-- Money is always available. Gold, ROL, and item payments can be enabled or disabled below.
-- Server-side validation also uses these values, so disabled payment types cannot be saved or used.

Config.allowPayingInGold = true  -- Allow sellers to add gold costs to mannequin outfit prices.
Config.allowPayingInRol = false  -- Allow sellers to add ROL costs to mannequin outfit prices.
Config.allowPayingInItems = true -- Allow sellers to add item costs to mannequin outfit prices.

-- If true, sellers can create several payment options for the same mannequin outfit.
-- Example: buyer pays either "$10 + 1 water" OR "2 gold".
-- If false, sellers can only create one payment option, but that option can still contain several costs paid together.
Config.allowPriceOR = true
```

## 4. For developers

### Events

### `[Client]` Open Mannequin Creator
Trigger to open the mannequin creator menu for a specific player.

```lua
TriggerClientEvent("jo_clothingstore_mannequin:openMannequinMenu", playerId)
```

### Actions

Actions are one of the two types of Hooks. They provide a way to run a function at a specific point in the execution of scripts. Callback functions for an Action do not return anything back to the calling Action hook. They are the counterpart to Filters.

Below is a complete list of all available actions in the `jo_clothingstore_mannequin` script.

#### `[Shared]` init
Triggered when the add-on is initialized after `jo_clothingstore` starts.

```lua
exports.jo_clothingstore_mannequin:registerAction("init", function()
    -- Your code here
end)
```

#### `[Server]` mannequinCreated
Triggered after a mannequin is created and synchronized to clients.

```lua
-- @param source - integer server ID of the creator
-- @param mannequin - Mannequin created mannequin instance
exports.jo_clothingstore_mannequin:registerAction("mannequinCreated", function(source, mannequin)
    -- Your code here
end)
```

#### `[Server]` mannequinDeleted
Triggered after a mannequin is deleted and removed from clients.

```lua
-- @param source - integer server ID of the owner
-- @param mannequinId - integer deleted mannequin ID
exports.jo_clothingstore_mannequin:registerAction("mannequinDeleted", function(source, mannequinId)
    -- Your code here
end)
```

#### `[Server]` mannequinUpdated
Triggered after a mannequin is updated and synchronized to clients.

```lua
-- @param source - integer server ID of the editor
-- @param mannequin - Mannequin updated mannequin instance
exports.jo_clothingstore_mannequin:registerAction("mannequinUpdated", function(source, mannequin)
    -- Your code here
end)
```

#### `[Server]` outfitBought
Triggered after an outfit is purchased from a mannequin.

```lua
-- @param source - integer server ID of the buyer
-- @param mannequin - Mannequin purchased mannequin instance
-- @param outfit - table outfit data
-- @param fullPrice - PriceGroupClass full mannequin price group
exports.jo_clothingstore_mannequin:registerAction("outfitBought", function(source, mannequin, outfit, fullPrice)
    -- Your code here
end)
```

#### `[Server]` outfitItemGiven
Triggered after a purchase gives the buyer an outfit item.

```lua
-- @param source - integer server ID of the buyer
-- @param mannequin - Mannequin purchased mannequin instance
-- @param itemName - string outfit item name
-- @param metadata - table item metadata containing outfit, name, sexe, and description
exports.jo_clothingstore_mannequin:registerAction("outfitItemGiven", function(source, mannequin, itemName, metadata)
    -- Your code here
end)
```

#### `[Server]` salesCollected
Triggered after the owner collects stored mannequin sales.

```lua
-- @param source - integer server ID of the owner
-- @param mannequin - Mannequin mannequin instance after stored sales are cleared
-- @param priceToRefund - PriceClass price refunded to the owner
exports.jo_clothingstore_mannequin:registerAction("salesCollected", function(source, mannequin, priceToRefund)
    -- Your code here
end)
```

#### `[Server]` sellerRevenueStored
Triggered after a purchase stores revenue on the seller's mannequin.

```lua
-- @param source - integer server ID of the buyer
-- @param mannequin - Mannequin purchased mannequin instance
-- @param revenuePrice - PriceClass price stored for the seller
-- @param price - PriceClass price paid by the buyer
-- @param priceIndex - integer selected price index
exports.jo_clothingstore_mannequin:registerAction("sellerRevenueStored", function(source, mannequin, revenuePrice, price, priceIndex)
    -- Your code here
end)
```

### Filters

[Filters](/DeveloperResources/filters) allow you to modify data or permissions synchronously at specific points in the script. Below is the complete list of `jo_clothingstore_mannequin` filters and how to use them.

#### `[Server]` canBuyOutfit
Control whether a player can buy an outfit from a mannequin. This filter is called after the selected price has been resolved and before the player is charged.

```lua
-- @param canBuy - boolean (default true)
-- @param source - integer server ID of the buyer
-- @param mannequin - Mannequin purchased mannequin instance
-- @param price - PriceClass selected price
-- @param priceIndex - integer selected price index
exports.jo_clothingstore_mannequin:registerFilter("canBuyOutfit", function(canBuy, source, mannequin, price, priceIndex)
    return canBuy
end)
```

#### `[Server]` canCollectSales
Control whether the mannequin owner can collect stored sales.

```lua
-- @param canCollect - boolean (default true)
-- @param source - integer server ID of the owner
-- @param mannequin - Mannequin mannequin instance
exports.jo_clothingstore_mannequin:registerFilter("canCollectSales", function(canCollect, source, mannequin)
    return canCollect
end)
```

#### `[Server]` canCreateMannequin
Control whether a player can create a mannequin. This filter is called after the mannequin owner, sex, limit, and price have been validated.

```lua
-- @param canCreate - boolean (default true)
-- @param source - integer server ID of the creator
-- @param mannequin - Mannequin mannequin instance being created
exports.jo_clothingstore_mannequin:registerFilter("canCreateMannequin", function(canCreate, source, mannequin)
    return canCreate
end)
```

#### `[Server]` canDeleteMannequin
Control whether a player can delete a mannequin.

```lua
-- @param canDelete - boolean (default true)
-- @param source - integer server ID of the owner
-- @param mannequin - Mannequin mannequin instance being deleted
exports.jo_clothingstore_mannequin:registerFilter("canDeleteMannequin", function(canDelete, source, mannequin)
    return canDelete
end)
```

#### `[Server]` canUpdateMannequin
Control whether a player can update a mannequin. This filter is called after ownership, sex, and price validation.

```lua
-- @param canUpdate - boolean (default true)
-- @param source - integer server ID of the editor
-- @param existing - Mannequin current persisted mannequin instance
-- @param incoming - table incoming mannequin data
exports.jo_clothingstore_mannequin:registerFilter("canUpdateMannequin", function(canUpdate, source, existing, incoming)
    return canUpdate
end)
```

#### `[Server]` canOpenMannequinCreator
Control whether a player can open the mannequin creator menu.

```lua
-- @param canOpen - boolean (default true)
-- @param source - integer server ID of the player
exports.jo_clothingstore_mannequin:registerFilter("canOpenMannequinCreator", function(canOpen, source)
    return canOpen
end)
```

#### `[Server]` overwriteBuyerPrice
Override the selected price before the buyer is charged. The second argument is the order type and is `"mannequinOutfit"` for Mannequin purchases.

```lua
-- @param price - PriceClass selected price
-- @param typeOrder - string "mannequinOutfit"
-- @param source - integer server ID of the buyer
-- @param mannequin - Mannequin purchased mannequin instance
-- @param priceIndex - integer selected price index
exports.jo_clothingstore_mannequin:registerFilter("overwriteBuyerPrice", function(price, typeOrder, source, mannequin, priceIndex)
    return price
end)
```

#### `[Server]` overwriteSellerRevenue
Override the amount stored for the mannequin owner after a sale. By default, the stored revenue matches the buyer price, except item costs marked with `keep = true`.

::: warning
The `price` argument is the buyer price after `overwriteBuyerPrice` has already been applied. If you use `overwriteBuyerPrice` to change what the buyer pays, `overwriteSellerRevenue` receives that updated buyer price.
:::

Return `nil` to prevent storing seller revenue for this purchase.

```lua
-- @param revenuePrice - PriceClass|nil default seller revenue
-- @param source - integer server ID of the buyer
-- @param mannequin - Mannequin purchased mannequin instance
-- @param price - PriceClass price paid by the buyer
-- @param priceIndex - integer selected price index
exports.jo_clothingstore_mannequin:registerFilter("overwriteSellerRevenue", function(revenuePrice, source, mannequin, price, priceIndex)
    return revenuePrice
end)
```

## 5. Snippets

### Mannequin in item

Here is a snippet to manage the mannequin creation as an item:

1. Create the `mannequin` item in your inventory script.
2. Create a `mannequin_items.lua` file inside the `config/custom` folder.
3. Add the following code to the file:

```lua
--Define the mannequin item
local mannequinItem = "mannequin"

--Turn off the command to open the mannequin menu
Config.commands.openMannequinMenu = false

--Create all the server side logic
if jo.isServerSide() then
  --load the framework module
  jo.require("framework")

  --Register the mannequin item has usable
  jo.framework:registerUseItem(mannequinItem, true, function(source)
    TriggerClientEvent("jo_clothingstore_mannequin:openMannequinMenu", source)
  end)

  --Register the filter to check if the player has the mannequin item in their inventory
  jo.hook.registerFilter("canCreateMannequin", function(canCreate, source)
    if jo.framework:getItemCount(source, mannequinItem) <= 0 then
      jo.notif.rightError(source, "You don't have a mannequin in your inventory!")
      return false
    end
    return canCreate
  end)

  --Register the action to remove the mannequin item when a mannequin is created
  jo.hook.registerAction("mannequinCreated", function(source)
    jo.framework:removeItem(source, mannequinItem, 1)
  end)

  --Register the action to add the mannequin item when a mannequin is deleted
  jo.hook.registerAction("mannequinDeleted", function(source)
    jo.framework:giveItem(source, mannequinItem, 1)
  end)
end
```

4. Restart your server.
5. Done, you can now use the mannequin item to create mannequins.
6. When you delete a mannequin, the item will be added back to your inventory.
