@haworth/menu-store
Version:
A visual store and cart system for samp-node servers.
1,365 lines (1,189 loc) • 56 kB
text/typescript
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;