{
  "name": "economyUtils",
  "author": "Takaro",
  "supportedGames": [
    "all"
  ],
  "takaroVersion": ">=0.0.1",
  "versions": [
    {
      "tag": "0.0.3",
      "description": "A set of commands to allow players to manage their currency.",
      "configSchema": "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"pendingAmount\":{\"title\":\"Pending amount\",\"type\":\"number\",\"description\":\"When a player transfers money, they must confirm the transfer when the amount is equal or above this value. Set to 0 to disable.\",\"default\":0},\"zombieKillReward\":{\"title\":\"Zombie kill reward\",\"type\":\"number\",\"description\":\"The default amount of currency a player receives for killing a zombie. This can be overridden by roles.\",\"default\":1}},\"required\":[],\"additionalProperties\":false}",
      "uiSchema": "{}",
      "commands": [
        {
          "name": "balance",
          "trigger": "balance",
          "helpText": "Check your balance.",
          "function": "import { takaro, data } from '@takaro/helpers';\nasync function main() {\n    const currencyName = (await takaro.settings.settingsControllerGetOne('currencyName', data.gameServerId)).data.data;\n    await data.player.pm(`balance: ${data.pog.currency} ${currencyName.value}`);\n}\nawait main();\n//# sourceMappingURL=balance.js.map",
          "arguments": []
        },
        {
          "name": "topCurrency",
          "trigger": "topcurrency",
          "helpText": "List of the 10 players with the highest balance.",
          "function": "import { takaro, data } from '@takaro/helpers';\nasync function main() {\n    const richest = (await takaro.playerOnGameserver.playerOnGameServerControllerSearch({\n        limit: 10,\n        sortBy: 'currency',\n        sortDirection: 'desc',\n        extend: ['player'],\n    })).data.data;\n    const currencyName = (await takaro.settings.settingsControllerGetOne('currencyName', data.gameServerId)).data.data\n        .value;\n    // TODO: change this to name when it become available in playerOnGameServer\n    const richestStrings = richest.map(async (pog, index) => {\n        const playerName = (await takaro.player.playerControllerGetOne(pog.playerId)).data.data.name;\n        return `${index + 1}. ${playerName} - ${pog.currency} ${currencyName}`;\n    });\n    await data.player.pm('Richest players:');\n    for (const string of richestStrings) {\n        await data.player.pm(await string);\n    }\n}\nawait main();\n//# sourceMappingURL=topCurrency.js.map",
          "arguments": []
        },
        {
          "name": "grantCurrency",
          "trigger": "grantcurrency",
          "helpText": "Grant money to a player. The money is not taken from your own balance but is new currency.",
          "function": "import { takaro, data } from '@takaro/helpers';\nasync function main() {\n    const { pog: granter, arguments: args, gameServerId } = data;\n    // args.receiver has an argument type of \"player\". Arguments of this type are automatically resolved to the player's id.\n    // If the player doesn't exist or multiple players with the same name where found, it will have thrown an error before this command is executed.\n    const receiver = args.receiver;\n    const currencyName = (await takaro.settings.settingsControllerGetOne('currencyName', gameServerId)).data.data.value;\n    const granterName = (await takaro.player.playerControllerGetOne(granter.playerId)).data.data.name;\n    const receiverName = (await takaro.player.playerControllerGetOne(receiver.playerId)).data.data.name;\n    await takaro.playerOnGameserver.playerOnGameServerControllerAddCurrency(receiver.gameServerId, receiver.playerId, {\n        currency: args.amount,\n        reason: 'Admin grant',\n    });\n    const messageToReceiver = takaro.gameserver.gameServerControllerSendMessage(gameServerId, {\n        message: `Granted ${args.amount} ${currencyName} by ${granterName}`,\n        opts: {\n            recipient: {\n                gameId: receiver.gameId,\n            },\n        },\n    });\n    await Promise.all([\n        granter.pm(`You successfully granted ${args.amount} ${currencyName} to ${receiverName}`),\n        messageToReceiver,\n    ]);\n    return;\n}\nawait main();\n//# sourceMappingURL=grantCurrency.js.map",
          "arguments": [
            {
              "name": "receiver",
              "type": "player",
              "helpText": "The player to grant currency to.",
              "position": 0
            },
            {
              "name": "amount",
              "type": "number",
              "helpText": "The amount of money.",
              "position": 1
            }
          ]
        },
        {
          "name": "revokeCurrency",
          "trigger": "revokecurrency",
          "helpText": "Revokes money from a player. The money disappears.",
          "function": "import { takaro, data } from '@takaro/helpers';\nasync function main() {\n    const { pog: revoker, arguments: args, gameServerId } = data;\n    // args.receiver has an argument type of \"player\". Arguments of this type are automatically resolved to the player's id.\n    // If the player doesn't exist or multiple players with the same name where found, it will have thrown an error before this command is executed.\n    const receiver = args.receiver;\n    const currencyName = (await takaro.settings.settingsControllerGetOne('currencyName', gameServerId)).data.data.value;\n    const revokerName = (await takaro.player.playerControllerGetOne(revoker.playerId)).data.data.name;\n    const receiverName = (await takaro.player.playerControllerGetOne(receiver.playerId)).data.data.name;\n    await takaro.playerOnGameserver.playerOnGameServerControllerDeductCurrency(receiver.gameServerId, receiver.playerId, {\n        currency: args.amount,\n        reason: 'Admin revoke',\n    });\n    const messageToReceiver = takaro.gameserver.gameServerControllerSendMessage(gameServerId, {\n        message: `${args.amount} ${currencyName} were revoked by ${revokerName}`,\n        opts: {\n            recipient: {\n                gameId: receiver.gameId,\n            },\n        },\n    });\n    await Promise.all([\n        revoker.pm(`You successfully revoked ${args.amount} ${currencyName} of ${receiverName}'s balance`),\n        messageToReceiver,\n    ]);\n    return;\n}\nawait main();\n//# sourceMappingURL=revokeCurrency.js.map",
          "arguments": [
            {
              "name": "receiver",
              "type": "player",
              "helpText": "The player to revoke currency from.",
              "position": 0
            },
            {
              "name": "amount",
              "type": "number",
              "helpText": "The amount of money.",
              "position": 1
            }
          ]
        },
        {
          "name": "confirmTransfer",
          "trigger": "confirmtransfer",
          "helpText": "Confirms a pending transfer.",
          "function": "import { takaro, data, TakaroUserError } from '@takaro/helpers';\nasync function main() {\n    const { gameServerId, pog: sender, module: mod } = data;\n    // try to find a variable with key \"confirmTransfer\"\n    const variables = (await takaro.variable.variableControllerSearch({\n        filters: {\n            key: ['confirmTransfer'],\n            gameServerId: [gameServerId],\n            moduleId: [mod.moduleId],\n            playerId: [sender.playerId],\n        },\n    })).data.data;\n    if (variables.length === 0) {\n        throw new TakaroUserError('You have no pending transfer.');\n    }\n    // Remove the variable before potentially executing the transaction.\n    await takaro.variable.variableControllerDelete(variables[0].id);\n    const pendingTransfer = JSON.parse(variables[0].value);\n    await takaro.playerOnGameserver.playerOnGameServerControllerTransactBetweenPlayers(sender.gameServerId, sender.id, pendingTransfer.receiver.id, {\n        currency: pendingTransfer.amount,\n        reason: 'Player transfer',\n    });\n    const receiverName = (await takaro.player.playerControllerGetOne(pendingTransfer.receiver.playerId)).data.data.name;\n    const senderName = (await takaro.player.playerControllerGetOne(sender.playerId)).data.data.name;\n    const currencyName = (await takaro.settings.settingsControllerGetOne('currencyName', gameServerId)).data.data.value;\n    const messageToSender = sender.pm(`You successfully transferred ${pendingTransfer.amount} ${currencyName} to ${receiverName}`);\n    const messageToReceiver = takaro.gameserver.gameServerControllerSendMessage(gameServerId, {\n        message: `You received ${pendingTransfer.amount} ${currencyName} from ${senderName}`,\n        opts: {\n            recipient: {\n                gameId: pendingTransfer.receiver.gameId,\n            },\n        },\n    });\n    await Promise.all([messageToSender, messageToReceiver]);\n    return;\n}\nawait main();\n//# sourceMappingURL=confirmTransfer.js.map",
          "arguments": []
        },
        {
          "name": "transfer",
          "trigger": "transfer",
          "helpText": "Transfer money to another player.",
          "function": "import { takaro, data, TakaroUserError } from '@takaro/helpers';\nasync function main() {\n    const { pog: sender, arguments: args, gameServerId, module: mod } = data;\n    const currencyName = (await takaro.settings.settingsControllerGetOne('currencyName', gameServerId)).data.data.value;\n    const prefix = (await takaro.settings.settingsControllerGetOne('commandPrefix', gameServerId)).data.data.value;\n    // args.receiver has an argument type of \"player\". Arguments of this type are automatically resolved to the player's id.\n    // If the player doesn't exist or multiple players with the same name where found, it will have thrown an error before this command is executed.\n    const receiver = args.receiver;\n    const senderName = (await takaro.player.playerControllerGetOne(sender.playerId)).data.data.name;\n    const receiverName = (await takaro.player.playerControllerGetOne(receiver.playerId)).data.data.name;\n    if (mod.userConfig.pendingAmount !== 0 && args.amount >= mod.userConfig.pendingAmount) {\n        // create a variable to store confirmation requirement\n        // TODO: in the future, we should probably add an expiration date to this variable.\n        await takaro.variable.variableControllerCreate({\n            key: 'confirmTransfer',\n            value: JSON.stringify({\n                amount: args.amount,\n                receiver: {\n                    id: receiver.id,\n                    gameId: receiver.gameId,\n                    playerId: receiver.playerId,\n                },\n            }),\n            moduleId: mod.moduleId,\n            playerId: sender.playerId,\n            gameServerId,\n        });\n        // NOTE: we should maybe check if the player has enough balance to send the amount since this is only checked when the transaction is executed.\n        await sender.pm(`You are about to send ${args.amount} ${currencyName} to ${receiverName}. (Please confirm by typing ${prefix}confirmtransfer)`);\n        return;\n    }\n    try {\n        await takaro.playerOnGameserver.playerOnGameServerControllerTransactBetweenPlayers(sender.gameServerId, sender.id, receiver.id, {\n            currency: args.amount,\n            reason: 'Player transfer',\n        });\n    }\n    catch {\n        throw new TakaroUserError(`Failed to transfer ${args.amount} ${currencyName} to ${receiverName}. Are you sure you have enough balance?`);\n    }\n    const messageToReceiver = takaro.gameserver.gameServerControllerSendMessage(gameServerId, {\n        message: `You received ${args.amount} ${currencyName} from ${senderName}`,\n        opts: {\n            recipient: {\n                gameId: receiver.gameId,\n            },\n        },\n    });\n    await Promise.all([\n        sender.pm(`You successfully transferred ${args.amount} ${currencyName} to ${receiverName}`),\n        messageToReceiver,\n    ]);\n    return;\n}\nawait main();\n//# sourceMappingURL=transfer.js.map",
          "arguments": [
            {
              "name": "receiver",
              "type": "player",
              "helpText": "The player to transfer money to.",
              "position": 0
            },
            {
              "name": "amount",
              "type": "number",
              "helpText": "The amount of money to transfer.",
              "position": 1
            }
          ]
        },
        {
          "name": "claim",
          "trigger": "claim",
          "helpText": "Claim your pending shop orders.",
          "function": "import { takaro, data, TakaroUserError } from '@takaro/helpers';\nasync function main() {\n    const { user, player, arguments: args, gameServerId } = data;\n    if (!user) {\n        throw new TakaroUserError('You must link your account to Takaro to use this command.');\n    }\n    const filters = {\n        userId: [user.id],\n        status: ['PAID'],\n    };\n    // Only filter by gameServerId if it's available (should always be present in command context)\n    if (gameServerId) {\n        filters.gameServerId = [gameServerId];\n    }\n    const pendingOrdersRes = await takaro.shopOrder.shopOrderControllerSearch({\n        filters,\n        sortBy: 'createdAt',\n        sortDirection: 'asc',\n    });\n    if (pendingOrdersRes.data.data.length === 0) {\n        await player.pm('You have no pending orders.');\n        return;\n    }\n    let ordersToClaim = [];\n    if (args.all) {\n        ordersToClaim = pendingOrdersRes.data.data;\n    }\n    else {\n        ordersToClaim.push(pendingOrdersRes.data.data[0]);\n    }\n    for (const order of ordersToClaim) {\n        await takaro.shopOrder.shopOrderControllerClaim(order.id);\n    }\n}\nawait main();\n//# sourceMappingURL=claim.js.map",
          "arguments": [
            {
              "name": "all",
              "type": "boolean",
              "helpText": "If true, claim ALL pending orders. If false, claim only the first one.",
              "defaultValue": "false",
              "position": 0
            }
          ]
        },
        {
          "name": "shop",
          "trigger": "shop",
          "helpText": "Browse the shop and view available items.",
          "function": "import { takaro, data, TakaroUserError } from '@takaro/helpers';\nasync function main() {\n    const { arguments: args, player, gameServerId } = data;\n    const { page, item, action } = args;\n    const prefix = (await takaro.settings.settingsControllerGetOne('commandPrefix', gameServerId)).data.data.value;\n    // If command is called without any arguments\n    const messageWithoutPrefix = data.chatMessage.msg.slice(prefix.length).trim();\n    if (!messageWithoutPrefix.includes(' ')) {\n        await player.pm('This command allows you to browse the shop and view available items.');\n        await player.pm(`Usage: ${prefix}shop [page] [item] [action]`);\n        await player.pm(`${prefix}shop 2 - View the second page of shop items`);\n        await player.pm(`${prefix}shop 1 3 - View details about the third item on the first page`);\n        await player.pm(`${prefix}shop 1 3 buy - Purchase the third item on the first page`);\n        return;\n    }\n    const shopItems = await takaro.shopListing.shopListingControllerSearch({\n        limit: 5,\n        page: page - 1,\n        sortBy: 'name',\n        sortDirection: 'asc',\n        filters: {\n            gameServerId: [gameServerId],\n            draft: false,\n        },\n    });\n    if (shopItems.data.data.length === 0) {\n        await player.pm('No items found.');\n        return;\n    }\n    const currencyName = (await takaro.settings.settingsControllerGetOne('currencyName', data.gameServerId)).data.data;\n    if (!item) {\n        // List the shop items with index\n        let index = 1;\n        for (const listing of shopItems.data.data) {\n            const items = listing.items.slice(0, 3).map((item) => {\n                return `${item.amount}x ${item.item.name}`;\n            });\n            await player.pm(`${index} - ${listing.name} - ${listing.price} ${currencyName.value}. ${items.join(', ')}`);\n            index++;\n        }\n        return;\n    }\n    const selectedItem = shopItems.data.data[item - 1];\n    if (!selectedItem) {\n        throw new TakaroUserError(`Item not found. Please select an item from the list, valid options are 1-${shopItems.data.data.length}.`);\n    }\n    if (action === 'none') {\n        // Display more info about the item\n        await player.pm(`Listing ${selectedItem.name} - ${selectedItem.price} ${currencyName.value}`);\n        await Promise.all(selectedItem.items.map((item) => {\n            const quality = item.quality ? `Quality: ${item.quality}` : '';\n            const description = (item.item.description ? `Description: ${item.item.description}` : '').replaceAll('\\\\n', ' ');\n            return player.pm(`- ${item.amount}x ${item.item.name}. ${quality} ${description}`);\n        }));\n        return;\n    }\n    if (action === 'buy') {\n        const orderRes = await takaro.shopOrder.shopOrderControllerCreate({\n            amount: 1,\n            listingId: selectedItem.id,\n            playerId: player.id,\n        });\n        await player.pm(`You have purchased ${selectedItem.name} for ${selectedItem.price} ${currencyName.value}.`);\n        await takaro.shopOrder.shopOrderControllerClaim(orderRes.data.data.id);\n        return;\n    }\n    throw new TakaroUserError('Invalid action. Valid actions are \"buy\".');\n}\nawait main();\n//# sourceMappingURL=shop.js.map",
          "arguments": [
            {
              "name": "page",
              "type": "number",
              "helpText": "Display more items from the shop by specifying a page number.",
              "defaultValue": "1",
              "position": 0
            },
            {
              "name": "item",
              "type": "number",
              "helpText": "Select a specific item to view more details.",
              "defaultValue": "0",
              "position": 1
            },
            {
              "name": "action",
              "type": "string",
              "helpText": "Perform an action on the selected item. Currently only \"buy\" is supported.",
              "defaultValue": "none",
              "position": 2
            }
          ]
        }
      ],
      "hooks": [],
      "cronJobs": [
        {
          "name": "zombieKillReward",
          "temporalValue": "*/5 * * * *",
          "description": "",
          "function": "import { data, takaro, checkPermission } from '@takaro/helpers';\nconst VARIABLE_KEY = 'lastZombieKillReward';\nasync function main() {\n    const { gameServerId, module: mod } = data;\n    const lastRunRes = (await takaro.variable.variableControllerSearch({\n        filters: {\n            key: [VARIABLE_KEY],\n            gameServerId: [gameServerId],\n            moduleId: [mod.moduleId],\n        },\n    })).data.data;\n    // We last ran the rewards script at this time\n    // If this is the first time we run it, just get the last 5 minutes\n    const lastRun = lastRunRes.length ? new Date(JSON.parse(lastRunRes[0].value)) : new Date(Date.now() - 5 * 60 * 1000);\n    // Fetch all the kill events since the last time we gave out rewards\n    const killEvents = (await takaro.event.eventControllerSearch({\n        filters: { eventName: ['entity-killed'], gameserverId: [gameServerId] },\n        greaterThan: { createdAt: lastRun.toISOString() },\n        limit: 1000,\n    })).data.data;\n    console.log(`Found ${killEvents.length} kill events since ${lastRun.toISOString()}`);\n    // Group the events by player\n    const playerKills = {};\n    for (const killEvent of killEvents) {\n        if (!playerKills[killEvent.playerId]) {\n            playerKills[killEvent.playerId] = [];\n        }\n        playerKills[killEvent.playerId].push(killEvent);\n    }\n    // Give each player their reward\n    // We use Promise.allSettled to run this concurrently\n    const results = await Promise.allSettled(Object.entries(playerKills).map(async ([playerId, kills]) => {\n        const pog = (await takaro.playerOnGameserver.playerOnGameServerControllerGetOne(gameServerId, playerId)).data\n            .data;\n        const hasPermission = checkPermission(pog, 'ZOMBIE_KILL_REWARD_OVERRIDE');\n        const defaultReward = mod.userConfig.zombieKillReward;\n        const reward = hasPermission && hasPermission.count !== null && hasPermission.count !== undefined\n            ? hasPermission.count\n            : defaultReward;\n        const totalReward = reward * kills.length;\n        return takaro.playerOnGameserver.playerOnGameServerControllerAddCurrency(gameServerId, playerId, {\n            currency: totalReward,\n            reason: 'Zombie kill reward',\n        });\n    }));\n    // Log any errors\n    for (const result of results) {\n        if (result.status === 'rejected') {\n            console.error(result.reason);\n            throw new Error(`Failed to give rewards: ${result.reason}`);\n        }\n    }\n    // Update the last run time\n    if (lastRunRes.length) {\n        await takaro.variable.variableControllerUpdate(lastRunRes[0].id, {\n            value: JSON.stringify(new Date()),\n        });\n    }\n    else {\n        await takaro.variable.variableControllerCreate({\n            key: VARIABLE_KEY,\n            value: JSON.stringify(new Date()),\n            moduleId: mod.moduleId,\n            gameServerId,\n        });\n    }\n}\nawait main();\n//# sourceMappingURL=zombieKillReward.js.map"
        }
      ],
      "functions": [],
      "permissions": [
        {
          "permission": "ECONOMY_UTILS_MANAGE_CURRENCY",
          "friendlyName": "Manage currency",
          "description": "Allows players to manage currency of other players. This includes granting and revoking currency.",
          "canHaveCount": false
        },
        {
          "permission": "ZOMBIE_KILL_REWARD_OVERRIDE",
          "friendlyName": "Zombie kill reward override",
          "description": "Allows a role to override the amount of currency a player receives for killing a entity.",
          "canHaveCount": true
        }
      ]
    }
  ]
}