├── client ├── src │ └── .gitkeep └── index.ts ├── icons ├── beer.png ├── cola.png ├── bread.png ├── hotdog.png ├── energydrink.png └── waterbottle.png ├── webview ├── OSS_shopUI.jpg └── OSS_ShopUI.vue ├── dependencies.json ├── shared ├── enums │ ├── ShopType.ts │ ├── Translations.ts │ └── ShopEvents.ts └── interfaces │ ├── IShopItem.ts │ ├── IShopLocation.ts │ └── IShop.ts ├── server ├── src │ ├── config │ │ ├── index.ts │ │ └── locations.ts │ ├── items │ │ ├── shop_drinks_alcoholic.ts │ │ ├── main.ts │ │ ├── shop_food.ts │ │ ├── shop_drinks.ts │ │ └── shop_items.ts │ ├── events.ts │ ├── registry.ts │ └── controller.ts └── index.ts ├── LICENSE └── README.md /client/src/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/beer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booster1212/plugin-shop/HEAD/icons/beer.png -------------------------------------------------------------------------------- /icons/cola.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booster1212/plugin-shop/HEAD/icons/cola.png -------------------------------------------------------------------------------- /icons/bread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booster1212/plugin-shop/HEAD/icons/bread.png -------------------------------------------------------------------------------- /icons/hotdog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booster1212/plugin-shop/HEAD/icons/hotdog.png -------------------------------------------------------------------------------- /icons/energydrink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booster1212/plugin-shop/HEAD/icons/energydrink.png -------------------------------------------------------------------------------- /icons/waterbottle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booster1212/plugin-shop/HEAD/icons/waterbottle.png -------------------------------------------------------------------------------- /webview/OSS_shopUI.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booster1212/plugin-shop/HEAD/webview/OSS_shopUI.jpg -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "githubDependencies": ["https://github.com/Booster1212/plugin-notifications"] 3 | } 4 | -------------------------------------------------------------------------------- /shared/enums/ShopType.ts: -------------------------------------------------------------------------------- 1 | export const enum ShopType { 2 | BUY = 'buy', //Players can buy stuff 3 | SELL = 'sell', //Players can sell stuff 4 | } 5 | -------------------------------------------------------------------------------- /shared/enums/Translations.ts: -------------------------------------------------------------------------------- 1 | export const enum ShopTranslations { 2 | openShop = 'Open Shop', 3 | openSellingShop = 'Open Shop', 4 | notEnoughFunds = 'Not enough funds!', 5 | } 6 | -------------------------------------------------------------------------------- /server/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export const shopConfig = { 2 | name: 'Athena Framework - Open Source Shop', 3 | version: 'v3', 4 | randomizeBuyers: false, // Will randomize output of vending machines as well. 5 | randomizeSellers: false, // Randomize drug dealer prices for examples (based on list.) 6 | }; 7 | -------------------------------------------------------------------------------- /shared/interfaces/IShopItem.ts: -------------------------------------------------------------------------------- 1 | export interface IShopItem { 2 | dbName: string; // Database name of the item in the database items collection. 3 | price: number; // Price of the item. 4 | name?: string; // Optional: The name of the Item. Default is name from Item-Factory 5 | } 6 | // TODO: Add 7 | /* 8 | export interface OSSItem { 9 | name: string; 10 | } 11 | */ 12 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import * as alt from 'alt-server'; 2 | import * as Athena from '@AthenaServer/api/index.js'; 3 | import * as Shop from './src/controller.js'; 4 | import { shopConfig } from './src/config/index.js'; 5 | 6 | import './src/items/main.js'; 7 | import './src/events.js'; 8 | 9 | Athena.systems.plugins.registerPlugin(shopConfig.name, async () => { 10 | await Shop.loadShops(); 11 | 12 | alt.log(`~lg~${shopConfig.name} ${shopConfig.version} successfully loaded.`); 13 | }); 14 | -------------------------------------------------------------------------------- /shared/interfaces/IShopLocation.ts: -------------------------------------------------------------------------------- 1 | import { Animation } from '@AthenaShared/interfaces/animation'; 2 | import * as alt from 'alt-shared'; 3 | 4 | export interface IShopLocation { 5 | x: number; 6 | y: number; 7 | z: number; 8 | isBlip?: boolean; //Enable/Disable blip e.g. none for Vendors. Already defined from Athena in shared/information 9 | isVendingMachine?: boolean; 10 | shopAcceptsCard?: boolean; // default is false 11 | ped?: { 12 | model: string; 13 | heading: number; 14 | pos: alt.Vector3; 15 | animations?: Animation[]; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /shared/interfaces/IShop.ts: -------------------------------------------------------------------------------- 1 | import { ShopType } from '../enums/ShopType'; 2 | import { IShopItem } from './IShopItem'; 3 | import { IShopLocation } from './IShopLocation'; 4 | 5 | export interface IShop { 6 | _id?: string; 7 | name: string; 8 | shopType?: ShopType; // BUY || SELL - Default BUY 9 | shopImage?: string; 10 | 11 | data: { 12 | items?: IShopItem[]; 13 | interactionRange?: number; 14 | blip?: { 15 | shortRange: boolean; 16 | sprite: number; 17 | color: number; 18 | scale: number; 19 | }; 20 | }; 21 | 22 | locations: IShopLocation[]; 23 | } 24 | -------------------------------------------------------------------------------- /server/src/items/shop_drinks_alcoholic.ts: -------------------------------------------------------------------------------- 1 | import { ShopEvents } from '@AthenaPlugins/plugin-shop/shared/enums/ShopEvents.js'; 2 | import { BaseItem } from '@AthenaShared/interfaces/item.js'; 3 | 4 | const drinkBehavior = { canDrop: true, canStack: true, canTrade: true }; 5 | export const shopDrinksAlcoholic: Array = [ 6 | { 7 | name: 'Beer', 8 | icon: '@AthenaPlugins/icons/plugin-shop/beer.png', 9 | behavior: drinkBehavior, 10 | data: { 11 | amount: 25, 12 | }, 13 | dbName: 'shop-beer', 14 | weight: 1, 15 | consumableEventToCall: ShopEvents.DRINK_EFFECT_ALCOHOLIC, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /shared/enums/ShopEvents.ts: -------------------------------------------------------------------------------- 1 | export const enum ShopEvents { 2 | OPEN_SHOP = 'open-source-shop:open-shop', 3 | CLOSE_SHOP = 'open-source-shop:close-shop', 4 | HANDLE_SHOP = 'open-source-shop:handle-shop', 5 | BUY_ITEMS_FROM_CART = 'open-source-shop:buy-items-from-cart', 6 | SELL_ITEMS_FROM_CART = 'open-source-shop:sell-items-from-cart', 7 | SET_ITEMS = 'open-source-shop:set-items', 8 | DRINK_EFFECT_ALCOHOLIC = 'open-source-shop:drink-effect-alcoholic', 9 | DRINK_EFFECT = 'open-source-shop:drink-effect', 10 | FOOD_EFFECT = 'open-source-shop:food-effect', 11 | RESET_CART = 'open-source-shop:reset-cart', 12 | REQUEST_SHOP_ITEMS = 'open-source-shop:request-shop-items', 13 | } 14 | -------------------------------------------------------------------------------- /server/src/items/main.ts: -------------------------------------------------------------------------------- 1 | import * as Athena from '@AthenaServer/api/index.js'; 2 | import * as Shop from '@AthenaPlugins/plugin-shop/server/src/controller.js'; 3 | 4 | import { shopFood } from './shop_food.js'; 5 | import { shopDrinks } from './shop_drinks.js'; 6 | import { shopDrinksAlcoholic } from './shop_drinks_alcoholic.js'; 7 | import { ShopEvents } from '@AthenaPlugins/plugin-shop/shared/enums/ShopEvents.js'; 8 | 9 | const itemsToAdd = [...shopFood, ...shopDrinks, ...shopDrinksAlcoholic]; 10 | 11 | async function registerShopItems() { 12 | for (const item of itemsToAdd) { 13 | await Athena.systems.inventory.factory.upsertAsync(item); 14 | } 15 | } 16 | 17 | registerShopItems(); 18 | 19 | Athena.systems.inventory.effects.add(ShopEvents.FOOD_EFFECT, Shop.foodEffect); 20 | Athena.systems.inventory.effects.add(ShopEvents.DRINK_EFFECT, Shop.drinkEffect); 21 | Athena.systems.inventory.effects.add(ShopEvents.DRINK_EFFECT_ALCOHOLIC, Shop.drinkEffectAlcoholic); 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Der Lord! 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/src/items/shop_food.ts: -------------------------------------------------------------------------------- 1 | import { ShopEvents } from '@AthenaPlugins/plugin-shop/shared/enums/ShopEvents.js'; 2 | import { BaseItem } from '@AthenaShared/interfaces/item.js'; 3 | 4 | const foodBehavior = { canDrop: true, canStack: true, canTrade: true }; 5 | 6 | export const shopFood: Array = [ 7 | { 8 | name: 'Bread', 9 | weight: 0.25, 10 | icon: '@AthenaPlugins/icons/plugin-shop/bread.png', 11 | behavior: foodBehavior, 12 | data: { 13 | amount: 25, 14 | }, 15 | dbName: 'shop-bread', 16 | consumableEventToCall: ShopEvents.FOOD_EFFECT, 17 | }, 18 | { 19 | name: 'Hotdog', 20 | weight: 1, 21 | icon: '@AthenaPlugins/icons/plugin-shop/hotdog.png', 22 | behavior: foodBehavior, 23 | data: { 24 | amount: 25, 25 | }, 26 | dbName: 'shop-hotdog', 27 | consumableEventToCall: ShopEvents.FOOD_EFFECT, 28 | }, 29 | { 30 | name: 'Pizza', 31 | weight: 2.5, 32 | icon: '@AthenaPlugins/icons/plugin-shop/pizza.png', 33 | behavior: foodBehavior, 34 | data: { 35 | amount: 25, 36 | }, 37 | dbName: 'shop-pizza', 38 | consumableEventToCall: ShopEvents.FOOD_EFFECT, 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /server/src/items/shop_drinks.ts: -------------------------------------------------------------------------------- 1 | import { ShopEvents } from '@AthenaPlugins/plugin-shop/shared/enums/ShopEvents.js'; 2 | import { BaseItem } from '@AthenaShared/interfaces/item.js'; 3 | 4 | const drinkBehavior = { canDrop: true, canStack: true, canTrade: true }; 5 | 6 | export const shopDrinks: Array = [ 7 | { 8 | name: 'Waterbottle', 9 | icon: '@AthenaPlugins/icons/plugin-shop/waterbottle.png', 10 | behavior: drinkBehavior, 11 | data: { 12 | amount: 25, 13 | }, 14 | dbName: 'shop-water', 15 | weight: 1, 16 | consumableEventToCall: ShopEvents.DRINK_EFFECT, 17 | }, 18 | { 19 | name: 'Energy Drink', 20 | icon: '@AthenaPlugins/icons/plugin-shop/energydrink.png', 21 | behavior: drinkBehavior, 22 | data: { 23 | amount: 10, 24 | }, 25 | dbName: 'shop-energy', 26 | weight: 0.25, 27 | consumableEventToCall: ShopEvents.DRINK_EFFECT, 28 | }, 29 | { 30 | name: 'Cola', 31 | icon: '@AthenaPlugins/icons/plugin-shop/cola.png', 32 | behavior: drinkBehavior, 33 | data: { 34 | amount: 5, 35 | }, 36 | dbName: 'shop-cola', 37 | weight: 0.75, 38 | consumableEventToCall: ShopEvents.DRINK_EFFECT, 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /client/index.ts: -------------------------------------------------------------------------------- 1 | import * as alt from 'alt-client'; 2 | import * as AthenaClient from '@AthenaClient/api/index.js'; 3 | import { onTicksStart } from '@AthenaClient/events/onTicksStart.js'; 4 | import { ShopEvents } from '../shared/enums/ShopEvents.js'; 5 | 6 | function init() { 7 | const page = new AthenaClient.webview.Page({ 8 | name: 'OSS_ShopUI', 9 | callbacks: { 10 | onReady: async () => {}, 11 | onClose: () => {}, 12 | }, 13 | options: { 14 | onOpen: { 15 | focus: true, 16 | hideHud: true, 17 | setIsMenuOpenToTrue: true, 18 | hideOverlays: false, 19 | showCursor: true, 20 | disableControls: 'all', 21 | disablePauseMenu: true, 22 | }, 23 | onClose: { 24 | hideCursor: true, 25 | showHud: true, 26 | unfocus: true, 27 | setIsMenuOpenToFalse: true, 28 | enableControls: true, 29 | enablePauseMenu: true, 30 | }, 31 | }, 32 | }); 33 | 34 | alt.onServer(ShopEvents.OPEN_SHOP, () => { 35 | if (typeof page !== 'undefined') { 36 | page.open(); 37 | } 38 | }); 39 | } 40 | 41 | onTicksStart.add(init); 42 | -------------------------------------------------------------------------------- /server/src/items/shop_items.ts: -------------------------------------------------------------------------------- 1 | export const coreShops: Array<{ dbName: string; price: number }> = [ 2 | { dbName: 'shop-bread', price: 75 }, 3 | { dbName: 'shop-hotdog', price: 375 }, 4 | { dbName: 'shop-water', price: 250 }, 5 | { dbName: 'shop-cola', price: 250 }, 6 | { dbName: 'shop-energy', price: 300 }, 7 | { dbName: 'shop-beer', price: 250 }, 8 | ]; 9 | 10 | export const sellingShops: Array<{ dbName: string; price: number }> = [ 11 | { dbName: 'shop-water', price: 25 }, 12 | { dbName: 'shop-bread', price: 25 }, 13 | ]; 14 | 15 | export const ltdShops: Array<{ dbName: string; price: number }> = [{ dbName: 'shop-water', price: 250 }]; 16 | 17 | export const robsLiquorShops: Array<{ dbName: string; price: number }> = [{ dbName: 'shop-beer', price: 330 }]; 18 | 19 | export const juiceShops: Array<{ dbName: string; price: number }> = [ 20 | { dbName: 'shop-water', price: 220 }, 21 | { dbName: 'shop-energy', price: 330 }, 22 | ]; 23 | 24 | export const liquorAceShops: Array<{ dbName: string; price: number }> = [ 25 | { dbName: 'shop-water', price: 250 }, 26 | { dbName: 'shop-beer', price: 330 }, 27 | ]; 28 | 29 | export const ammunationShops: Array<{ dbName: string; price: number }> = [ 30 | { dbName: 'appistol', price: 50000 }, 31 | { dbName: 'assaultrifle', price: 500000 }, 32 | ]; 33 | 34 | export const tequilalaShops: Array<{ dbName: string; price: number }> = [ 35 | { dbName: 'shop-water', price: 330 }, 36 | { dbName: 'shop-beer', price: 425 }, 37 | ]; 38 | 39 | export const bahamaMamasShops: Array<{ dbName: string; price: number }> = [{ dbName: 'shop-beer', price: 500 }]; 40 | 41 | export const vanillaUnicornShops: Array<{ dbName: string; price: number }> = [{ dbName: 'shop-beer', price: 425 }]; 42 | 43 | export const toolShops: Array<{ dbName: string; price: number }> = [{ dbName: 'shop-water', price: 250 }]; 44 | 45 | export const vendingMachines: Array<{ dbName: string; price: number }> = [{ dbName: 'shop-water', price: 500 }]; 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Source Shop 2 | 3 |

4 | 5 |

6 | Herewith we bring a free store system for the Athena Framework which is under the MIT license and thus can be completely modified and re-released. 7 | 8 | ## Description 9 | 10 | The store system is completely adapted to the Athena Framework and only things that are already integrated into the Athena Framework (Core) are used. 11 | 12 | - Features 13 | - Athena Framework (v5.0.0) 14 | - Custom Shops (Buy/Sell) 15 | - Searchbar 16 | - Choose between cash-only or cash and bank 17 | - TypeScript / VueJS 18 | 19 | ## Getting Started 20 | 21 | ### Dependencies 22 | 23 | - Basic Knowledge of the Athena Framework, TypeScript and VueJS. 24 | 25 | ### Installing 26 | 27 | - Clone Repository (https://github.com/Booster1212/OpenSourceShop.git) directly into src/core/plugins. 28 | 29 | ## Help 30 | 31 | In case of any unforeseen problems with the store system, please feel free to contact us in our Discord server, we will try to help you as soon as possible. 32 | 33 | ## Contribute to this plugin 34 | 35 | If you want to contribute something to our open source store system, you are very welcome to do so by creating a pull request, you can of course also submit bugs or feature requests via the GitHub issue system! 36 | 37 | ## Authors & Contributors 38 | 39 | - Author 40 | - Der Lord! 41 | - Contributors 42 | - CANAKIL 43 | - DavRenz 44 | - deeMace 45 | - jonesXYZ 46 | 47 | ## License 48 | 49 | This project is licensed under the [MIT] License - see the LICENSE.md file for details 50 | 51 | ## Links 52 | 53 | - [Athena Framework](https://athenaframework.com/) 54 | - [Hypedmedia.de - Discord Server](https://discord.gg/baHqqw7fbS) 55 | 56 | ## Support my work 57 | 58 | Programming plugins of course takes a lot of time, since I provide most of it as open source code for learning purposes, you have the opportunity to support me here, this is of course on a purely voluntary basis, thank you! <3 59 | 60 |

61 | 62 | 63 | 64 |

65 | -------------------------------------------------------------------------------- /server/src/events.ts: -------------------------------------------------------------------------------- 1 | import * as alt from 'alt-server'; 2 | import * as Athena from '@AthenaServer/api/index.js'; 3 | import { ShopEvents } from '@AthenaPlugins/plugin-shop/shared/enums/ShopEvents.js'; 4 | import { icons } from '@AthenaPlugins/plugin-notifications/shared/config.js'; 5 | 6 | alt.onClient(ShopEvents.BUY_ITEMS_FROM_CART, async (player: alt.Player, cartItems: Array) => { 7 | const Notify = await Athena.systems.plugins.useAPI('notify'); 8 | 9 | const playerData = Athena.document.character.get(player); 10 | const cartData = Object.values(cartItems); 11 | 12 | if (cartData.length === 0) { 13 | Notify.send(player, icons['icon-warning'], 10, 'Open Source Shop', 'There are no items in your cart.'); 14 | return; 15 | } 16 | 17 | let totalPrice = 0; 18 | 19 | for (const item of cartData) { 20 | const itemTotalPrice = item.quantity * item.price; 21 | totalPrice += itemTotalPrice; 22 | 23 | if (totalPrice > playerData.cash) { 24 | Athena.player.emit.notification(player, `Not enough money`); 25 | return; 26 | } 27 | } 28 | 29 | if (!Athena.player.currency.sub(player, 'cash', totalPrice)) return; 30 | 31 | for (const item of cartData) { 32 | const baseItemFound = item; 33 | 34 | const isAdded = await Athena.player.inventory.add(player, { 35 | dbName: baseItemFound.dbName, 36 | data: baseItemFound.data, 37 | quantity: item.quantity, 38 | }); 39 | 40 | if (!isAdded) { 41 | Athena.player.emit.notification(player, `Can't add item. Inventory full?`); 42 | return; 43 | } 44 | } 45 | 46 | Athena.webview.emit(player, ShopEvents.RESET_CART); 47 | Notify.send( 48 | player, 49 | icons['icon-info2'], 50 | 10, 51 | 'Open Source Shop', 52 | `Successfully purchased items. Total: $${totalPrice}`, 53 | ); 54 | }); 55 | 56 | alt.onClient(ShopEvents.SELL_ITEMS_FROM_CART, async (player: alt.Player, cartItems: Array) => { 57 | const playerData = Athena.document.character.get(player); 58 | const cartData = Object.values(cartItems); 59 | 60 | let totalPrice = 0; 61 | let allItemsRemoved = true; 62 | 63 | for (const item of cartData) { 64 | const baseItemFound = item; 65 | const isRemoved = await Athena.player.inventory.sub(player, { 66 | dbName: baseItemFound.dbName, 67 | quantity: baseItemFound.quantity, 68 | }); 69 | 70 | if (!isRemoved) { 71 | allItemsRemoved = false; 72 | } 73 | 74 | const itemTotalPrice = item.quantity * item.price; 75 | totalPrice += itemTotalPrice; 76 | } 77 | 78 | if (allItemsRemoved && totalPrice > 0) { 79 | console.log(`Sold items for a total of $${totalPrice}`); 80 | console.log(`All items sold successfully.`); 81 | Athena.player.currency.add(player, 'cash', totalPrice); 82 | } else { 83 | Athena.player.emit.notification(player, `Can't sell items. Something went wrong.`); 84 | return false; 85 | } 86 | return true; 87 | }); 88 | -------------------------------------------------------------------------------- /server/src/config/locations.ts: -------------------------------------------------------------------------------- 1 | import { IShopLocation } from '@AthenaPlugins/plugin-shop/shared/interfaces/IShopLocation.js'; 2 | import vendingMachines from '@AthenaShared/information/vendingMachines.js'; 3 | 4 | /* <-- AMMUNATION --> */ 5 | export const ammunationLocations: IShopLocation[] = [ 6 | { x: 21.903297424316406, y: -1108.140625, z: 29.785400390625, isBlip: true }, 7 | { x: -330.4879150390625, y: 6083.93408203125, z: 31.4534912109375, isBlip: true }, 8 | { x: 1693.3714599609375, y: 3760.12744140625, z: 34.6885986328125, isBlip: true }, 9 | { x: -1117.4769287109375, y: 2698.839599609375, z: 18.5465087890625, isBlip: true }, 10 | { x: 2567.301025390625, y: 294.1450500488281, z: 108.7266845703125, isBlip: true }, 11 | { x: 251.92088317871094, y: -50.36043930053711, z: 69.9384765625, isBlip: true }, 12 | { x: -1305.982421875, y: -394.4703369140625, z: 36.6937255859375, isBlip: true }, 13 | { x: -661.9384765625, y: -935.2879028320312, z: 21.8154296875, isBlip: true }, 14 | { x: 842.1626586914062, y: -1033.81982421875, z: 28.1845703125, isBlip: true }, 15 | { x: 810.0263671875, y: -2157.177978515625, z: 29.6168212890625, isBlip: true }, 16 | ]; 17 | 18 | /* <-- BAHAMA MAMAS --> */ 19 | export const bahamaMamasLocations: IShopLocation[] = [ 20 | { x: -1394.1494140625, y: -603.96923828125, z: 30.3077392578125, isBlip: true }, 21 | { x: -1377.6131591796875, y: -627.3362426757812, z: 30.813232421875, isBlip: true }, 22 | ]; 23 | 24 | /* <-- CORE SHOPS (24/7) --> */ 25 | export const coreShopLocations: IShopLocation[] = [ 26 | { x: 25.980966567993164, y: -1345.6417236328125, z: 28.497024536132812, isBlip: true, shopAcceptsCard: true }, 27 | { x: 374.3475341796875, y: 328.112060546875, z: 102.56637573242188, isBlip: true, shopAcceptsCard: true }, 28 | { x: -3041.32763671875, y: 585.155029296875, z: 6.908928871154785, isBlip: true, shopAcceptsCard: true }, 29 | { x: -3243.743408203125, y: 1001.3903198242188, z: 11.830706596374512, isBlip: true, shopAcceptsCard: true }, 30 | { x: 548.0447387695312, y: 2669.48876953125, z: 41.156490325927734, isBlip: true, shopAcceptsCard: true }, 31 | { x: 1960.2322998046875, y: 3742.317138671875, z: 31.343746185302734, isBlip: true, shopAcceptsCard: true }, 32 | { x: 1730.01171875, y: 6416.22021484375, z: 34.03722381591797, isBlip: true, shopAcceptsCard: true }, 33 | { x: 2555.4609375, y: 382.1643371582031, z: 107.62295532226562, isBlip: true, shopAcceptsCard: true }, 34 | ]; 35 | 36 | /* <-- JUICE --> */ 37 | export const juiceLocations: IShopLocation[] = [ 38 | { x: 1166.243896484375, y: 2709.356689453125, z: 37.15770721435547, isBlip: true }, 39 | ]; 40 | 41 | /* <-- LIQUOR ACE --> */ 42 | export const liquorAceLocations: IShopLocation[] = [ 43 | { x: 1393.5035400390625, y: 3605.268798828125, z: 33.98093032836914, isBlip: true }, 44 | ]; 45 | 46 | /* <-- LTD --> */ 47 | export const ltdLocations: IShopLocation[] = [ 48 | { x: -48.5690803527832, y: -1757.6961669921875, z: 28.4210147857666, isBlip: true, shopAcceptsCard: true }, 49 | { x: 1163.400634765625, y: -323.938232421875, z: 68.20509338378906, isBlip: true, shopAcceptsCard: true }, 50 | { x: -707.4390258789062, y: -914.5612182617188, z: 18.21558952331543, isBlip: true, shopAcceptsCard: true }, 51 | { x: 1697.968505859375, y: 4924.54833984375, z: 41.06367492675781, isBlip: true, shopAcceptsCard: true }, 52 | { x: -1820.6717529296875, y: 792.8975219726562, z: 137.11163330078125, isBlip: true, shopAcceptsCard: true }, 53 | ]; 54 | 55 | /* <-- ROBS LIQUOR --> */ 56 | export const robsLiquorLocations: IShopLocation[] = [ 57 | { x: 1135.9544677734375, y: -981.8599853515625, z: 45.41580581665039, isBlip: true }, 58 | { x: -1222.91162109375, y: -907.1942749023438, z: 11.326356887817383, isBlip: true }, 59 | { x: -1487.08349609375, y: -379.2518005371094, z: 39.163429260253906, isBlip: true }, 60 | { x: -2967.888427734375, y: 390.76654052734375, z: 14.043313026428223, isBlip: true }, 61 | ]; 62 | 63 | /* <-- CORE (SELLERS) --> */ 64 | export const sellerExampleLocations: IShopLocation[] = [ 65 | { x: 1241.4119873046875, y: -367.92218017578125, z: 68.08222961425781, isBlip: true }, 66 | ]; 67 | 68 | /* <-- TequiLala --> */ 69 | export const tequiLaLaLocations: IShopLocation[] = [ 70 | { x: -560.1758422851562, y: 286.4307556152344, z: 82.17138671875, isBlip: true }, 71 | ]; 72 | 73 | /* <-- TOOL SHOPS --> */ 74 | export const toolShopLocations: IShopLocation[] = [ 75 | { x: 2748.77392578125, y: 3472.442626953125, z: 55.67741012573242 - 1, isBlip: true, shopAcceptsCard: true }, 76 | ]; 77 | 78 | /* <-- Vanilla Unicorn --> */ 79 | export const vanillaUnicornLocations: IShopLocation[] = [ 80 | { x: 127.16043853759766, y: -1283.4989013671875, z: 29.2630615234375, isBlip: true }, 81 | ]; 82 | 83 | export const vendingMachinesShop: IShopLocation[] = [ 84 | ...vendingMachines.map((machine) => ({ ...machine, isBlip: false, isVendingMachine: true })), 85 | ]; 86 | -------------------------------------------------------------------------------- /server/src/registry.ts: -------------------------------------------------------------------------------- 1 | import * as ShopLocations from '@AthenaPlugins/plugin-shop/server/src/config/locations.js'; 2 | import * as ShopItems from '@AthenaPlugins/plugin-shop/server/src/items/shop_items.js'; 3 | 4 | import { ShopType } from '@AthenaPlugins/plugin-shop/shared/enums/ShopType.js'; 5 | import { IShop } from '@AthenaPlugins/plugin-shop/shared/interfaces/IShop.js'; 6 | 7 | export const ShopRegistry: IShop[] = [ 8 | { 9 | name: '24/7 Shop', 10 | shopType: ShopType.BUY, 11 | data: { 12 | blip: { 13 | sprite: 59, 14 | color: 2, 15 | scale: 1, 16 | shortRange: true, 17 | }, 18 | items: [...ShopItems.coreShops], 19 | interactionRange: 2, 20 | }, 21 | locations: ShopLocations.coreShopLocations, 22 | }, 23 | { 24 | name: 'Example Selling Store', 25 | shopType: ShopType.SELL, 26 | data: { 27 | blip: { 28 | sprite: 52, 29 | color: 1, 30 | scale: 1, 31 | shortRange: true, 32 | }, 33 | items: [...ShopItems.sellingShops], 34 | interactionRange: 2, 35 | }, 36 | locations: ShopLocations.sellerExampleLocations, 37 | }, 38 | { 39 | name: 'LTD', 40 | shopType: ShopType.BUY, 41 | data: { 42 | blip: { 43 | sprite: 59, 44 | color: 2, 45 | scale: 1, 46 | shortRange: true, 47 | }, 48 | items: [...ShopItems.ltdShops], 49 | interactionRange: 2, 50 | }, 51 | locations: ShopLocations.ltdLocations, 52 | }, 53 | { 54 | name: 'Robs Liquor', 55 | shopType: ShopType.BUY, 56 | data: { 57 | blip: { 58 | sprite: 59, 59 | color: 2, 60 | scale: 1, 61 | shortRange: true, 62 | }, 63 | items: [...ShopItems.robsLiquorShops], 64 | interactionRange: 2, 65 | }, 66 | locations: ShopLocations.robsLiquorLocations, 67 | }, 68 | { 69 | name: 'Juice', 70 | shopType: ShopType.BUY, 71 | data: { 72 | blip: { 73 | sprite: 59, 74 | color: 2, 75 | scale: 1, 76 | shortRange: true, 77 | }, 78 | items: [...ShopItems.juiceShops], 79 | interactionRange: 2, 80 | }, 81 | locations: ShopLocations.juiceLocations, 82 | }, 83 | { 84 | name: 'Liquor ACE', 85 | shopType: ShopType.BUY, 86 | data: { 87 | blip: { 88 | sprite: 59, 89 | color: 2, 90 | scale: 1, 91 | shortRange: true, 92 | }, 93 | items: [...ShopItems.liquorAceShops], 94 | interactionRange: 2, 95 | }, 96 | locations: ShopLocations.liquorAceLocations, 97 | }, 98 | { 99 | name: 'Tool Shop', 100 | shopType: ShopType.BUY, 101 | data: { 102 | blip: { 103 | sprite: 59, 104 | color: 2, 105 | scale: 1, 106 | shortRange: true, 107 | }, 108 | items: [...ShopItems.toolShops], 109 | interactionRange: 2, 110 | }, 111 | locations: ShopLocations.toolShopLocations, 112 | }, 113 | { 114 | name: 'Ammunation', 115 | shopType: ShopType.BUY, 116 | data: { 117 | blip: { 118 | sprite: 110, 119 | color: 2, 120 | scale: 1, 121 | shortRange: true, 122 | }, 123 | items: [...ShopItems.ammunationShops], 124 | interactionRange: 2, 125 | }, 126 | locations: ShopLocations.ammunationLocations, 127 | }, 128 | { 129 | name: 'Tequi-la-la', 130 | shopType: ShopType.BUY, 131 | data: { 132 | blip: { 133 | sprite: 93, 134 | color: 48, 135 | scale: 1, 136 | shortRange: true, 137 | }, 138 | items: [...ShopItems.tequilalaShops], 139 | interactionRange: 2, 140 | }, 141 | locations: ShopLocations.tequiLaLaLocations, 142 | }, 143 | { 144 | name: 'Bahama Mamas', 145 | shopType: ShopType.BUY, 146 | data: { 147 | blip: { 148 | sprite: 93, 149 | color: 48, 150 | scale: 1, 151 | shortRange: true, 152 | }, 153 | items: [...ShopItems.bahamaMamasShops], 154 | interactionRange: 2, 155 | }, 156 | locations: ShopLocations.bahamaMamasLocations, 157 | }, 158 | { 159 | name: 'Vanilla Unicorn', 160 | shopType: ShopType.BUY, 161 | data: { 162 | blip: { 163 | sprite: 93, 164 | color: 48, 165 | scale: 1, 166 | shortRange: true, 167 | }, 168 | items: [...ShopItems.vanillaUnicornShops], 169 | interactionRange: 2, 170 | }, 171 | locations: ShopLocations.vanillaUnicornLocations, 172 | }, 173 | { 174 | name: 'Vending Machine', 175 | shopType: ShopType.BUY, 176 | data: { 177 | items: [...ShopItems.vendingMachines], 178 | interactionRange: 2, 179 | }, 180 | locations: ShopLocations.vendingMachinesShop, 181 | }, 182 | ]; 183 | -------------------------------------------------------------------------------- /server/src/controller.ts: -------------------------------------------------------------------------------- 1 | import * as alt from 'alt-server'; 2 | import * as Athena from '@AthenaServer/api/index.js'; 3 | 4 | import { ShopRegistry } from './registry.js'; 5 | import { ShopType } from '@AthenaPlugins/plugin-shop/shared/enums/ShopType.js'; 6 | import { ShopEvents } from '@AthenaPlugins/plugin-shop/shared/enums/ShopEvents.js'; 7 | import { IShop } from '@AthenaPlugins/plugin-shop/shared/interfaces/IShop.js'; 8 | import { IShopLocation } from '@AthenaPlugins/plugin-shop/shared/interfaces/IShopLocation.js'; 9 | import { shopConfig } from './config/index.js'; 10 | import { ShopTranslations } from '@AthenaPlugins/plugin-shop/shared/enums/Translations.js'; 11 | 12 | export async function loadShops() { 13 | ShopRegistry.forEach((shop) => { 14 | if ( 15 | (shopConfig.randomizeSellers && shop.shopType === ShopType.SELL) || 16 | (shopConfig.randomizeBuyers && (!shop.shopType || shop.shopType === ShopType.BUY)) 17 | ) { 18 | shop.data.items.forEach((item) => { 19 | const registryItem = shop.data.items.find((itemToFind) => itemToFind.dbName === item.dbName); 20 | if (registryItem) { 21 | item.price = getRandomInt(1, registryItem.price); 22 | } 23 | }); 24 | } 25 | 26 | shop.locations.forEach(async (location, i) => { 27 | if (location.isBlip && shop.data.blip) { 28 | Athena.controllers.blip.append({ 29 | pos: new alt.Vector3(location.x, location.y, location.z), 30 | shortRange: shop.data.blip.shortRange, 31 | sprite: shop.data.blip.sprite, 32 | color: shop.data.blip.color, 33 | text: shop.name, 34 | scale: shop.data.blip.scale, 35 | uid: `OSS-Shop-${shop.name}-${i}`, 36 | }); 37 | } 38 | 39 | if (location.ped) { 40 | Athena.controllers.staticPed.append({ 41 | model: location.ped.model, 42 | pos: location.ped.pos, 43 | rotation: location.ped.heading, 44 | maxDistance: 100, 45 | animations: location.ped.animations, 46 | dimension: 0, 47 | uid: `OSS-PED-${shop.name}-${i}`, 48 | }); 49 | } 50 | 51 | Athena.controllers.interaction.append({ 52 | position: new alt.Vector3(location.x, location.y, location.z), 53 | description: ShopTranslations.openShop, 54 | range: shop.data.interactionRange, 55 | uid: `OSS-IC-${shop.name}-${i}`, 56 | debug: false, 57 | callback: (player: alt.Player) => createShopCallback(player, shop, location), 58 | }); 59 | }); 60 | }); 61 | } 62 | 63 | export function createShopCallback(player: alt.Player, shop: IShop, location: IShopLocation) { 64 | let currentShop = shop; 65 | let dataItems = []; 66 | let acceptsCard = location.shopAcceptsCard || false; 67 | 68 | for (const item of currentShop.data.items) { 69 | const currentItem = Athena.systems.inventory.factory.getBaseItem(item.dbName); 70 | let states = { 71 | name: currentItem.name, 72 | dbName: item.dbName, 73 | price: item.price, 74 | }; 75 | 76 | dataItems.push({ 77 | name: states.name, 78 | dbName: states.dbName, 79 | price: states.price, 80 | image: currentItem.icon, 81 | quantity: 1, 82 | }); 83 | } 84 | 85 | alt.emitClient(player, ShopEvents.OPEN_SHOP); 86 | 87 | alt.onClient(ShopEvents.REQUEST_SHOP_ITEMS, (player: alt.Player) => { 88 | Athena.webview.emit(player, ShopEvents.SET_ITEMS, dataItems, shop.shopType, shop.name, acceptsCard); 89 | }); 90 | } 91 | 92 | export function getRandomInt(min: number, max: number) { 93 | min = Math.ceil(min); 94 | max = Math.floor(max); 95 | return Math.floor(Math.random() * (max - min)) + min; 96 | } 97 | 98 | export function foodEffect(player: alt.Player, slot: number, type: 'inventory' | 'toolbar') { 99 | const propertyName = String(type); 100 | const data = Athena.document.character.get(player); 101 | const item = Athena.player.inventory.getAt(player, slot); 102 | 103 | if (typeof data === 'undefined' || typeof data[propertyName] === 'undefined' || typeof item === 'undefined') return; 104 | 105 | item.quantity = 1; 106 | Athena.player.inventory.sub(player, item); 107 | 108 | data.food += 25; 109 | } 110 | 111 | export function drinkEffect(player: alt.Player, slot: number, type: 'inventory' | 'toolbar') { 112 | const propertyName = String(type); 113 | const data = Athena.document.character.get(player); 114 | const item = Athena.player.inventory.getAt(player, slot); 115 | 116 | if (typeof data === 'undefined' || typeof data[propertyName] === 'undefined' || typeof item === 'undefined') return; 117 | 118 | item.quantity = 1; 119 | Athena.player.inventory.sub(player, item); 120 | data.water += 25; 121 | } 122 | 123 | export function drinkEffectAlcoholic(player: alt.Player, slot: number, type: 'inventory' | 'toolbar') { 124 | const propertyName = String(type); 125 | const data = Athena.document.character.get(player); 126 | const item = Athena.player.inventory.getAt(player, slot); 127 | 128 | if (typeof data === 'undefined' || typeof data[propertyName] === 'undefined' || typeof item === 'undefined') return; 129 | 130 | item.quantity = 1; 131 | Athena.player.inventory.sub(player, item); 132 | 133 | Athena.player.emit.setTimeCycleEffect(player, 'Drunk', 30000); 134 | } 135 | -------------------------------------------------------------------------------- /webview/OSS_ShopUI.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 185 | 386 | --------------------------------------------------------------------------------