CheezPuff

[Free] Jailbreak Knives Shop Abilities v1.2

Jul 12th, 2024 (edited)
1,601
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Pawn 48.52 KB | None | 0 0
  1. /*
  2.     Frag JailBreak - Knives Shop v1.7
  3.     Fixed:
  4.     - v1.1-v1.6 fixes preserved
  5.     - v1.7: Removed gravity/speed from enum and knives array.
  6.             Removed unused plugin_natives, register_natives stubs.
  7.             Removed duplicate is_user_connected guards.
  8.             ShowInventory now shows HTML MOTD (not menu).
  9.             Top Knife Leaderboard moved to main menu as separate item.
  10.             is_user_connected guard added to all Show* functions.
  11.     By CheezPuff (Ariel) | Discord: CheezPuff#7720
  12. */
  13.  
  14. #include <amxmodx>
  15. #include <amxmisc>
  16. #include <cstrike>
  17. #include <fun>
  18. #include <hamsandwich>
  19. #include <fakemeta>
  20. #include <nvault>
  21.  
  22. #define PLUGIN      "JailBreak Knives Shop"
  23. #define VERSION     "1.7"
  24. #define AUTHOR      "CheezPuff"
  25.  
  26. #define MAX_PLAYERS     32
  27. #define PREVIEW_TIME    5.0
  28. #define BASE_HEALTH     100
  29. #define BASE_ARMOR      0
  30. #define MAX_ARMOR       200
  31.  
  32. #define TASK_PREVIEW    7000    // 7000 + id
  33. #define TASK_HPREGEN    8000    // 8000 + id
  34.  
  35. enum _:eKnife {
  36.     KNIFE_MODEL[64],
  37.     KNIFE_NAME[32],
  38.     KNIFE_PRICE,
  39.     KNIFE_LEVEL_REQ,
  40.     Float:KNIFE_CASH_BONUS,
  41.     KNIFE_HEALTH_BONUS,
  42.     KNIFE_ARMOR_BONUS,
  43.     Float:KNIFE_DAMAGE_MULT,
  44.     KNIFE_HP_REGEN,
  45.     KNIFE_HP_REGEN_INTERVAL
  46. }
  47.  
  48. // Format: model, name, price, level, cash%, hp, armor, dmg_mult, hp_regen, regen_interval
  49. //
  50. // Progression logic:
  51. //   Knife 0 — Normal Boxer    : nothing, default
  52. //   Knife 1 — Steel Blade     : basic HP + tiny DMG
  53. //   Knife 2 — Hunter Knife    : more HP + DMG, small armor
  54. //   Knife 3 — Shadow Knife    : HP + armor + DMG + cash bonus
  55. //   Knife 4 — Blood Knife     : everything from 3 + HP regen begins
  56. //   Knife 5 — Soul Reaper     : everything maxed + strong regen + cash
  57. //   Knife 6 — Legendary Blade : VIP only — god tier
  58. //
  59. new const g_Knives[][eKnife] = {
  60.     // model                          name               price    lvl  cash%  hp   arm  dmg    regen  interval
  61.     {"models/v_knife.mdl",          "Normal Boxer",    0,       0,   0.0,   0,   0,   1.0,   0,     0},
  62.     {"models/knives/v_knife1.mdl",  "Steel Blade",     10000,   3,   0.0,   15,  0,   1.15,  0,     0},
  63.     {"models/knives/v_knife2.mdl",  "Hunter Knife",    30000,   7,   0.0,   25,  15,  1.25,  0,     0},
  64.     {"models/knives/v_knife3.mdl",  "Shadow Knife",    75000,   12,  0.10,  35,  30,  1.35,  0,     0},
  65.     {"models/knives/v_knife4.mdl",  "Blood Knife",     150000,  18,  0.20,  45,  45,  1.45,  2,     5},
  66.     {"models/knives/v_knife5.mdl",  "Soul Reaper",     300000,  25,  0.30,  60,  60,  1.55,  4,     4},
  67.     {"models/knives/v_knife6.mdl",  "Legendary Blade", 750000,  35,  0.50,  80,  80,  1.75,  7,     3}
  68. }
  69.  
  70.  
  71. #define KNIFE_COUNT 7
  72.  
  73. // VIP knife index — VIP players can buy this knife regardless of sequential lock
  74. #define VIP_KNIFE_INDEX     (KNIFE_COUNT - 1)   // Legendary Blade
  75. #define VIP_FLAG            ADMIN_LEVEL_H
  76.  
  77. // v1.2: bitmask of which knives have valid (existing) model files
  78. // set during plugin_precache — knives with missing models are skipped everywhere
  79. new g_iKnifeValid   // bit N = knife N model exists and was precached
  80.  
  81. new g_iPlayerCash[MAX_PLAYERS + 1]
  82. new g_iPlayerLevel[MAX_PLAYERS + 1]
  83. new g_iPlayerXP[MAX_PLAYERS + 1]
  84. new g_iPlayerEquippedKnife[MAX_PLAYERS + 1]
  85. new g_iPlayerOwnedKnives[MAX_PLAYERS + 1]
  86. new g_iPlayerPreviewingKnife[MAX_PLAYERS + 1]
  87. new g_iPlayerViewingKnife[MAX_PLAYERS + 1]
  88.  
  89. new g_vault
  90.  
  91. // -------------------------------------------------------------------------
  92.  
  93. public plugin_precache() {
  94.     g_iKnifeValid = 0
  95.  
  96.     for(new i = 0; i < KNIFE_COUNT; i++) {
  97.         // Knife 0 is the default CS knife — always exists, no file check needed
  98.         if(i == 0) {
  99.             precache_model(g_Knives[0][KNIFE_MODEL])
  100.             g_iKnifeValid |= (1 << 0)
  101.             continue
  102.         }
  103.  
  104.         // For custom knives: only precache if the file actually exists on disk
  105.         if(file_exists(g_Knives[i][KNIFE_MODEL])) {
  106.             precache_model(g_Knives[i][KNIFE_MODEL])
  107.             g_iKnifeValid |= (1 << i)
  108.         } else {
  109.             // Log once at startup so admin knows what is missing — no crash
  110.             log_amx("[KnifeShop] WARNING: model not found, knife disabled: %s", g_Knives[i][KNIFE_MODEL])
  111.         }
  112.     }
  113. }
  114.  
  115. public plugin_init() {
  116.     register_plugin(PLUGIN, VERSION, AUTHOR)
  117.  
  118.     g_vault = nvault_open("knife_shop_vault")
  119.     if(g_vault == INVALID_HANDLE)
  120.         set_fail_state("[KnifeShop] Failed to open vault!")
  121.  
  122.     register_clcmd("say /knife",      "CmdKnifeShop")
  123.     register_clcmd("say_team /knife", "CmdKnifeShop")
  124.  
  125.     register_concmd("amx_knivetop",   "CmdKnifeTop",   ADMIN_RCON, "- Show top 5 knife players")
  126.     register_concmd("amx_knifereset", "CmdKnifeReset", ADMIN_RCON, "- Reset all player knife data")
  127.     register_concmd("amx_givecash",   "CmdGiveCash",   ADMIN_RCON, "<player> <amount> - Give cash to player")
  128.     register_concmd("amx_givelevel",  "CmdGiveLevel",  ADMIN_RCON, "<player> <level> - Set player level")
  129.  
  130.     register_event("DeathMsg",  "EventPlayerKill",  "a")
  131.     register_event("HLTV",      "EventRoundStart",  "a", "1=0", "2=0")
  132.  
  133.     RegisterHam(Ham_TakeDamage, "player", "fw_TakeDamage")
  134.     register_forward(FM_PlayerPostThink, "fw_PlayerPostThink")
  135.  
  136.     set_task(120.0, "Task_KnifeAd", _, _, _, "b")
  137.  
  138.     new statsMenuId = register_menuid("Knife Stats Menu")
  139.     register_menucmd(statsMenuId, MENU_KEY_0, "knife_stats_menu_handler")
  140. }
  141.  
  142. public plugin_end() {
  143.     nvault_close(g_vault)
  144. }
  145.  
  146. // -------------------------------------------------------------------------
  147. // Helper: is this knife usable (valid model + owned)
  148.  
  149. stock bool:IsKnifeValid(knife_id) {
  150.     return bool:(g_iKnifeValid & (1 << knife_id))
  151. }
  152.  
  153. // -------------------------------------------------------------------------
  154. // Vault
  155.  
  156. stock LoadPlayerData(id) {
  157.     new authid[32]
  158.     get_user_authid(id, authid, charsmax(authid))
  159.  
  160.     new data[128], szCash[16], szLevel[8], szXP[8], szEquipped[4], szOwned[16]
  161.     if(nvault_get(g_vault, authid, data, charsmax(data))) {
  162.         parse(data,
  163.             szCash,     charsmax(szCash),
  164.             szLevel,    charsmax(szLevel),
  165.             szXP,       charsmax(szXP),
  166.             szEquipped, charsmax(szEquipped),
  167.             szOwned,    charsmax(szOwned))
  168.  
  169.         g_iPlayerCash[id]          = str_to_num(szCash)
  170.         g_iPlayerLevel[id]         = str_to_num(szLevel)
  171.         g_iPlayerXP[id]            = str_to_num(szXP)
  172.         g_iPlayerEquippedKnife[id] = str_to_num(szEquipped)
  173.         g_iPlayerOwnedKnives[id]   = str_to_num(szOwned)
  174.     } else {
  175.         g_iPlayerCash[id]          = 0
  176.         g_iPlayerLevel[id]         = 1
  177.         g_iPlayerXP[id]            = 0
  178.         g_iPlayerEquippedKnife[id] = 0
  179.         g_iPlayerOwnedKnives[id]   = (1 << 0)
  180.     }
  181.  
  182.     // Safety: if the player's equipped knife has no valid model, fall back to knife 0
  183.     if(!IsKnifeValid(g_iPlayerEquippedKnife[id]))
  184.         g_iPlayerEquippedKnife[id] = 0
  185. }
  186.  
  187. stock SavePlayerData(id) {
  188.     new authid[32]
  189.     get_user_authid(id, authid, charsmax(authid))
  190.  
  191.     new data[128]
  192.     formatex(data, charsmax(data), "%d %d %d %d %d",
  193.         g_iPlayerCash[id],
  194.         g_iPlayerLevel[id],
  195.         g_iPlayerXP[id],
  196.         g_iPlayerEquippedKnife[id],
  197.         g_iPlayerOwnedKnives[id])
  198.  
  199.     nvault_set(g_vault, authid, data)
  200. }
  201.  
  202. stock bool:IsKnifeOwned(id, knife_id) {
  203.     return bool:(g_iPlayerOwnedKnives[id] & (1 << knife_id))
  204. }
  205.  
  206. stock AddOwnedKnife(id, knife_id) {
  207.     g_iPlayerOwnedKnives[id] |= (1 << knife_id)
  208. }
  209.  
  210. // -------------------------------------------------------------------------
  211.  
  212. public client_putinserver(id) {
  213.     LoadPlayerData(id)
  214.     g_iPlayerPreviewingKnife[id] = -1
  215.     g_iPlayerViewingKnife[id]    = 0
  216.  
  217.     // Welcome message after 5 seconds (give time to fully connect)
  218.     set_task(5.0, "Task_WelcomeMsg", id)
  219. }
  220.  
  221. public client_disconnected(id) {
  222.     remove_task(TASK_PREVIEW + id)
  223.     remove_task(TASK_HPREGEN + id)
  224.     SavePlayerData(id)
  225. }
  226.  
  227.  
  228. public Task_WelcomeMsg(id) {
  229.     if(!is_user_connected(id)) return
  230.     client_print_color(id, print_team_default, "^4[Knife Shop] ^1Welcome! Type ^3/knife ^1to open the knives shop.")
  231.     client_print_color(id, print_team_default, "^4[Knife Shop] ^1Cash: ^3$%d ^1| Level: ^3%d ^1| Equipped: ^3%s",
  232.         g_iPlayerCash[id], g_iPlayerLevel[id], g_Knives[g_iPlayerEquippedKnife[id]][KNIFE_NAME])
  233. }
  234.  
  235. public Task_KnifeAd() {
  236.     client_print_color(0, print_team_default, "^4[Knife Shop] ^1Want a better knife? Type ^3/knife ^1to browse and purchase!")
  237. }
  238.  
  239. // -------------------------------------------------------------------------
  240.  
  241. public fw_PlayerPostThink(id) {
  242.     if(!is_user_alive(id)) return FMRES_IGNORED
  243.  
  244.     if(get_user_weapon(id) == CSW_KNIFE) {
  245.         new knife_id = (g_iPlayerPreviewingKnife[id] != -1) ? g_iPlayerPreviewingKnife[id] : g_iPlayerEquippedKnife[id]
  246.         // Only apply custom model if it was successfully precached
  247.         if(IsKnifeValid(knife_id))
  248.             set_pev(id, pev_viewmodel2, g_Knives[knife_id][KNIFE_MODEL])
  249.     }
  250.  
  251.     return FMRES_IGNORED
  252. }
  253.  
  254. public fw_TakeDamage(victim, inflictor, attacker, Float:damage, damage_type) {
  255.     if(!is_user_connected(attacker) || get_user_weapon(attacker) != CSW_KNIFE)
  256.         return HAM_IGNORED
  257.  
  258.     new knife_id = g_iPlayerEquippedKnife[attacker]
  259.     if(!IsKnifeValid(knife_id)) return HAM_IGNORED
  260.  
  261.     // Multiply damage by knife multiplier (1.0 = no change, 1.5 = +50%)
  262.     new Float:new_damage = damage * g_Knives[knife_id][KNIFE_DAMAGE_MULT]
  263.     SetHamParamFloat(4, new_damage)
  264.  
  265.     return HAM_HANDLED
  266. }
  267.  
  268. // -------------------------------------------------------------------------
  269.  
  270. public EventRoundStart() {
  271.     for(new id = 1; id <= MAX_PLAYERS; id++)
  272.         if(is_user_connected(id) && is_user_alive(id))
  273.             ApplyKnifeAbilities(id)
  274. }
  275.  
  276. stock ApplyKnifeAbilities(id) {
  277.     new knife_id = g_iPlayerEquippedKnife[id]
  278.     if(!IsKnifeValid(knife_id)) knife_id = 0
  279.  
  280.     new newHealth = BASE_HEALTH + g_Knives[knife_id][KNIFE_HEALTH_BONUS]
  281.     set_user_health(id, newHealth)
  282.  
  283.     new newArmor = BASE_ARMOR + g_Knives[knife_id][KNIFE_ARMOR_BONUS]
  284.     if(newArmor > MAX_ARMOR) newArmor = MAX_ARMOR
  285.     set_user_armor(id, newArmor)
  286.  
  287.     remove_task(TASK_HPREGEN + id)
  288.     if(g_Knives[knife_id][KNIFE_HP_REGEN] > 0 && g_Knives[knife_id][KNIFE_HP_REGEN_INTERVAL] > 0)
  289.         set_task(float(g_Knives[knife_id][KNIFE_HP_REGEN_INTERVAL]), "Task_HPRegen", TASK_HPREGEN + id, _, _, "b")
  290. }
  291.  
  292. public Task_HPRegen(taskid) {
  293.     new id = taskid - TASK_HPREGEN
  294.     if(!is_user_connected(id) || !is_user_alive(id)) {
  295.         remove_task(taskid)
  296.         return
  297.     }
  298.  
  299.     new knife_id = g_iPlayerEquippedKnife[id]
  300.     if(!IsKnifeValid(knife_id) || g_Knives[knife_id][KNIFE_HP_REGEN] <= 0) {
  301.         remove_task(taskid)
  302.         return
  303.     }
  304.  
  305.     new hp    = get_user_health(id)
  306.     new maxHp = BASE_HEALTH + g_Knives[knife_id][KNIFE_HEALTH_BONUS]
  307.  
  308.     if(hp < maxHp) {
  309.         new newHp = hp + g_Knives[knife_id][KNIFE_HP_REGEN]
  310.         if(newHp > maxHp) newHp = maxHp
  311.         set_user_health(id, newHp)
  312.     }
  313. }
  314.  
  315. public EventPlayerKill() {
  316.     new killer = read_data(1)
  317.     new victim = read_data(2)
  318.  
  319.     if(killer == victim || !is_user_connected(killer)) return
  320.  
  321.     new xp_gain = (cs_get_user_team(victim) == CS_TEAM_CT) ? 20 : 10
  322.  
  323.     g_iPlayerCash[killer] += 100
  324.     g_iPlayerXP[killer]   += xp_gain
  325.  
  326.     client_print_color(killer, print_team_default, "^4[Knife] ^3+%d XP ^1and ^3+$100 ^1for the kill!", xp_gain)
  327.  
  328.     if(g_iPlayerXP[killer] >= g_iPlayerLevel[killer] * 100) {
  329.         g_iPlayerXP[killer] -= g_iPlayerLevel[killer] * 100
  330.         g_iPlayerLevel[killer]++
  331.  
  332.         new name[32]
  333.         get_user_name(killer, name, charsmax(name))
  334.         client_print_color(0, print_team_default, "^4[Knife] ^3%s ^1leveled up to level ^3%d^1!", name, g_iPlayerLevel[killer])
  335.     }
  336.  
  337.     new knife_id = g_iPlayerEquippedKnife[killer]
  338.     if(IsKnifeValid(knife_id)) {
  339.         new Float:cash_bonus = g_Knives[knife_id][KNIFE_CASH_BONUS]
  340.         if(cash_bonus > 0.0) {
  341.             new bonus_cash = floatround(100.0 * cash_bonus)
  342.             g_iPlayerCash[killer] += bonus_cash
  343.             client_print_color(killer, print_team_default, "^4[Knife] ^3+$%d ^1knife bonus!", bonus_cash)
  344.         }
  345.     }
  346.  
  347.     SavePlayerData(killer)
  348. }
  349.  
  350. // -------------------------------------------------------------------------
  351. // XP Progress bar helper — returns e.g. "[####......] 40/100"
  352.  
  353. stock BuildXPBar(id, bar[], len) {
  354.     new xp       = g_iPlayerXP[id]
  355.     new xpNeeded = g_iPlayerLevel[id] * 100
  356.     new filled   = (xpNeeded > 0) ? (xp * 10 / xpNeeded) : 10
  357.     if(filled > 10) filled = 10
  358.  
  359.     new barStr[16]
  360.     barStr[0] = '['
  361.     for(new i = 1; i <= 10; i++)
  362.         barStr[i] = (i <= filled) ? '#' : '.'
  363.     barStr[11] = ']'
  364.     barStr[12] = EOS
  365.  
  366.     formatex(bar, len, "%s %d/%d XP", barStr, xp, xpNeeded)
  367. }
  368.  
  369. // -------------------------------------------------------------------------
  370. // Main Menu
  371.  
  372. public CmdKnifeShop(id) {
  373.     ShowMainMenu(id)
  374.     return PLUGIN_HANDLED
  375. }
  376.  
  377. ShowMainMenu(id) {
  378.     if(!is_user_connected(id)) return
  379.  
  380.     new xpBar[32]
  381.     BuildXPBar(id, xpBar, charsmax(xpBar))
  382.  
  383.     new title[160]
  384.     formatex(title, charsmax(title),
  385.         "\r[JailBreak] \wKnives Shop^n\wCash: \y$%d \w| Level: \y%d^n\w%s^nEquipped: \y%s",
  386.         g_iPlayerCash[id], g_iPlayerLevel[id],
  387.         xpBar,
  388.         g_Knives[g_iPlayerEquippedKnife[id]][KNIFE_NAME])
  389.  
  390.     new menu = menu_create(title, "main_menu_handler")
  391.     menu_additem(menu, "\wView Knives")
  392.     menu_additem(menu, "\wMy Inventory \d| Top Knife Leaderboard")
  393.     menu_additem(menu, "\wShop Information")
  394.     menu_additem(menu, "\wPreview Knife")
  395.     menu_setprop(menu, MPROP_EXIT, MEXIT_ALL)
  396.     menu_display(id, menu, 0)
  397. }
  398.  
  399. public main_menu_handler(id, menu, item) {
  400.     menu_destroy(menu)
  401.     if(item == MENU_EXIT) return PLUGIN_HANDLED
  402.  
  403.     switch(item) {
  404.         case 0: ShowKnifesList(id)
  405.         case 1: ShowInventoryMenu(id)
  406.         case 2: ShowShopInfo(id)
  407.         case 3: ShowPreviewMenu(id)
  408.     }
  409.     return PLUGIN_HANDLED
  410. }
  411.  
  412. // -------------------------------------------------------------------------
  413. // Knives List — skips knives whose model is missing
  414.  
  415. ShowKnifesList(id) {
  416.     if(!is_user_connected(id)) return
  417.     new title[64]
  418.     formatex(title, charsmax(title), "\yKnives List \w| Cash: $%d | Lvl: %d",
  419.         g_iPlayerCash[id], g_iPlayerLevel[id])
  420.  
  421.     new menu = menu_create(title, "knives_list_handler")
  422.  
  423.     for(new i = 0; i < KNIFE_COUNT; i++) {
  424.         new item_text[96], item_info[4]
  425.         new bool:owned    = IsKnifeOwned(id, i)
  426.         new bool:valid    = IsKnifeValid(i)
  427.         // Sequential lock: must own the previous knife first (except knife 0)
  428.         new bool:prevOwned = (i == 0) ? true : IsKnifeOwned(id, i - 1)
  429.         // VIP bypass for last knife
  430.         new bool:isVIP = bool:(get_user_flags(id) & VIP_FLAG)
  431.         if(isVIP && i == VIP_KNIFE_INDEX) prevOwned = true
  432.  
  433.         if(owned) {
  434.             formatex(item_text, charsmax(item_text), "\y%s \d[Owned]\w | $%d | Lvl %d",
  435.                 g_Knives[i][KNIFE_NAME],
  436.                 g_Knives[i][KNIFE_PRICE],
  437.                 g_Knives[i][KNIFE_LEVEL_REQ])
  438.         } else if(!valid) {
  439.             // Model missing — show VIP tag if applicable
  440.             if(isVIP && i == VIP_KNIFE_INDEX)
  441.                 formatex(item_text, charsmax(item_text), "\d%s | Model missing \y[VIP]", g_Knives[i][KNIFE_NAME])
  442.             else
  443.                 formatex(item_text, charsmax(item_text), "\d%s | Model missing", g_Knives[i][KNIFE_NAME])
  444.         } else if(!prevOwned) {
  445.             formatex(item_text, charsmax(item_text), "\d%s | Buy previous first",
  446.                 g_Knives[i][KNIFE_NAME])
  447.         } else if(g_iPlayerLevel[id] < g_Knives[i][KNIFE_LEVEL_REQ]) {
  448.             formatex(item_text, charsmax(item_text), "\d%s | Lvl %d required",
  449.                 g_Knives[i][KNIFE_NAME], g_Knives[i][KNIFE_LEVEL_REQ])
  450.         } else {
  451.             // Available to buy
  452.             if(isVIP && i == VIP_KNIFE_INDEX)
  453.                 formatex(item_text, charsmax(item_text), "\w%s | \y$%d \r[VIP KNIFE]\w | Lvl %d",
  454.                     g_Knives[i][KNIFE_NAME], g_Knives[i][KNIFE_PRICE], g_Knives[i][KNIFE_LEVEL_REQ])
  455.             else
  456.                 formatex(item_text, charsmax(item_text), "\w%s | \y$%d\w | Lvl %d",
  457.                     g_Knives[i][KNIFE_NAME], g_Knives[i][KNIFE_PRICE], g_Knives[i][KNIFE_LEVEL_REQ])
  458.         }
  459.  
  460.         formatex(item_info, charsmax(item_info), "%d", i)
  461.         menu_additem(menu, item_text, item_info)
  462.     }
  463.  
  464.     menu_setprop(menu, MPROP_EXIT, MEXIT_ALL)
  465.     menu_display(id, menu, 0)
  466. }
  467.  
  468. public knives_list_handler(id, menu, item) {
  469.     if(item == MENU_EXIT) {
  470.         menu_destroy(menu)
  471.         ShowMainMenu(id)
  472.         return PLUGIN_HANDLED
  473.     }
  474.  
  475.     new item_info[4], dummy[64], access, callback
  476.     menu_item_getinfo(menu, item, access, item_info, charsmax(item_info), dummy, charsmax(dummy), callback)
  477.     menu_destroy(menu)
  478.  
  479.     new knife_id = str_to_num(item_info)
  480.     if(knife_id < 0) {
  481.         ShowMainMenu(id)
  482.         return PLUGIN_HANDLED
  483.     }
  484.  
  485.     // Sequential lock check — block entry if previous not owned
  486.     if(knife_id > 0 && !IsKnifeOwned(id, knife_id - 1)) {
  487.         client_print_color(id, print_team_default, "^4[Knife] ^1You must purchase ^3%s ^1first!", g_Knives[knife_id - 1][KNIFE_NAME])
  488.         ShowKnifesList(id)
  489.         return PLUGIN_HANDLED
  490.     }
  491.  
  492.     // Level check
  493.     if(g_iPlayerLevel[id] < g_Knives[knife_id][KNIFE_LEVEL_REQ]) {
  494.         client_print_color(id, print_team_default, "^4[Knife] ^1You need level ^3%d ^1to view this knife.", g_Knives[knife_id][KNIFE_LEVEL_REQ])
  495.         ShowKnifesList(id)
  496.         return PLUGIN_HANDLED
  497.     }
  498.  
  499.     ShowKnifeDetails(id, knife_id)
  500.     return PLUGIN_HANDLED
  501. }
  502.  
  503. // -------------------------------------------------------------------------
  504. // Knife Details
  505.  
  506. ShowKnifeDetails(id, knife_id) {
  507.     if(!is_user_connected(id)) return
  508.     g_iPlayerViewingKnife[id] = knife_id
  509.  
  510.     new bool:owned     = IsKnifeOwned(id, knife_id)
  511.     new bool:valid     = IsKnifeValid(knife_id)
  512.     new bool:prevOwned = (knife_id == 0) ? true : IsKnifeOwned(id, knife_id - 1)
  513.     new bool:levelOk   = (g_iPlayerLevel[id] >= g_Knives[knife_id][KNIFE_LEVEL_REQ])
  514.     // VIP players bypass sequential lock for the VIP knife (last knife)
  515.     new bool:isVIP     = bool:(get_user_flags(id) & VIP_FLAG)
  516.     if(isVIP && knife_id == VIP_KNIFE_INDEX) prevOwned = true
  517.  
  518.     // Title shows knife info — no need to repeat as a menu item
  519.     new title[160]
  520.     if(g_Knives[knife_id][KNIFE_PRICE] == 0) {
  521.         formatex(title, charsmax(title),
  522.             "\r%s \d[Default Knife]^n\wNo bonuses",
  523.             g_Knives[knife_id][KNIFE_NAME])
  524.     } else {
  525.         formatex(title, charsmax(title),
  526.             "\r%s\w | \y$%d\w | Lvl %d^n\wHP: +%d | Armor: +%d | Dmg: \yx%.2f",
  527.             g_Knives[knife_id][KNIFE_NAME],
  528.             g_Knives[knife_id][KNIFE_PRICE],
  529.             g_Knives[knife_id][KNIFE_LEVEL_REQ],
  530.             g_Knives[knife_id][KNIFE_HEALTH_BONUS],
  531.             g_Knives[knife_id][KNIFE_ARMOR_BONUS],
  532.             g_Knives[knife_id][KNIFE_DAMAGE_MULT])
  533.     }
  534.  
  535.     new menu = menu_create(title, "knife_details_handler")
  536.  
  537.     if(owned) {
  538.         if(g_iPlayerEquippedKnife[id] == knife_id) {
  539.             menu_additem(menu, "\d>> Currently Equipped", "info")
  540.         } else if(!valid) {
  541.             menu_additem(menu, "\dEquip (model not uploaded yet)", "locked")
  542.         } else {
  543.             menu_additem(menu, "\wEquip this knife", "equip")
  544.         }
  545.  
  546.         // Show next knife info — buyable only if requirements met
  547.         new nextId = knife_id + 1
  548.         if(nextId < KNIFE_COUNT && !IsKnifeOwned(id, nextId)) {
  549.             new bool:nextVIPLocked = (nextId == VIP_KNIFE_INDEX) && !(get_user_flags(id) & VIP_FLAG)
  550.             if(!nextVIPLocked) {
  551.                 new bool:canAfford = (g_iPlayerCash[id] >= g_Knives[nextId][KNIFE_PRICE])
  552.                 new bool:hasLevel  = (g_iPlayerLevel[id] >= g_Knives[nextId][KNIFE_LEVEL_REQ])
  553.                 new nextText[72]
  554.  
  555.                 if(canAfford && hasLevel) {
  556.                     formatex(nextText, charsmax(nextText), "\wBuy Next: \y%s \w(\y$%d\w)", g_Knives[nextId][KNIFE_NAME], g_Knives[nextId][KNIFE_PRICE])
  557.                     menu_additem(menu, nextText, "buynext")
  558.                 } else {
  559.                     // Grayed — show what's missing, not clickable
  560.                     if(!hasLevel)
  561.                         formatex(nextText, charsmax(nextText), "\dNext: %s (Need Lvl %d)", g_Knives[nextId][KNIFE_NAME], g_Knives[nextId][KNIFE_LEVEL_REQ])
  562.                     else
  563.                         formatex(nextText, charsmax(nextText), "\dNext: %s (Need $%d)", g_Knives[nextId][KNIFE_NAME], g_Knives[nextId][KNIFE_PRICE])
  564.                     menu_additem(menu, nextText, "info", ADMIN_RCON)
  565.                 }
  566.             }
  567.         }
  568.     } else if(!prevOwned) {
  569.         new lock_text[64]
  570.         formatex(lock_text, charsmax(lock_text), "\dBuy %s first", g_Knives[knife_id - 1][KNIFE_NAME])
  571.         menu_additem(menu, lock_text, "locked")
  572.     } else if(!levelOk) {
  573.         new lock_text[48]
  574.         formatex(lock_text, charsmax(lock_text), "\dNeed level %d to unlock", g_Knives[knife_id][KNIFE_LEVEL_REQ])
  575.         menu_additem(menu, lock_text, "locked")
  576.     } else if(g_iPlayerCash[id] < g_Knives[knife_id][KNIFE_PRICE]) {
  577.         new lock_text[48]
  578.         formatex(lock_text, charsmax(lock_text), "\dNot enough cash ($%d / $%d)", g_iPlayerCash[id], g_Knives[knife_id][KNIFE_PRICE])
  579.         menu_additem(menu, lock_text, "locked")
  580.     } else {
  581.         new buy_text[40]
  582.         formatex(buy_text, charsmax(buy_text), "\wPurchase \y$%d", g_Knives[knife_id][KNIFE_PRICE])
  583.         menu_additem(menu, buy_text, "buy")
  584.     }
  585.  
  586.     menu_additem(menu, "\wView Stats", "stats")
  587.  
  588.     menu_setprop(menu, MPROP_EXIT, MEXIT_ALL)
  589.     menu_display(id, menu, 0)
  590. }
  591.  
  592. public knife_details_handler(id, menu, item) {
  593.     if(item == MENU_EXIT) {
  594.         menu_destroy(menu)
  595.         ShowKnifesList(id)
  596.         return PLUGIN_HANDLED
  597.     }
  598.  
  599.     new item_info[8], dummy[64], access, callback
  600.     menu_item_getinfo(menu, item, access, item_info, charsmax(item_info), dummy, charsmax(dummy), callback)
  601.     menu_destroy(menu)
  602.  
  603.     new knife_id = g_iPlayerViewingKnife[id]
  604.  
  605.     if(equal(item_info, "equip")) {
  606.         g_iPlayerEquippedKnife[id] = knife_id
  607.         ApplyKnifeAbilities(id)
  608.         client_print_color(id, print_team_default, "^4[Knife] ^1Equipped: ^3%s", g_Knives[knife_id][KNIFE_NAME])
  609.         SavePlayerData(id)
  610.         ShowKnifeDetails(id, knife_id)
  611.     } else if(equal(item_info, "buynext")) {
  612.         new nextId = knife_id + 1
  613.         if(nextId < KNIFE_COUNT && !IsKnifeOwned(id, nextId)
  614.         && g_iPlayerCash[id] >= g_Knives[nextId][KNIFE_PRICE]
  615.         && g_iPlayerLevel[id] >= g_Knives[nextId][KNIFE_LEVEL_REQ]) {
  616.             g_iPlayerCash[id] -= g_Knives[nextId][KNIFE_PRICE]
  617.             AddOwnedKnife(id, nextId)
  618.  
  619.             new buyerName[32]
  620.             get_user_name(id, buyerName, charsmax(buyerName))
  621.             client_print_color(0, print_team_default, "^4[Knife] ^3%s ^1purchased ^4%s^1!", buyerName, g_Knives[nextId][KNIFE_NAME])
  622.  
  623.             if(IsKnifeValid(nextId)) {
  624.                 g_iPlayerEquippedKnife[id] = nextId
  625.                 ApplyKnifeAbilities(id)
  626.                 client_print_color(id, print_team_default, "^4[Knife] ^1Purchased and equipped: ^3%s^1! Remaining: ^3$%d",
  627.                     g_Knives[nextId][KNIFE_NAME], g_iPlayerCash[id])
  628.             } else {
  629.                 client_print_color(id, print_team_default, "^4[Knife] ^1Purchased: ^3%s^1! Remaining: ^3$%d ^1(model not uploaded yet — equip later)",
  630.                     g_Knives[nextId][KNIFE_NAME], g_iPlayerCash[id])
  631.             }
  632.             SavePlayerData(id)
  633.             ShowKnifeDetails(id, nextId)
  634.         } else {
  635.             client_print_color(id, print_team_default, "^4[Knife] ^1Cannot buy next knife yet.")
  636.             ShowKnifeDetails(id, knife_id)
  637.         }
  638.     } else if(equal(item_info, "buy")) {
  639.         g_iPlayerCash[id] -= g_Knives[knife_id][KNIFE_PRICE]
  640.         AddOwnedKnife(id, knife_id)
  641.  
  642.         new buyerName[32]
  643.         get_user_name(id, buyerName, charsmax(buyerName))
  644.         client_print_color(0, print_team_default, "^4[Knife] ^3%s ^1purchased ^4%s^1!", buyerName, g_Knives[knife_id][KNIFE_NAME])
  645.  
  646.         if(IsKnifeValid(knife_id)) {
  647.             g_iPlayerEquippedKnife[id] = knife_id
  648.             ApplyKnifeAbilities(id)
  649.             client_print_color(id, print_team_default, "^4[Knife] ^1Purchased and equipped: ^3%s^1! Remaining: ^3$%d",
  650.                 g_Knives[knife_id][KNIFE_NAME], g_iPlayerCash[id])
  651.         } else {
  652.             client_print_color(id, print_team_default, "^4[Knife] ^1Purchased: ^3%s^1! Remaining: ^3$%d ^1(model not uploaded yet)",
  653.                 g_Knives[knife_id][KNIFE_NAME], g_iPlayerCash[id])
  654.         }
  655.  
  656.         SavePlayerData(id)
  657.         ShowKnifeDetails(id, knife_id)
  658.     } else if(equal(item_info, "stats")) {
  659.         ShowKnifeStats(id, knife_id)
  660.     } else {
  661.         // "locked", "info", or anything else — just re-open details
  662.         ShowKnifeDetails(id, knife_id)
  663.     }
  664.  
  665.     return PLUGIN_HANDLED
  666. }
  667.  
  668. // -------------------------------------------------------------------------
  669. // Knife Stats — shown as menu text (no MOTD)
  670.  
  671. ShowKnifeStats(id, knife_id) {
  672.     if(!is_user_connected(id)) return
  673.  
  674.     new bool:isVIP    = bool:(get_user_flags(id) & VIP_FLAG)
  675.     new bool:vipKnife = (knife_id == VIP_KNIFE_INDEX)
  676.     new bool:owned    = IsKnifeOwned(id, knife_id)
  677.     new bool:equipped = (g_iPlayerEquippedKnife[id] == knife_id)
  678.  
  679.     new stats[768], line[80], sl[80]
  680.  
  681.     // ── Header ──────────────────────────────────────────
  682.     if(vipKnife && isVIP)
  683.         formatex(stats, charsmax(stats), "\r=====[ \y%s \r]=====^n\r          [ VIP EXCLUSIVE ]^n", g_Knives[knife_id][KNIFE_NAME])
  684.     else
  685.         formatex(stats, charsmax(stats), "\r=====[ \y%s \r]=====^n", g_Knives[knife_id][KNIFE_NAME])
  686.  
  687.     // ── Price & Status ───────────────────────────────────
  688.     if(g_Knives[knife_id][KNIFE_PRICE] == 0) {
  689.         add(stats, charsmax(stats), "\d  Type    \w: Default Knife^n")
  690.         add(stats, charsmax(stats), "\d  Price   \w: \yFree^n")
  691.     } else {
  692.         formatex(sl, charsmax(sl), "\d  Price   \w: \y$%d^n", g_Knives[knife_id][KNIFE_PRICE])
  693.         add(stats, charsmax(stats), sl)
  694.         formatex(sl, charsmax(sl), "\d  Level   \w: \yLvl %d required^n", g_Knives[knife_id][KNIFE_LEVEL_REQ])
  695.         add(stats, charsmax(stats), sl)
  696.     }
  697.  
  698.     if(equipped)
  699.         add(stats, charsmax(stats), "\d  Status  \w: \r[ Currently Equipped ]^n")
  700.     else if(owned)
  701.         add(stats, charsmax(stats), "\d  Status  \w: \y[ Owned ]^n")
  702.     else
  703.         add(stats, charsmax(stats), "\d  Status  \w: \d[ Not Owned ]^n")
  704.  
  705.     add(stats, charsmax(stats), "\d--------------------------------^n")
  706.  
  707.     // ── Combat Stats ─────────────────────────────────────
  708.     add(stats, charsmax(stats), "\w  COMBAT STATS^n")
  709.  
  710.     formatex(line, charsmax(line), "\d  Health  \w: \r+%d HP^n", g_Knives[knife_id][KNIFE_HEALTH_BONUS])
  711.     add(stats, charsmax(stats), line)
  712.  
  713.     formatex(line, charsmax(line), "\d  Armor   \w: \r+%d^n", g_Knives[knife_id][KNIFE_ARMOR_BONUS])
  714.     add(stats, charsmax(stats), line)
  715.  
  716.     formatex(line, charsmax(line), "\d  Damage  \w: \rx%.2f multiplier^n", g_Knives[knife_id][KNIFE_DAMAGE_MULT])
  717.     add(stats, charsmax(stats), line)
  718.  
  719.     add(stats, charsmax(stats), "\d--------------------------------^n")
  720.  
  721.     // ── Passive Stats ────────────────────────────────────
  722.     add(stats, charsmax(stats), "\w  PASSIVE STATS^n")
  723.  
  724.     if(g_Knives[knife_id][KNIFE_HP_REGEN] > 0)
  725.         formatex(line, charsmax(line), "\d  HP Regen \w: \r+%d HP\w every \r%ds^n", g_Knives[knife_id][KNIFE_HP_REGEN], g_Knives[knife_id][KNIFE_HP_REGEN_INTERVAL])
  726.     else
  727.         copy(line, charsmax(line), "\d  HP Regen \w: \d None^n")
  728.     add(stats, charsmax(stats), line)
  729.  
  730.     if(g_Knives[knife_id][KNIFE_CASH_BONUS] > 0.0)
  731.         formatex(line, charsmax(line), "\d  Cash/Kill \w: \r+%.0f%%\w per kill^n", g_Knives[knife_id][KNIFE_CASH_BONUS] * 100.0)
  732.     else
  733.         copy(line, charsmax(line), "\d  Cash/Kill \w: \d None^n")
  734.     add(stats, charsmax(stats), line)
  735.  
  736.     add(stats, charsmax(stats), "\d--------------------------------^n")
  737.  
  738.     // ── Your Stats ───────────────────────────────────────
  739.     add(stats, charsmax(stats), "\w  YOUR STATS^n")
  740.     formatex(line, charsmax(line), "\d  Cash    \w: \y$%d^n", g_iPlayerCash[id])
  741.     add(stats, charsmax(stats), line)
  742.     formatex(line, charsmax(line), "\d  Level   \w: \yLvl %d^n", g_iPlayerLevel[id])
  743.     add(stats, charsmax(stats), line)
  744.  
  745.     add(stats, charsmax(stats), "\d--------------------------------^n")
  746.     add(stats, charsmax(stats), "\r0. \wBack")
  747.  
  748.     show_menu(id, MENU_KEY_0, stats, -1, "Knife Stats Menu")
  749. }
  750.  
  751. public knife_stats_menu_handler(id, key) {
  752.     ShowKnifeDetails(id, g_iPlayerViewingKnife[id])
  753.     return PLUGIN_HANDLED
  754. }
  755.  
  756. // -------------------------------------------------------------------------
  757. // Preview — skips missing models
  758.  
  759. ShowPreviewMenu(id) {
  760.     if(!is_user_connected(id)) return
  761.     new menu = menu_create("[JailBreak] Preview Knife", "preview_menu_handler")
  762.  
  763.     for(new i = 0; i < KNIFE_COUNT; i++) {
  764.         new item_text[64], item_info[4]
  765.         if(IsKnifeValid(i))
  766.             formatex(item_text, charsmax(item_text), "%s", g_Knives[i][KNIFE_NAME])
  767.         else
  768.             formatex(item_text, charsmax(item_text), "\d%s (unavailable)", g_Knives[i][KNIFE_NAME])
  769.  
  770.         formatex(item_info, charsmax(item_info), "%d", i)
  771.         menu_additem(menu, item_text, item_info)
  772.     }
  773.  
  774.     menu_setprop(menu, MPROP_EXIT, MEXIT_ALL)
  775.     menu_display(id, menu, 0)
  776. }
  777.  
  778. public preview_menu_handler(id, menu, item) {
  779.     if(item == MENU_EXIT) {
  780.         menu_destroy(menu)
  781.         ShowMainMenu(id)
  782.         return PLUGIN_HANDLED
  783.     }
  784.  
  785.     new item_info[4], dummy[64], access, callback
  786.     menu_item_getinfo(menu, item, access, item_info, charsmax(item_info), dummy, charsmax(dummy), callback)
  787.     menu_destroy(menu)
  788.  
  789.     new knife_id = str_to_num(item_info)
  790.  
  791.     if(!IsKnifeValid(knife_id)) {
  792.         client_print_color(id, print_team_default, "^4[Knife] ^1Cannot preview ^3%s ^1- model not uploaded yet.", g_Knives[knife_id][KNIFE_NAME])
  793.         ShowPreviewMenu(id)
  794.         return PLUGIN_HANDLED
  795.     }
  796.  
  797.     PreviewKnife(id, knife_id)
  798.     return PLUGIN_HANDLED
  799. }
  800.  
  801. PreviewKnife(id, knife_id) {
  802.     remove_task(TASK_PREVIEW + id)
  803.     g_iPlayerPreviewingKnife[id] = knife_id
  804.     client_print(id, print_chat, "[Knife] Previewing %s for %.0f seconds.", g_Knives[knife_id][KNIFE_NAME], PREVIEW_TIME)
  805.     set_task(PREVIEW_TIME, "EndPreview", TASK_PREVIEW + id)
  806. }
  807.  
  808. public EndPreview(taskid) {
  809.     new id = taskid - TASK_PREVIEW
  810.     g_iPlayerPreviewingKnife[id] = -1
  811.     if(is_user_connected(id))
  812.         client_print(id, print_chat, "[Knife] Preview ended.")
  813. }
  814.  
  815. // -------------------------------------------------------------------------
  816. // Inventory Menu — small selector before opening MOTD or leaderboard
  817.  
  818. ShowInventoryMenu(id) {
  819.     if(!is_user_connected(id)) return
  820.  
  821.     new title[64]
  822.     formatex(title, charsmax(title), "\yMy Inventory \w| Lvl: %d | $%d",
  823.         g_iPlayerLevel[id], g_iPlayerCash[id])
  824.  
  825.     new menu = menu_create(title, "inventory_select_handler")
  826.     menu_additem(menu, "\wView My Knives",         "inv")
  827.     menu_additem(menu, "\wTop Knife Leaderboard",  "top")
  828.     menu_setprop(menu, MPROP_EXIT, MEXIT_ALL)
  829.     menu_display(id, menu, 0)
  830. }
  831.  
  832. public inventory_select_handler(id, menu, item) {
  833.     if(item == MENU_EXIT) {
  834.         menu_destroy(menu)
  835.         ShowMainMenu(id)
  836.         return PLUGIN_HANDLED
  837.     }
  838.  
  839.     new item_info[4], dummy[64], access, callback
  840.     menu_item_getinfo(menu, item, access, item_info, charsmax(item_info), dummy, charsmax(dummy), callback)
  841.     menu_destroy(menu)
  842.  
  843.     if(equal(item_info, "inv"))     ShowInventory(id)
  844.     else if(equal(item_info, "top")) ShowTopLeaderboard(id)
  845.  
  846.     return PLUGIN_HANDLED
  847. }
  848.  
  849. // -------------------------------------------------------------------------
  850. // Inventory — MOTD HTML
  851.  
  852. ShowInventory(id) {
  853.     if(!is_user_connected(id)) return
  854.  
  855.     static motd[3072], tmp[256]
  856.     motd[0] = EOS
  857.     tmp[0]  = EOS
  858.  
  859.     new xpBar[32]
  860.     BuildXPBar(id, xpBar, charsmax(xpBar))
  861.  
  862.     copy(motd, charsmax(motd), "<body bgcolor=#111111>")
  863.     add(motd, charsmax(motd), "<font face=Arial size=4 color=#FFB000><b>My Inventory</b></font><br>")
  864.  
  865.     formatex(tmp, charsmax(tmp), "<font face=Arial size=2 color=#777777>Cash: <b>$%d</b> | Level: <b>%d</b> | %s</font><br><br>",
  866.         g_iPlayerCash[id], g_iPlayerLevel[id], xpBar)
  867.     add(motd, charsmax(motd), tmp)
  868.  
  869.     new found = 0
  870.     for(new i = 0; i < KNIFE_COUNT; i++) {
  871.         if(!IsKnifeOwned(id, i)) continue
  872.  
  873.         new bool:equipped = (i == g_iPlayerEquippedKnife[id])
  874.         new bool:valid    = IsKnifeValid(i)
  875.  
  876.         // Row background
  877.         new rowBg[8]
  878.         if(equipped)      copy(rowBg, charsmax(rowBg), "#1a2a1a")
  879.         else if(!valid)   copy(rowBg, charsmax(rowBg), "#2a1a1a")
  880.         else              copy(rowBg, charsmax(rowBg), "#1a1a1a")
  881.  
  882.         formatex(tmp, charsmax(tmp), "<table bgcolor=%s width=100%%><tr>", rowBg)
  883.         add(motd, charsmax(motd), tmp)
  884.  
  885.         // Name + equipped badge
  886.         if(equipped) {
  887.             formatex(tmp, charsmax(tmp), "<td width=160><font face=Arial size=3 color=#FFD700><b>%s</b></font> <font face=Arial size=2 color=#00CC44>[Equipped]</font></td>", g_Knives[i][KNIFE_NAME])
  888.         } else if(!valid) {
  889.             formatex(tmp, charsmax(tmp), "<td width=160><font face=Arial size=3 color=#555555>%s</font> <font face=Arial size=2 color=#FF4444>[missing]</font></td>", g_Knives[i][KNIFE_NAME])
  890.         } else {
  891.             formatex(tmp, charsmax(tmp), "<td width=160><font face=Arial size=3 color=#FFFFFF>%s</font></td>", g_Knives[i][KNIFE_NAME])
  892.         }
  893.         add(motd, charsmax(motd), tmp)
  894.  
  895.         // Stats inline
  896.         if(g_Knives[i][KNIFE_HEALTH_BONUS] > 0 || g_Knives[i][KNIFE_ARMOR_BONUS] > 0 || g_Knives[i][KNIFE_DAMAGE_MULT] > 1.0) {
  897.             formatex(tmp, charsmax(tmp), "<td><font face=Arial size=2 color=#AAAAAA>HP +%d | Arm +%d | Dmg x%.2f",
  898.                 g_Knives[i][KNIFE_HEALTH_BONUS], g_Knives[i][KNIFE_ARMOR_BONUS], g_Knives[i][KNIFE_DAMAGE_MULT])
  899.             add(motd, charsmax(motd), tmp)
  900.  
  901.             if(g_Knives[i][KNIFE_HP_REGEN] > 0) {
  902.                 formatex(tmp, charsmax(tmp), " | Regen +%d/%ds", g_Knives[i][KNIFE_HP_REGEN], g_Knives[i][KNIFE_HP_REGEN_INTERVAL])
  903.                 add(motd, charsmax(motd), tmp)
  904.             }
  905.             add(motd, charsmax(motd), "</font></td>")
  906.         } else {
  907.             add(motd, charsmax(motd), "<td><font face=Arial size=2 color=#555555>No bonuses</font></td>")
  908.         }
  909.  
  910.         add(motd, charsmax(motd), "</tr></table>")
  911.         found++
  912.     }
  913.  
  914.     if(!found)
  915.         add(motd, charsmax(motd), "<font face=Arial size=3 color=#555555>No knives owned yet.</font>")
  916.  
  917.     add(motd, charsmax(motd), "<br><font face=Arial size=2 color=#555555>Click OK to close. Use /knife to equip knives.</font>")
  918.  
  919.     show_motd(id, motd, "My Inventory")
  920. }
  921.  
  922. // -------------------------------------------------------------------------
  923. // Shop Info
  924.  
  925. ShowShopInfo(id) {
  926.     static info[768], tmp[128]
  927.     info[0] = EOS
  928.     tmp[0]  = EOS
  929.  
  930.     copy(info, charsmax(info), "<body bgcolor=#111111>")
  931.     add(info, charsmax(info), "<font color=#FFB000><b>[Frag JailBreak] Knives Shop</b></font><br><br>")
  932.     add(info, charsmax(info), "<font color=#FFFFFF>Purchase and equip knives with unique abilities.</font><br><br>")
  933.  
  934.     add(info, charsmax(info), "<font color=#FFB000><b>How to earn cash & XP:</b></font><br>")
  935.     add(info, charsmax(info), "<font color=#AAAAAA>- Kill enemies to earn $100 and XP per kill<br>")
  936.     add(info, charsmax(info), "- CT kills give 20 XP, T kills give 10 XP<br>")
  937.     add(info, charsmax(info), "- Level up to unlock stronger knives<br><br></font>")
  938.  
  939.     add(info, charsmax(info), "<font color=#FFB000><b>Knife bonuses (per equipped knife):</b></font><br>")
  940.     add(info, charsmax(info), "<font color=#AAAAAA>- Extra health and armor each round<br>")
  941.     add(info, charsmax(info), "- Damage bonus on knife hits<br>")
  942.     add(info, charsmax(info), "- Cash bonus on kills<br>")
  943.     add(info, charsmax(info), "- Gravity reduction (jump higher)<br><br></font>")
  944.  
  945.     add(info, charsmax(info), "<font color=#FFB000><b>Knife unlock order:</b></font><br>")
  946.  
  947.     new i
  948.     for(i = 0; i < KNIFE_COUNT; i++) {
  949.         if(g_Knives[i][KNIFE_PRICE] == 0) {
  950.             formatex(tmp, charsmax(tmp), "<font color=#AAAAAA>%d. %s - Free (default)</font><br>",
  951.                 i + 1, g_Knives[i][KNIFE_NAME])
  952.         } else {
  953.             formatex(tmp, charsmax(tmp), "<font color=#AAAAAA>%d. %s - $%d | Level %d</font><br>",
  954.                 i + 1, g_Knives[i][KNIFE_NAME],
  955.                 g_Knives[i][KNIFE_PRICE],
  956.                 g_Knives[i][KNIFE_LEVEL_REQ])
  957.         }
  958.         add(info, charsmax(info), tmp)
  959.     }
  960.  
  961.     add(info, charsmax(info), "<br><font color=#FFFFFF>Type <b>/knife</b> to open the shop.</font>")
  962.  
  963.     show_motd(id, info, "Shop Information")
  964. }
  965.  
  966. // -------------------------------------------------------------------------
  967. // Top Knife Leaderboard — shows top 5 online players by most expensive knife owned
  968.  
  969. ShowTopLeaderboard(id) {
  970.     // Collect online players sorted by top knife price (simple insertion sort)
  971.     new topIds[5], topPrices[5], count = 0
  972.  
  973.     for(new p = 1; p <= MAX_PLAYERS; p++) {
  974.         if(!is_user_connected(p)) continue
  975.  
  976.         new knifeId  = GetPlayerTopKnife(p)
  977.         new knifePrice = g_Knives[knifeId][KNIFE_PRICE]
  978.  
  979.         // Insert into top 5
  980.         if(count < 5) {
  981.             topIds[count]    = p
  982.             topPrices[count] = knifePrice
  983.             count++
  984.         } else {
  985.             // Find lowest in top5 and replace if current is higher
  986.             new minIdx = 0
  987.             for(new j = 1; j < 5; j++)
  988.                 if(topPrices[j] < topPrices[minIdx]) minIdx = j
  989.             if(knifePrice > topPrices[minIdx]) {
  990.                 topIds[minIdx]    = p
  991.                 topPrices[minIdx] = knifePrice
  992.             }
  993.         }
  994.     }
  995.  
  996.     // Bubble sort descending
  997.     for(new a = 0; a < count - 1; a++) {
  998.         for(new b = a + 1; b < count; b++) {
  999.             if(topPrices[b] > topPrices[a]) {
  1000.                 new tmpId = topIds[a];    topIds[a]    = topIds[b];    topIds[b]    = tmpId
  1001.                 new tmpPr = topPrices[a]; topPrices[a] = topPrices[b]; topPrices[b] = tmpPr
  1002.             }
  1003.         }
  1004.     }
  1005.  
  1006.     // static — allocated once, not on stack each call
  1007.     static motd[4096], tmp[600]
  1008.     motd[0] = EOS
  1009.     tmp[0]  = EOS
  1010.     copy(motd, charsmax(motd), "<body bgcolor=#111111 style='padding:12px'>")
  1011.     add(motd, charsmax(motd), "<font face=Arial size=4 color=#FFB000><b>Top Knife Leaderboard</b></font><br>")
  1012.     add(motd, charsmax(motd), "<font face=Arial size=2 color=#777777>Online players only</font><br><br>")
  1013.  
  1014.     if(count == 0) {
  1015.         add(motd, charsmax(motd), "<font face=Arial size=3 color=#AAAAAA>No players online.</font>")
  1016.     } else {
  1017.         for(new i = 0; i < count; i++) {
  1018.             new p = topIds[i], name[32]
  1019.             get_user_name(p, name, charsmax(name))
  1020.             new knifeId = GetPlayerTopKnife(p)
  1021.  
  1022.             // Alternate row shade
  1023.             new rowColor[8]
  1024.             if(i % 2 == 0) copy(rowColor, charsmax(rowColor), "#1a1a1a")
  1025.             else           copy(rowColor, charsmax(rowColor), "#222222")
  1026.  
  1027.             // Medal color
  1028.             new medalColor[8]
  1029.             if(i == 0)      copy(medalColor, charsmax(medalColor), "#FFD700")  // gold
  1030.             else if(i == 1) copy(medalColor, charsmax(medalColor), "#C0C0C0")  // silver
  1031.             else if(i == 2) copy(medalColor, charsmax(medalColor), "#CD7F32")  // bronze
  1032.             else            copy(medalColor, charsmax(medalColor), "#888888")
  1033.  
  1034.             // Build row in parts — Pawn no multi-line strings
  1035.             new rowTmp[128]
  1036.             tmp[0] = EOS
  1037.  
  1038.             formatex(rowTmp, charsmax(rowTmp), "<table bgcolor=%s width=100%%><tr>", rowColor)
  1039.             add(tmp, charsmax(tmp), rowTmp)
  1040.  
  1041.             formatex(rowTmp, charsmax(rowTmp), "<td width=30><font face=Arial size=3 color=%s><b>#%d</b></font></td>", medalColor, i + 1)
  1042.             add(tmp, charsmax(tmp), rowTmp)
  1043.  
  1044.             formatex(rowTmp, charsmax(rowTmp), "<td width=160><font face=Arial size=3 color=#FFFFFF>%s</font></td>", name)
  1045.             add(tmp, charsmax(tmp), rowTmp)
  1046.  
  1047.             formatex(rowTmp, charsmax(rowTmp), "<td width=130><font face=Arial size=3 color=#FFB000>%s</font></td>", g_Knives[knifeId][KNIFE_NAME])
  1048.             add(tmp, charsmax(tmp), rowTmp)
  1049.  
  1050.             formatex(rowTmp, charsmax(rowTmp), "<td width=50><font face=Arial size=3 color=#AAAAAA>Lvl %d</font></td>", g_iPlayerLevel[p])
  1051.             add(tmp, charsmax(tmp), rowTmp)
  1052.  
  1053.             formatex(rowTmp, charsmax(rowTmp), "<td><font face=Arial size=3 color=#AAAAAA>$%d</font></td></tr></table>", g_iPlayerCash[p])
  1054.             add(tmp, charsmax(tmp), rowTmp)
  1055.  
  1056.             add(motd, charsmax(motd), tmp)
  1057.         }
  1058.     }
  1059.  
  1060.     show_motd(id, motd, "Top Knife Leaderboard")
  1061. }
  1062.  
  1063. // -------------------------------------------------------------------------
  1064. // Admin: give cash to a player
  1065.  
  1066. public CmdGiveCash(id, level, cid) {
  1067.     if(!cmd_access(id, level, cid, 3)) return PLUGIN_HANDLED
  1068.  
  1069.     new argName[32], argAmount[16]
  1070.     read_argv(1, argName,   charsmax(argName))
  1071.     read_argv(2, argAmount, charsmax(argAmount))
  1072.  
  1073.     new amount = str_to_num(argAmount)
  1074.     if(amount <= 0) {
  1075.         console_print(id, "[KnifeShop] Usage: amx_givecash <player> <amount>")
  1076.         return PLUGIN_HANDLED
  1077.     }
  1078.  
  1079.     new target = cmd_target(id, argName, CMDTARGET_ALLOW_SELF)
  1080.     if(!target) return PLUGIN_HANDLED
  1081.  
  1082.     g_iPlayerCash[target] += amount
  1083.     SavePlayerData(target)
  1084.  
  1085.     new targetName[32], adminName[32]
  1086.     get_user_name(target, targetName, charsmax(targetName))
  1087.     get_user_name(id,     adminName,  charsmax(adminName))
  1088.  
  1089.     // Notify target
  1090.     client_print_color(target, print_team_default, "^4[Knife] ^1Admin ^3%s ^1gave you ^3$%d^1! Total: ^3$%d",
  1091.         adminName, amount, g_iPlayerCash[target])
  1092.  
  1093.     // Notify admin
  1094.     console_print(id, "[KnifeShop] Gave $%d to %s. New total: $%d", amount, targetName, g_iPlayerCash[target])
  1095.  
  1096.     // Server announcement
  1097.     client_print_color(0, print_team_default, "^4[Knife] ^3%s ^1received ^3$%d ^1cash from admin!",
  1098.         targetName, amount)
  1099.  
  1100.     return PLUGIN_HANDLED
  1101. }
  1102.  
  1103. // Admin: set player level
  1104. public CmdGiveLevel(id, level, cid) {
  1105.     if(!cmd_access(id, level, cid, 3)) return PLUGIN_HANDLED
  1106.  
  1107.     new argName[32], argLevel[8]
  1108.     read_argv(1, argName,  charsmax(argName))
  1109.     read_argv(2, argLevel, charsmax(argLevel))
  1110.  
  1111.     new newLevel = str_to_num(argLevel)
  1112.     if(newLevel <= 0) {
  1113.         console_print(id, "[KnifeShop] Usage: amx_givelevel <player> <level>")
  1114.         return PLUGIN_HANDLED
  1115.     }
  1116.  
  1117.     new target = cmd_target(id, argName, CMDTARGET_ALLOW_SELF)
  1118.     if(!target) return PLUGIN_HANDLED
  1119.  
  1120.     new oldLevel = g_iPlayerLevel[target]
  1121.     g_iPlayerLevel[target] = newLevel
  1122.     g_iPlayerXP[target]    = 0      // reset XP bar on level change
  1123.     SavePlayerData(target)
  1124.  
  1125.     new targetName[32], adminName[32]
  1126.     get_user_name(target, targetName, charsmax(targetName))
  1127.     get_user_name(id,     adminName,  charsmax(adminName))
  1128.  
  1129.     // Notify target
  1130.     client_print_color(target, print_team_default, "^4[Knife] ^1Admin ^3%s ^1set your level to ^3%d",
  1131.         adminName, newLevel)
  1132.  
  1133.     // Notify admin
  1134.     console_print(id, "[KnifeShop] Set %s level %d -> %d", targetName, oldLevel, newLevel)
  1135.  
  1136.     // Server announcement
  1137.     client_print_color(0, print_team_default, "^4[Knife] ^3%s ^1was set to level ^3%d ^1by admin!",
  1138.         targetName, newLevel)
  1139.  
  1140.     return PLUGIN_HANDLED
  1141. }
  1142.  
  1143. // Admin command — show top leaderboard in console
  1144. public CmdKnifeTop(id, level, cid) {
  1145.     if(!cmd_access(id, level, cid, 1)) return PLUGIN_HANDLED
  1146.  
  1147.     console_print(id, "--- Top Knife Leaderboard (online) ---")
  1148.     new found = 0
  1149.  
  1150.     // Build sorted list same as ShowTopLeaderboard
  1151.     new topIds[5], topPrices[5], count = 0
  1152.     for(new p = 1; p <= MAX_PLAYERS; p++) {
  1153.         if(!is_user_connected(p)) continue
  1154.         new knifeId    = GetPlayerTopKnife(p)
  1155.         new knifePrice = g_Knives[knifeId][KNIFE_PRICE]
  1156.         if(count < 5) {
  1157.             topIds[count] = p; topPrices[count] = knifePrice; count++
  1158.         } else {
  1159.             new minIdx = 0
  1160.             for(new j = 1; j < 5; j++)
  1161.                 if(topPrices[j] < topPrices[minIdx]) minIdx = j
  1162.             if(knifePrice > topPrices[minIdx]) {
  1163.                 topIds[minIdx] = p; topPrices[minIdx] = knifePrice
  1164.             }
  1165.         }
  1166.     }
  1167.     for(new a = 0; a < count - 1; a++)
  1168.         for(new b = a + 1; b < count; b++)
  1169.             if(topPrices[b] > topPrices[a]) {
  1170.                 new tId = topIds[a]; topIds[a] = topIds[b]; topIds[b] = tId
  1171.                 new tPr = topPrices[a]; topPrices[a] = topPrices[b]; topPrices[b] = tPr
  1172.             }
  1173.  
  1174.     for(new i = 0; i < count; i++) {
  1175.         new p = topIds[i], name[32]
  1176.         get_user_name(p, name, charsmax(name))
  1177.         new knifeId = GetPlayerTopKnife(p)
  1178.         console_print(id, "#%d | %s | Knife: %s | Level: %d | Cash: $%d",
  1179.             i + 1, name, g_Knives[knifeId][KNIFE_NAME], g_iPlayerLevel[p], g_iPlayerCash[p])
  1180.         found++
  1181.     }
  1182.  
  1183.     if(!found) console_print(id, "No players online.")
  1184.     return PLUGIN_HANDLED
  1185. }
  1186.  
  1187. // Admin command — wipe all vault data and notify server
  1188. public CmdKnifeReset(id, level, cid) {
  1189.     if(!cmd_access(id, level, cid, 1)) return PLUGIN_HANDLED
  1190.  
  1191.     // Reset all online players immediately
  1192.     for(new p = 1; p <= MAX_PLAYERS; p++) {
  1193.         if(!is_user_connected(p)) continue
  1194.  
  1195.         g_iPlayerCash[p]          = 0
  1196.         g_iPlayerLevel[p]         = 1
  1197.         g_iPlayerXP[p]            = 0
  1198.         g_iPlayerEquippedKnife[p] = 0
  1199.         g_iPlayerOwnedKnives[p]   = (1 << 0)
  1200.  
  1201.         SavePlayerData(p)
  1202.         client_print_color(p, print_team_default, "^4[Knife] ^1Seasonal reset! All knife data has been wiped. Start fresh!")
  1203.     }
  1204.  
  1205.     // Wipe the entire vault so offline players also start fresh on next connect
  1206.     nvault_prune(g_vault, 0, get_systime() + 999999999)
  1207.  
  1208.     new adminName[32]
  1209.     get_user_name(id, adminName, charsmax(adminName))
  1210.     log_amx("[KnifeShop] Seasonal reset performed by %s", adminName)
  1211.  
  1212.     console_print(id, "[KnifeShop] Seasonal reset complete. All data wiped.")
  1213.     return PLUGIN_HANDLED
  1214. }
  1215.  
  1216. // -------------------------------------------------------------------------
  1217.  
  1218. stock GetPlayerTopKnife(id) {
  1219.     new top = 0
  1220.     for(new i = 0; i < KNIFE_COUNT; i++)
  1221.         if(IsKnifeOwned(id, i) && IsKnifeValid(i) && g_Knives[i][KNIFE_PRICE] > g_Knives[top][KNIFE_PRICE])
  1222.             top = i
  1223.     return top
  1224. }
  1225.  
  1226. // -------------------------------------------------------------------------
  1227. // Natives
  1228.  
  1229. public native_get_player_knife_cash(plugin_id, num_params) {
  1230.     new id = get_param(1)
  1231.     if(id < 1 || id > MAX_PLAYERS) {
  1232.         log_error(AMX_ERR_NATIVE, "Invalid player id %d", id)
  1233.         return 0
  1234.     }
  1235.     return g_iPlayerCash[id]
  1236. }
  1237.  
  1238. public native_set_player_knife_cash(plugin_id, num_params) {
  1239.     new id   = get_param(1)
  1240.     new cash = get_param(2)
  1241.     if(id < 1 || id > MAX_PLAYERS) {
  1242.         log_error(AMX_ERR_NATIVE, "Invalid player id %d", id)
  1243.         return 0
  1244.     }
  1245.     g_iPlayerCash[id] = cash
  1246.     SavePlayerData(id)
  1247.     return 1
  1248. }
  1249.  
  1250. public native_get_player_knife_level(plugin_id, num_params) {
  1251.     new id = get_param(1)
  1252.     if(id < 1 || id > MAX_PLAYERS) {
  1253.         log_error(AMX_ERR_NATIVE, "Invalid player id %d", id)
  1254.         return 0
  1255.     }
  1256.     return g_iPlayerLevel[id]
  1257. }
  1258.  
  1259. public native_set_player_knife_level(plugin_id, num_params) {
  1260.     new id    = get_param(1)
  1261.     new level = get_param(2)
  1262.     if(id < 1 || id > MAX_PLAYERS) {
  1263.         log_error(AMX_ERR_NATIVE, "Invalid player id %d", id)
  1264.         return 0
  1265.     }
  1266.     g_iPlayerLevel[id] = level
  1267.     SavePlayerData(id)
  1268.     return 1
  1269. }
Comments
  • CheezPuff
    2 years
    # text 0.26 KB | 0 0
    1. מי שעוקב אחריי המוד רק בתהליכי בנייה כי יש בחור מתנשא
    2. infender חושב שהוא מתכנת על
    3. build shop hardcoded 1.8.3 so lets copy and paste the SAME SHIT, OFC NOT 100% SAME only half works because not have time
Add Comment
Please, Sign In to add comment