UNPKG

@haworth/menu-store

Version:

A visual store and cart system for samp-node servers.

1,365 lines (1,189 loc) 56 kB
import { Player, TextDraw, Dialog, DialogStylesEnum, TextDrawEvent } from '@infernus/core'; /** * MenuStore v5.0 - TypeScript Edition * * A comprehensive shopping cart system for SA-MP servers using Infernus framework. * Features include: * - Dynamic item management with 3D model previews * - Shopping cart with pagination and quantity selection * - Customizable store themes and callbacks * - Responsive TextDraw-based UI with hover effects * - Multi-page navigation for both store items and cart * * @author Original: CaioTJF | TypeScript Port: cha5e369 * @version 0.0.1 * @framework Infernus Core * @license MIT */ // Configuration constants (immutable) const MS_CONFIG = { MAX_ITEMS_PER_PAGE: 6, MAX_ITEMS_CART_PER_PAGE: 4, MAX_ITEMS_CART: 20, MAX_ITEMS: 30, MAX_SIZE_DESCRIPTION: 7.5, INTERVAL_DOUBLECLICK: 300, DIALOG_ID: 23132, // Default texts DEFAULT_MONEY_SIGN: "$", DEFAULT_CONFIRM: "Buy", // Messages MSG_TITLE_DIALOG: "Choose quantity", MSG_DIALOG: "Enter the quantity you want to buy", WORD_LIMIT: "Limit", WORD_CLOSE: "Close", WORD_TOTAL: "Total", WORD_GO: "Go", WORD_CANCEL: "Cancel", WORD_CART: "Cart" } as const; // Colors configuration const MS_COLORS = { SELECT_TD: 0xFFFFFFFF, TEXT_MAIN: 0xFFFFFFFF, BACKGROUND_1: 0x00000080, BACKGROUND_2: 0x2C2C2CAA, BACKGROUND_ITEM: 0x3E3E3EFF, BACKGROUND_ITEM_SELECTED: 0xFFFFFF50, BACKGROUND_CONFIRM: 0x5EAF3AFF, BACKGROUND_MODEL: 0xFFFFFF50, CART_REMOVE_ITEM: 0xCC30A0FF, DESCRIPTION_NAME_ITEM: 0xFFFFFFFF, DESCRIPTION_TEXT: 0xFFFFFFAA, DESCRIPTION_BACKGROUND_MODEL: 0xFFFFFF50 } as const; const MSTORE_INVALID_ITEM_ID = -1; // Interfaces interface StoreItem { itemID: number; itemModel: number; itemPrice: number; itemStack: number; itemName: string; itemDescription: string; itemDescriptionBonus: number; itemLineJump: boolean; itemPreviewRot: [number, number, number, number]; } interface StoreInfo { totalItems: number; currentPage: number; currentCartPage: number; isOpen: boolean; storeID: string; moneySign: string; doubleClick: number; doubleClickRow: number; selected: number; toggleControll: boolean; } interface CartItem { itemID: number; itemPrice: number; itemAmount: number; } interface StoreTextDraws { // Global TextDraws backgrounds: [TextDraw | null, TextDraw | null]; next: TextDraw | null; previous: TextDraw | null; cartBackgrounds: [TextDraw | null, TextDraw | null]; cartBackgroundConfirm: TextDraw | null; cartCartName: TextDraw | null; cartNext: TextDraw | null; cartPrevious: TextDraw | null; // Player TextDraws itemName: (TextDraw | null)[]; itemPrice: (TextDraw | null)[]; itemModel: (TextDraw | null)[]; itemBackground: (TextDraw | null)[]; storeName: TextDraw | null; pagination: TextDraw | null; cartPagination: TextDraw | null; descriptionItemName: TextDraw | null; descriptionItemText: TextDraw | null; descriptionItemModel: TextDraw | null; descriptionBackground: [TextDraw | null, TextDraw | null]; cartItemBackground: (TextDraw | null)[]; cartItemName: (TextDraw | null)[]; cartItemClose: (TextDraw | null)[]; cartBuyName: TextDraw | null; cartTotal: TextDraw | null; } // Store callback type type StoreCallback = ( player: Player, purchased: boolean, itemID: number, model: number, totalPrice: number, amount: number, itemName: string ) => void; class MenuStoreManager { private playerItems: Map<number, StoreItem[]> = new Map(); private playerInfo: Map<number, StoreInfo> = new Map(); private playerCart: Map<number, CartItem[]> = new Map(); private playerTextDraws: Map<number, StoreTextDraws> = new Map(); private globalTextDraws: StoreTextDraws | null = null; private storeCallbacks: Map<string, StoreCallback> = new Map(); constructor() { // Do not auto-initialize events } // Method to initialize GameMode events public initializeGameModeEvents(): void { this.createGlobalTextDraws(); } // Method to initialize Player events public initializePlayerEvents(): void { TextDrawEvent.onPlayerClickGlobal(({ player, textDraw }) => { console.log(`[MenuStore Debug] Global TextDraw clicked by ${player.getName()}, textDraw: ${textDraw || 'null'}`); // Handle ESC (when textDraw is null) if (!textDraw) { const playerStoreInfo = this.playerInfo.get(player.id); if (playerStoreInfo && playerStoreInfo.isOpen) { console.log(`[MenuStore Debug] ESC detected for ${player.getName()}`); this.handlePlayerESC(player); } return; } // Handle clicks on global textdraws this.handleGlobalTextDrawClick(player, textDraw); }); TextDrawEvent.onPlayerClickPlayer(({ player, textDraw }) => { console.log(`[MenuStore Debug] Player TextDraw clicked by ${player.getName()}, textDraw: ${textDraw || 'null'}`); this.handlePlayerTextDrawClick(player, textDraw); }); } // Method to initialize a specific player public initializePlayerData(player: Player): void { this.initializePlayer(player); } // Method to clean up a specific player's data public cleanupPlayerData(player: Player): void { this.closeStore(player); // Remove all player data this.playerItems.delete(player.id); this.playerInfo.delete(player.id); this.playerCart.delete(player.id); this.playerTextDraws.delete(player.id); } private initializePlayer(player: Player): void { // Initialize items array this.playerItems.set(player.id, Array(MS_CONFIG.MAX_ITEMS).fill(null).map(() => ({ itemID: MSTORE_INVALID_ITEM_ID, itemModel: 0, itemPrice: 0, itemStack: 0, itemName: "", itemDescription: "", itemDescriptionBonus: 0.0, itemLineJump: true, itemPreviewRot: [0.0, 0.0, 0.0, 1.0] }))); // Initialize info this.playerInfo.set(player.id, { totalItems: 0, currentPage: 1, currentCartPage: 1, // Inicializar página del carrito isOpen: false, storeID: "", moneySign: "", doubleClick: 0, doubleClickRow: -1, selected: -1, toggleControll: true }); // Initialize cart - aumentar capacidad this.playerCart.set(player.id, Array(MS_CONFIG.MAX_ITEMS_CART).fill(null).map(() => ({ itemID: MSTORE_INVALID_ITEM_ID, itemPrice: 0, itemAmount: 0 }))); // Initialize TextDraws this.playerTextDraws.set(player.id, { backgrounds: [null, null], next: null, previous: null, cartBackgrounds: [null, null], cartBackgroundConfirm: null, cartCartName: null, cartNext: null, // Inicializar botones del carrito cartPrevious: null, itemName: Array(MS_CONFIG.MAX_ITEMS_PER_PAGE).fill(null), itemPrice: Array(MS_CONFIG.MAX_ITEMS_PER_PAGE).fill(null), itemModel: Array(MS_CONFIG.MAX_ITEMS_PER_PAGE).fill(null), itemBackground: Array(MS_CONFIG.MAX_ITEMS_PER_PAGE).fill(null), storeName: null, pagination: null, cartPagination: null, // Inicializar paginación del carrito descriptionItemName: null, descriptionItemText: null, descriptionItemModel: null, descriptionBackground: [null, null], cartItemBackground: Array(MS_CONFIG.MAX_ITEMS_CART_PER_PAGE).fill(null), cartItemName: Array(MS_CONFIG.MAX_ITEMS_CART_PER_PAGE).fill(null), cartItemClose: Array(MS_CONFIG.MAX_ITEMS_CART_PER_PAGE).fill(null), cartBuyName: null, cartTotal: null }); } private createGlobalTextDraws(): void { if (this.globalTextDraws) return; this.globalTextDraws = { backgrounds: [null, null], next: null, previous: null, cartBackgrounds: [null, null], cartBackgroundConfirm: null, cartCartName: null, cartNext: null, cartPrevious: null, itemName: [], itemPrice: [], itemModel: [], itemBackground: [], storeName: null, pagination: null, cartPagination: null, descriptionItemName: null, descriptionItemText: null, descriptionItemModel: null, descriptionBackground: [null, null], cartItemBackground: [], cartItemName: [], cartItemClose: [], cartBuyName: null, cartTotal: null }; // Background 1 this.globalTextDraws.backgrounds[0] = new TextDraw({ x: 145.5, y: 130.499969, text: "box" }).create() .setLetterSize(0.0, 30.198539) .setTextSize(322.0, 0.0) .setAlignment(1) .setColor(0xFFFFFFFF) .useBox(true) .setBoxColors(MS_COLORS.BACKGROUND_1) .setShadow(0) .setOutline(0) .setFont(1) .setProportional(true); // Background 2 this.globalTextDraws.backgrounds[1] = new TextDraw({ x: 153.0, y: 149.383575, text: "box" }).create() .setLetterSize(0.0, 26.226503) .setTextSize(315.0, 0.0) .setAlignment(1) .setColor(0xFFFFFFFF) .useBox(true) .setBoxColors(MS_COLORS.BACKGROUND_2) .setShadow(0) .setOutline(0) .setFont(1) .setProportional(true); // Next button this.globalTextDraws.next = new TextDraw({ x: 251.485229, y: 390.033355, text: "ld_beat:right" }).create() .setLetterSize(0.0, 0.0) .setTextSize(11.0, 13.0) .setAlignment(1) .setColor(MS_COLORS.TEXT_MAIN) .setShadow(0) .setOutline(0) .setFont(4) .setProportional(true) .setSelectable(true); // Previous button this.globalTextDraws.previous = new TextDraw({ x: 201.94522, y: 390.033355, text: "ld_beat:left" }).create() .setLetterSize(0.0, 0.0) .setTextSize(11.0, 13.0) .setAlignment(1) .setColor(MS_COLORS.TEXT_MAIN) .setShadow(0) .setOutline(0) .setFont(4) .setProportional(true) .setSelectable(true); // Cart Confirm Button - EL MÁS IMPORTANTE this.globalTextDraws.cartBackgroundConfirm = new TextDraw({ x: 444.89923, y: 390.233489, text: "box" }).create() .setLetterSize(0.0, 0.850001) .setTextSize(507.70169, 15.0) .setAlignment(1) .setColor(0xFFFFFFFF) .useBox(true) .setBoxColors(MS_COLORS.BACKGROUND_CONFIRM) .setShadow(0) .setOutline(0) .setFont(1) .setProportional(true) .setSelectable(true); this.createCartGlobalElements(); } private createCartGlobalElements(): void { if (!this.globalTextDraws) return; // Cart Background 1 this.globalTextDraws.cartBackgrounds[0] = new TextDraw({ x: 333.201141, y: 271.666656, text: "box" }).create() .setLetterSize(0.0, 14.450012) .setTextSize(516.0, 0.0) .setAlignment(1) .setColor(0xFFFFFFFF) .useBox(true) .setBoxColors(MS_COLORS.BACKGROUND_1) .setShadow(0) .setOutline(0) .setFont(1) .setProportional(true); // Cart Background 2 this.globalTextDraws.cartBackgrounds[1] = new TextDraw({ x: 341.0, y: 290.133575, text: "box" }).create() .setLetterSize(0.0, 10.134258) .setTextSize(508.0, 0.0) .setAlignment(1) .setColor(0xFFFFFFFF) .useBox(true) .setBoxColors(MS_COLORS.BACKGROUND_2) .setShadow(0) .setOutline(0) .setFont(1) .setProportional(true); // Cart Name this.globalTextDraws.cartCartName = new TextDraw({ x: 339.143798, y: 270.73352, text: MS_CONFIG.WORD_CART }).create() .setLetterSize(0.310357, 1.434998) .setTextSize(508.0, 0.0) .setAlignment(1) .setColor(MS_COLORS.TEXT_MAIN) .useBox(true) .setBoxColors(0xFFFFFF00) .setShadow(0) .setOutline(0) .setFont(2) .setProportional(true); // Cart Next button this.globalTextDraws.cartNext = new TextDraw({ x: 395.0, y: 371.0, text: "ld_beat:right" }).create() .setLetterSize(0.0, 0.0) .setTextSize(8.0, 10.0) .setAlignment(1) .setColor(MS_COLORS.TEXT_MAIN) .setShadow(0) .setOutline(0) .setFont(4) .setProportional(true) .setSelectable(true); // Cart Previous button this.globalTextDraws.cartPrevious = new TextDraw({ x: 355.0, y: 371.0, text: "ld_beat:left" }).create() .setLetterSize(0.0, 0.0) .setTextSize(8.0, 10.0) .setAlignment(1) .setColor(MS_COLORS.TEXT_MAIN) .setShadow(0) .setOutline(0) .setFont(4) .setProportional(true) .setSelectable(true); } private handleGlobalTextDrawClick(player: Player, textDraw: any): void { const playerStoreInfo = this.playerInfo.get(player.id); if (!playerStoreInfo || !playerStoreInfo.toggleControll) return; if (textDraw === this.globalTextDraws?.next) { if (playerStoreInfo.currentPage < this.getNumberOfPages(player.id)) { playerStoreInfo.currentPage++; this.setPage(player, playerStoreInfo.currentPage); this.unselectRow(player); } } else if (textDraw === this.globalTextDraws?.previous) { if (playerStoreInfo.currentPage > 1) { playerStoreInfo.currentPage--; this.setPage(player, playerStoreInfo.currentPage); this.unselectRow(player); } } else if (textDraw === this.globalTextDraws?.cartNext) { if (playerStoreInfo.currentCartPage < this.getCartNumberOfPages(player.id)) { playerStoreInfo.currentCartPage++; this.setCartPage(player, playerStoreInfo.currentCartPage); } } else if (textDraw === this.globalTextDraws?.cartPrevious) { if (playerStoreInfo.currentCartPage > 1) { playerStoreInfo.currentCartPage--; this.setCartPage(player, playerStoreInfo.currentCartPage); } } else if (textDraw === this.globalTextDraws?.cartBackgroundConfirm) { const itemsInCart = this.getTotalItemsInCart(player.id); if (itemsInCart > 0) { this.processPurchase(player); this.closeStore(player); } else { player.sendClientMessage(0xFF0000FF, "The cart is empty. Double-click an item to add it."); } } } private handlePlayerTextDrawClick(player: Player, textDraw: any): void { const playerStoreInfo = this.playerInfo.get(player.id); const textDraws = this.playerTextDraws.get(player.id); if (!playerStoreInfo || !textDraws || !playerStoreInfo.toggleControll) return; // Check item clicks for (let i = 0; i < MS_CONFIG.MAX_ITEMS_PER_PAGE; i++) { if (textDraw === textDraws.itemBackground[i]) { this.handleItemClick(player, i); return; } } // Check cart item clicks for (let row = 0; row < MS_CONFIG.MAX_ITEMS_CART_PER_PAGE; row++) { if (textDraw === textDraws.cartItemBackground[row]) { const realIndex = row + (MS_CONFIG.MAX_ITEMS_CART_PER_PAGE * (playerStoreInfo.currentCartPage - 1)); this.removeFromCart(player, realIndex); return; } } } private handleItemClick(player: Player, row: number): void { const playerStoreInfo = this.playerInfo.get(player.id); const items = this.playerItems.get(player.id); if (!playerStoreInfo || !items) return; const index = row + (MS_CONFIG.MAX_ITEMS_PER_PAGE * (playerStoreInfo.currentPage - 1)); const now = Date.now(); // Handle double click if ((now - playerStoreInfo.doubleClick) < MS_CONFIG.INTERVAL_DOUBLECLICK && playerStoreInfo.doubleClickRow === row) { if (items[index].itemStack > 1) { this.showQuantityDialog(player, index); } else { this.addToCart(player, items[index].itemID, items[index].itemPrice, items[index].itemStack); this.unselectRow(player); this.closeDescription(player); } playerStoreInfo.doubleClickRow = -1; playerStoreInfo.doubleClick = 0; return; } else { playerStoreInfo.doubleClickRow = row; playerStoreInfo.doubleClick = now; } // Handle selection if (playerStoreInfo.selected === row) { this.unselectRow(player); this.closeDescription(player); return; } else if (playerStoreInfo.selected !== -1) { this.unselectRow(player); } this.selectRow(player, row); this.openDescription(player, index); } private handlePlayerESC(player: Player): void { const playerStoreInfo = this.playerInfo.get(player.id); if (!playerStoreInfo || !playerStoreInfo.isOpen) return; console.log(`[MenuStore Debug] ESC handled for ${player.getName()}`); const callback = this.storeCallbacks.get(playerStoreInfo.storeID); if (callback) { callback(player, false, MSTORE_INVALID_ITEM_ID, 0, 0, 0, "CANCELLED"); } this.closeStore(player); } // Helper methods private getNumberOfPages(playerId: number): number { const playerStoreInfo = this.playerInfo.get(playerId); if (!playerStoreInfo) return 1; const totalItemsCount = this.getTotalValidItems(playerId); if (totalItemsCount <= MS_CONFIG.MAX_ITEMS_PER_PAGE) return 1; return Math.ceil(totalItemsCount / MS_CONFIG.MAX_ITEMS_PER_PAGE); } private getCartNumberOfPages(playerId: number): number { const totalCartItems = this.getTotalItemsInCart(playerId); if (totalCartItems <= MS_CONFIG.MAX_ITEMS_CART_PER_PAGE) return 1; return Math.ceil(totalCartItems / MS_CONFIG.MAX_ITEMS_CART_PER_PAGE); } private getTotalValidItems(playerId: number): number { const items = this.playerItems.get(playerId); if (!items) return 0; return items.filter(item => item.itemID !== MSTORE_INVALID_ITEM_ID).length; } private getTotalItemsInCart(playerId: number): number { const cart = this.playerCart.get(playerId); if (!cart) return 0; return cart.filter(item => item.itemID !== MSTORE_INVALID_ITEM_ID).length; } private getItemNameByID(playerId: number, id: number): string { const items = this.playerItems.get(playerId); if (!items) return "N/A"; for (const item of items) { if (item.itemID === id) { return item.itemName; } } return "N/A"; } private getItemModelByID(playerId: number, id: number): number { const items = this.playerItems.get(playerId); if (!items) return 0; for (const item of items) { if (item.itemID === id) { return item.itemModel; } } return 0; } private addToCart(player: Player, itemID: number, price: number, amount: number): void { const cart = this.playerCart.get(player.id); if (!cart) return; // Find empty slot in cart for (let i = 0; i < cart.length; i++) { const item = cart[i]; if (item.itemID === MSTORE_INVALID_ITEM_ID) { item.itemID = itemID; item.itemPrice = price; item.itemAmount = amount; console.log(`[MenuStore Debug] Added to cart: ${this.getItemNameByID(player.id, itemID)} (${amount}x) at slot ${i}`); // Update cart visuals this.setCartPage(player, this.playerInfo.get(player.id)?.currentCartPage || 1); this.updateCartTotalValue(player); // Notify player const itemName = this.getItemNameByID(player.id, itemID); player.sendClientMessage(0x00FF00FF, `Added to cart: ${amount}x ${itemName}`); break; } } } private removeFromCart(player: Player, row: number): void { const cart = this.playerCart.get(player.id); if (!cart || !cart[row]) return; cart[row].itemID = MSTORE_INVALID_ITEM_ID; cart[row].itemPrice = 0; cart[row].itemAmount = 0; this.updateCartTotalValue(player); this.sortCart(player); } private sortCart(player: Player): void { const cart = this.playerCart.get(player.id); if (!cart) return; // Compact cart: move valid items to the front, fill the rest with empty slots const validItems = cart.filter(item => item.itemID !== MSTORE_INVALID_ITEM_ID); const emptySlots = Array(MS_CONFIG.MAX_ITEMS_CART - validItems.length).fill({ itemID: MSTORE_INVALID_ITEM_ID, itemPrice: 0, itemAmount: 0 }); const newCart = validItems.concat(emptySlots); for (let i = 0; i < cart.length; i++) { cart[i] = { ...newCart[i] }; } this.updateCart(player); } private updateCart(player: Player): void { const playerStoreInfo = this.playerInfo.get(player.id); if (!playerStoreInfo) return; const totalCartPages = this.getCartNumberOfPages(player.id); if (playerStoreInfo.currentCartPage > totalCartPages && totalCartPages > 0) { playerStoreInfo.currentCartPage = totalCartPages; } else if (totalCartPages === 0) { playerStoreInfo.currentCartPage = 1; } this.setCartPage(player, playerStoreInfo.currentCartPage); this.updateCartPagination(player, playerStoreInfo.currentCartPage); // Actualizar visibilidad de botones de navegación del carrito if (totalCartPages > 1) { this.globalTextDraws?.cartPrevious?.show(player); this.globalTextDraws?.cartNext?.show(player); } else { this.globalTextDraws?.cartPrevious?.hide(player); this.globalTextDraws?.cartNext?.hide(player); } } private updateCartTotalValue(player: Player): void { const cart = this.playerCart.get(player.id); const textDraws = this.playerTextDraws.get(player.id); const playerStoreInfo = this.playerInfo.get(player.id); if (!cart || !textDraws || !playerStoreInfo) return; let totalValue = 0; for (const item of cart) { if (item.itemID !== MSTORE_INVALID_ITEM_ID) { totalValue += item.itemPrice * item.itemAmount; } } // Destruir textdraw existente del total textDraws.cartTotal?.destroy(); textDraws.cartTotal = null; // Crear nuevo textdraw del total textDraws.cartTotal = new TextDraw({ player: player, x: 339.200012, y: 386.949951, text: `${MS_CONFIG.WORD_TOTAL}: ${playerStoreInfo.moneySign}${totalValue}`, charset: player.charset }); textDraws.cartTotal .create() .setLetterSize(0.219999, 1.331665) .setTextSize(437.000000, 0.0) .setAlignment(1) .setColor(MS_COLORS.TEXT_MAIN) .setShadow(0) .setOutline(0) .setFont(2) .setProportional(true) .show(player); } private showQuantityDialog(player: Player, index: number): void { const playerStoreInfo = this.playerInfo.get(player.id); const items = this.playerItems.get(player.id); if (!playerStoreInfo || !items) return; playerStoreInfo.toggleControll = false; this.selectRow(player, playerStoreInfo.selected); const dialog = new Dialog({ style: DialogStylesEnum.INPUT, caption: MS_CONFIG.MSG_TITLE_DIALOG, info: `${MS_CONFIG.MSG_DIALOG} (${MS_CONFIG.WORD_LIMIT}: ${items[index].itemStack})`, button1: MS_CONFIG.WORD_GO, button2: MS_CONFIG.WORD_CANCEL }); dialog.show(player).then(({ response, inputText }) => { playerStoreInfo.toggleControll = true; this.closeDescription(player); if (!response || playerStoreInfo.selected === -1 || !inputText) { this.unselectRow(player); // Reactivate TextDraw selection mode if store is still open if (playerStoreInfo.isOpen) { player.selectTextDraw(MS_COLORS.SELECT_TD); } return; } const quantity = parseInt(inputText); if (isNaN(quantity) || quantity < 1 || quantity > items[index].itemStack) { this.unselectRow(player); // Reactivate TextDraw selection mode if store is still open if (playerStoreInfo.isOpen) { player.selectTextDraw(MS_COLORS.SELECT_TD); } return; } this.addToCart(player, items[index].itemID, items[index].itemPrice, quantity); this.unselectRow(player); // Reactivate TextDraw selection mode if store is still open if (playerStoreInfo.isOpen) { player.selectTextDraw(MS_COLORS.SELECT_TD); } }); } private setPage(player: Player, pageId: number): void { const items = this.playerItems.get(player.id); const textDraws = this.playerTextDraws.get(player.id); const playerStoreInfo = this.playerInfo.get(player.id); if (!items || !textDraws || !playerStoreInfo) return; const startIndex = MS_CONFIG.MAX_ITEMS_PER_PAGE * (pageId - 1); let currentRow = 0; // Remove existing item textdraws for (let i = 0; i < MS_CONFIG.MAX_ITEMS_PER_PAGE; i++) { textDraws.itemBackground[i]?.destroy(); textDraws.itemName[i]?.destroy(); textDraws.itemPrice[i]?.destroy(); textDraws.itemModel[i]?.destroy(); textDraws.itemBackground[i] = null; textDraws.itemName[i] = null; textDraws.itemPrice[i] = null; textDraws.itemModel[i] = null; } // Create new item textdraws for actual items for (let i = startIndex; i < Math.min(startIndex + MS_CONFIG.MAX_ITEMS_PER_PAGE, MS_CONFIG.MAX_ITEMS); i++) { const item = items[i]; if (item.itemID === MSTORE_INVALID_ITEM_ID || currentRow >= MS_CONFIG.MAX_ITEMS_PER_PAGE) continue; const baseY = 155.2 + (currentRow * 39.0); // Item background textDraws.itemBackground[currentRow] = new TextDraw({ player: player, x: 158.0, y: baseY, text: "box", charset: player.charset }); textDraws.itemBackground[currentRow]! .create() .setLetterSize(0.0, 3.3) .setTextSize(311.0, 22.0) .setAlignment(1) .setColor(0xFFFFFFFF) .useBox(true) .setBoxColors(MS_COLORS.BACKGROUND_ITEM) .setShadow(0) .setOutline(0) .setFont(1) .setProportional(true) .setSelectable(true) .show(player); // Item name textDraws.itemName[currentRow] = new TextDraw({ player: player, x: 190.0, y: baseY + 3.0, text: item.itemName, charset: player.charset }); textDraws.itemName[currentRow]! .create() .setLetterSize(0.231, 1.198) .setTextSize(310.0, 0.0) .setAlignment(1) .setColor(MS_COLORS.TEXT_MAIN) .useBox(true) .setBoxColors(0xFFFFFF00) .setShadow(0) .setOutline(0) .setFont(2) .setProportional(true) .show(player); // Item price textDraws.itemPrice[currentRow] = new TextDraw({ player: player, x: 311.1, y: baseY + 18.0, text: `${playerStoreInfo.moneySign}${item.itemPrice}`, charset: player.charset }); textDraws.itemPrice[currentRow]! .create() .setLetterSize(0.284, 1.227) .setTextSize(0.0, 0.0) .setAlignment(3) .setColor(MS_COLORS.TEXT_MAIN) .setShadow(0) .setOutline(0) .setFont(2) .setProportional(true) .show(player); // Item model (3D preview) textDraws.itemModel[currentRow] = new TextDraw({ player: player, x: 158.7, y: baseY + 1.0, text: "", charset: player.charset }); textDraws.itemModel[currentRow]! .create() .setLetterSize(0.0, 0.0) .setTextSize(29.0, 28.0) .setAlignment(1) .setColor(0xFFFFFFFF) .setShadow(0) .setOutline(0) .setBackgroundColors(MS_COLORS.BACKGROUND_MODEL) .setFont(5) .setProportional(false) .setPreviewModel(item.itemModel) .setPreviewRot(item.itemPreviewRot[0], item.itemPreviewRot[1], item.itemPreviewRot[2], item.itemPreviewRot[3]) .show(player); currentRow++; } // Update pagination textDraws.pagination?.destroy(); textDraws.pagination = null; if (playerStoreInfo.totalItems > MS_CONFIG.MAX_ITEMS_PER_PAGE) { const totalPages = this.getNumberOfPages(player.id); textDraws.pagination = new TextDraw({ player: player, x: 232.25, y: 388.98, text: `${pageId}/${totalPages}`, charset: player.charset }); textDraws.pagination .create() .setLetterSize(0.318, 1.454) .setTextSize(0.0, 0.0) .setAlignment(2) .setColor(MS_COLORS.TEXT_MAIN) .setShadow(0) .setOutline(0) .setFont(2) .setProportional(true) .show(player); } } private selectRow(player: Player, row: number): void { const playerStoreInfo = this.playerInfo.get(player.id); const textDraws = this.playerTextDraws.get(player.id); if (!playerStoreInfo || !textDraws) return; playerStoreInfo.selected = row; if (textDraws.itemBackground[row]) { textDraws.itemBackground[row]!.setBoxColors(MS_COLORS.BACKGROUND_ITEM_SELECTED); textDraws.itemBackground[row]!.hide(player); textDraws.itemBackground[row]!.show(player); } } private unselectRow(player: Player): void { const playerStoreInfo = this.playerInfo.get(player.id); const textDraws = this.playerTextDraws.get(player.id); if (!playerStoreInfo || !textDraws || playerStoreInfo.selected === -1) return; const row = playerStoreInfo.selected; if (textDraws.itemBackground[row]) { textDraws.itemBackground[row]!.setBoxColors(MS_COLORS.BACKGROUND_ITEM); textDraws.itemBackground[row]!.hide(player); textDraws.itemBackground[row]!.show(player); } playerStoreInfo.selected = -1; } private openDescription(player: Player, index: number): void { console.log(`Opening description ${index} for ${player.getName()}`); // Basic implementation - can be expanded later } private closeDescription(player: Player): void { console.log(`Closing description for ${player.getName()}`); // Basic implementation - can be expanded later } private createCartRow(player: Player, row: number): void { const cart = this.playerCart.get(player.id); const textDraws = this.playerTextDraws.get(player.id); const playerStoreInfo = this.playerInfo.get(player.id); if (!cart || !textDraws || !playerStoreInfo) return; const cartItem = cart[row]; if (cartItem.itemID === MSTORE_INVALID_ITEM_ID) return; // Calcular la posición visual en la página actual del carrito const pageStartIndex = (playerStoreInfo.currentCartPage - 1) * MS_CONFIG.MAX_ITEMS_CART_PER_PAGE; const visualRow = row - pageStartIndex; // Solo crear si está en la página actual if (visualRow < 0 || visualRow >= MS_CONFIG.MAX_ITEMS_CART_PER_PAGE) return; const baseY = 293.88 + (visualRow * 23.0); // Destruir textdraws existentes para esta fila visual textDraws.cartItemBackground[visualRow]?.destroy(); textDraws.cartItemName[visualRow]?.destroy(); textDraws.cartItemClose[visualRow]?.destroy(); // Cart item background textDraws.cartItemBackground[visualRow] = new TextDraw({ player: player, x: 344.0, y: baseY, text: "box", charset: player.charset }); textDraws.cartItemBackground[visualRow]! .create() .setLetterSize(0.0, 1.55) .setTextSize(505.0, 15.0) .setAlignment(1) .setColor(0xFFFFFFFF) .useBox(true) .setBoxColors(MS_COLORS.BACKGROUND_ITEM) .setShadow(0) .setOutline(0) .setFont(1) .setProportional(true) .setSelectable(true) .show(player); // Cart item name y cantidad const itemName = this.getItemNameByID(player.id, cartItem.itemID); const itemTotal = cartItem.itemPrice * cartItem.itemAmount; textDraws.cartItemName[visualRow] = new TextDraw({ player: player, x: 345.2, y: baseY + 0.7, text: `${cartItem.itemAmount}x ${itemName} - ${playerStoreInfo.moneySign}${itemTotal}`, charset: player.charset }); textDraws.cartItemName[visualRow]! .create() .setLetterSize(0.18, 1.0) .setTextSize(500.0, 0.0) .setAlignment(1) .setColor(MS_COLORS.TEXT_MAIN) .useBox(true) .setBoxColors(0xFFFFFF00) .setShadow(0) .setOutline(0) .setFont(2) .setProportional(true) .show(player); // Cart item close button (X) textDraws.cartItemClose[visualRow] = new TextDraw({ player: player, x: 502.0, y: baseY + 0.7, text: "X", charset: player.charset }); textDraws.cartItemClose[visualRow]! .create() .setLetterSize(0.25, 1.2) .setTextSize(0.0, 0.0) .setAlignment(3) .setColor(MS_COLORS.CART_REMOVE_ITEM) .setShadow(0) .setOutline(0) .setFont(1) .setProportional(true) .show(player); console.log(`[MenuStore Debug] Cart row ${visualRow} created for item ${itemName} (${cartItem.itemAmount}x)`); } private setCartPage(player: Player, pageId: number): void { const cart = this.playerCart.get(player.id); const textDraws = this.playerTextDraws.get(player.id); const playerStoreInfo = this.playerInfo.get(player.id); if (!cart || !textDraws || !playerStoreInfo) return; // Limpiar todos los textdraws del carrito for (let i = 0; i < MS_CONFIG.MAX_ITEMS_CART_PER_PAGE; i++) { textDraws.cartItemBackground[i]?.destroy(); textDraws.cartItemName[i]?.destroy(); textDraws.cartItemClose[i]?.destroy(); textDraws.cartItemBackground[i] = null; textDraws.cartItemName[i] = null; textDraws.cartItemClose[i] = null; } // Calcular índices para esta página const startIndex = (pageId - 1) * MS_CONFIG.MAX_ITEMS_CART_PER_PAGE; const endIndex = Math.min(startIndex + MS_CONFIG.MAX_ITEMS_CART_PER_PAGE, MS_CONFIG.MAX_ITEMS_CART); let visualRow = 0; // Mostrar items de esta página for (let i = startIndex; i < endIndex && visualRow < MS_CONFIG.MAX_ITEMS_CART_PER_PAGE; i++) { const cartItem = cart[i]; if (cartItem.itemID !== MSTORE_INVALID_ITEM_ID) { const baseY = 293.88 + (visualRow * 23.0); // Cart item background textDraws.cartItemBackground[visualRow] = new TextDraw({ player: player, x: 344.0, y: baseY, text: "box", charset: player.charset }); textDraws.cartItemBackground[visualRow]! .create() .setLetterSize(0.0, 1.55) .setTextSize(505.0, 15.0) .setAlignment(1) .setColor(0xFFFFFFFF) .useBox(true) .setBoxColors(MS_COLORS.BACKGROUND_ITEM) .setShadow(0) .setOutline(0) .setFont(1) .setProportional(true) .setSelectable(true) .show(player); // Cart item name y información const itemName = this.getItemNameByID(player.id, cartItem.itemID); const itemTotal = cartItem.itemPrice * cartItem.itemAmount; textDraws.cartItemName[visualRow] = new TextDraw({ player: player, x: 345.2, y: baseY + 0.7, text: `${cartItem.itemAmount}x ${itemName} - ${playerStoreInfo.moneySign}${itemTotal}`, charset: player.charset }); textDraws.cartItemName[visualRow]! .create() .setLetterSize(0.18, 1.0) .setTextSize(500.0, 0.0) .setAlignment(1) .setColor(MS_COLORS.TEXT_MAIN) .useBox(true) .setBoxColors(0xFFFFFF00) .setShadow(0) .setOutline(0) .setFont(2) .setProportional(true) .show(player); // Cart item close button (X) textDraws.cartItemClose[visualRow] = new TextDraw({ player: player, x: 502.0, y: baseY + 0.7, text: "X", charset: player.charset }); textDraws.cartItemClose[visualRow]! .create() .setLetterSize(0.25, 1.2) .setTextSize(0.0, 0.0) .setAlignment(3) .setColor(MS_COLORS.CART_REMOVE_ITEM) .setShadow(0) .setOutline(0) .setFont(1) .setProportional(true) .show(player); visualRow++; } } console.log(`[MenuStore Debug] Cart page ${pageId} set with ${visualRow} visible items`); } private updateCartPagination(player: Player, pageId: number): void { // Only destroy if exists, but DO NOT create a new one const textDraws = this.playerTextDraws.get(player.id); if (!textDraws) return; textDraws.cartPagination?.destroy(); textDraws.cartPagination = null; } private processPurchase(player: Player): void { const cart = this.playerCart.get(player.id); const playerStoreInfo = this.playerInfo.get(player.id); if (!cart || !playerStoreInfo) return; let totalPrice = 0; const purchasedItems: Array<{itemID: number, model: number, amount: number, price: number, name: string}> = []; // Calculate total and prepare purchase data for (const item of cart) { if (item.itemID !== MSTORE_INVALID_ITEM_ID) { const itemTotal = item.itemPrice * item.itemAmount; totalPrice += itemTotal; purchasedItems.push({ itemID: item.itemID, model: this.getItemModelByID(player.id, item.itemID), amount: item.itemAmount, price: itemTotal, name: this.getItemNameByID(player.id, item.itemID) }); } } // Execute callback for each purchased item const callback = this.storeCallbacks.get(playerStoreInfo.storeID); if (callback) { for (const item of purchasedItems) { callback(player, true, item.itemID, item.model, item.price, item.amount, item.name); } } // Clear cart this.clearCart(player); } private clearCart(player: Player): void { const cart = this.playerCart.get(player.id); if (!cart) return; for (let i = 0; i < cart.length; i++) { cart[i].itemID = MSTORE_INVALID_ITEM_ID; cart[i].itemPrice = 0; cart[i].itemAmount = 0; } } public closeStore(player: Player): void { const playerStoreInfo = this.playerInfo.get(player.id); const textDraws = this.playerTextDraws.get(player.id); if (!playerStoreInfo || !textDraws) return; playerStoreInfo.isOpen = false; playerStoreInfo.toggleControll = false; // Hide and destroy all player textdraws for (let i = 0; i < MS_CONFIG.MAX_ITEMS_PER_PAGE; i++) { textDraws.itemBackground[i]?.hide(player); textDraws.itemBackground[i]?.destroy(); textDraws.itemName[i]?.hide(player); textDraws.itemName[i]?.destroy(); textDraws.itemPrice[i]?.hide(player); textDraws.itemPrice[i]?.destroy(); textDraws.itemModel[i]?.hide(player); textDraws.itemModel[i]?.destroy(); textDraws.itemBackground[i] = null; textDraws.itemName[i] = null; textDraws.itemPrice[i] = null; textDraws.itemModel[i] = null; } for (let i = 0; i < MS_CONFIG.MAX_ITEMS_CART_PER_PAGE; i++) { textDraws.cartItemBackground[i]?.hide(player); textDraws.cartItemBackground[i]?.destroy(); textDraws.cartItemName[i]?.hide(player); textDraws.cartItemName[i]?.destroy(); textDraws.cartItemClose[i]?.hide(player); textDraws.cartItemClose[i]?.destroy(); textDraws.cartItemBackground[i] = null; textDraws.cartItemName[i] = null; textDraws.cartItemClose[i] = null; } // Hide and destroy other player textdraws textDraws.storeName?.hide(player); textDraws.storeName?.destroy(); textDraws.pagination?.hide(player); textDraws.pagination?.destroy(); textDraws.cartPagination?.hide(player); textDraws.cartPagination?.destroy(); textDraws.descriptionItemName?.hide(player); textDraws.descriptionItemName?.destroy(); textDraws.descriptionItemText?.hide(player); textDraws.descriptionItemText?.destroy(); textDraws.descriptionItemModel?.hide(player); textDraws.descriptionItemModel?.destroy(); textDraws.cartBuyName?.hide(player); textDraws.cartBuyName?.destroy(); textDraws.cartTotal?.hide(player); textDraws.cartTotal?.destroy(); for (let i = 0; i < 2; i++) { textDraws.descriptionBackground[i]?.hide(player); textDraws.descriptionBackground[i]?.destroy(); textDraws.descriptionBackground[i] = null; } // Hide global textdraws this.globalTextDraws?.backgrounds[0]?.hide(player); this.globalTextDraws?.backgrounds[1]?.hide(player); this.globalTextDraws?.next?.hide(player); this.globalTextDraws?.previous?.hide(player); this.globalTextDraws?.cartBackgrounds[0]?.hide(player); this.globalTextDraws?.cartBackgrounds[1]?.hide(player); this.globalTextDraws?.cartBackgroundConfirm?.hide(player); this.globalTextDraws?.cartCartName?.hide(player); this.globalTextDraws?.cartNext?.hide(player); this.globalTextDraws?.cartPrevious?.hide(player); // Reset player state playerStoreInfo.currentPage = 1; playerStoreInfo.currentCartPage = 1; playerStoreInfo.selected = -1; playerStoreInfo.doubleClick = 0; playerStoreInfo.doubleClickRow = -1; playerStoreInfo.toggleControll = true; // Clear cart this.clearCart(player); // Cancelar modo de selección de TextDraw player.cancelSelectTextDraw(); } public addItem( player: Player, itemID: number, modelID: number, name: string, price: number, description: string = "", descriptionSize: number = MS_CONFIG.MAX_SIZE_DESCRIPTION, descriptionLineJump: boolean = true, stack: number = 1, rotX: number = 0.0, rotY: number = 0.0, rotZ: number = 0.0, zoom: number = 1.0 ): boolean { const items = this.playerItems.get(player.id); const playerStoreInfo = this.playerInfo.get(player.id); if (!items || !playerStoreInfo) return false; // Find empty slot for (let i = 0; i < MS_CONFIG.MAX_ITEMS; i++) { if (items[i].itemID === MSTORE_INVALID_ITEM_ID) { items[i].itemID = itemID; items[i].itemModel = modelID; items[i].itemName = name; items[i].itemPrice = price; items[i].itemDescription = description; items[i].itemDescriptionBonus = descriptionSize; items[i].itemLineJump = descriptionLineJump;