initial commit
This commit is contained in:
commit
d701598350
67 changed files with 9351 additions and 0 deletions
249
Outnumbered/Weapons.cs
Normal file
249
Outnumbered/Weapons.cs
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
using System.Collections.Frozen;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Menu;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using Outnumbered.Config;
|
||||
using Outnumbered.Data;
|
||||
using Outnumbered.Engine;
|
||||
|
||||
namespace Outnumbered;
|
||||
|
||||
// Free weapon selection (no economy), split into categories. !guns lists the categories; the direct
|
||||
// commands (!rifles/!smgs/!snipers/!shotguns/!heavy/!pistols) jump straight to a category. Choice persists
|
||||
// per-SteamID (DB) and is applied on spawn by ApplyLoadout (default until chosen / if a saved gun is delisted).
|
||||
public sealed partial class OutnumberedPlugin
|
||||
{
|
||||
private static readonly FrozenDictionary<string, string> WeaponNames = new Dictionary<string, string>
|
||||
{
|
||||
["weapon_ak47"] = "AK-47",
|
||||
["weapon_m4a1_silencer"] = "M4A1-S",
|
||||
["weapon_m4a1"] = "M4A4",
|
||||
["weapon_aug"] = "AUG",
|
||||
["weapon_sg556"] = "SG 553",
|
||||
["weapon_galilar"] = "Galil AR",
|
||||
["weapon_famas"] = "FAMAS",
|
||||
["weapon_awp"] = "AWP",
|
||||
["weapon_ssg08"] = "SSG 08",
|
||||
["weapon_scar20"] = "SCAR-20",
|
||||
["weapon_g3sg1"] = "G3SG1",
|
||||
["weapon_mp9"] = "MP9",
|
||||
["weapon_mp7"] = "MP7",
|
||||
["weapon_mp5sd"] = "MP5-SD",
|
||||
["weapon_ump45"] = "UMP-45",
|
||||
["weapon_p90"] = "P90",
|
||||
["weapon_mac10"] = "MAC-10",
|
||||
["weapon_bizon"] = "PP-Bizon",
|
||||
["weapon_nova"] = "Nova",
|
||||
["weapon_xm1014"] = "XM1014",
|
||||
["weapon_mag7"] = "MAG-7",
|
||||
["weapon_sawedoff"] = "Sawed-Off",
|
||||
["weapon_m249"] = "M249",
|
||||
["weapon_negev"] = "Negev",
|
||||
["weapon_deagle"] = "Desert Eagle",
|
||||
["weapon_revolver"] = "R8 Revolver",
|
||||
["weapon_glock"] = "Glock-18",
|
||||
["weapon_usp_silencer"] = "USP-S",
|
||||
["weapon_hkp2000"] = "P2000",
|
||||
["weapon_p250"] = "P250",
|
||||
["weapon_fiveseven"] = "Five-SeveN",
|
||||
["weapon_tec9"] = "Tec-9",
|
||||
["weapon_cz75a"] = "CZ75-Auto",
|
||||
["weapon_elite"] = "Dual Berettas",
|
||||
["weapon_knife"] = "Knife", // the GG finale rung — otherwise the prefix-strip fallback renders lowercase "knife"
|
||||
}.ToFrozenDictionary(StringComparer.Ordinal);
|
||||
|
||||
internal static string WeaponDisplayName(string item) =>
|
||||
WeaponNames.TryGetValue(item, out var n) ? n
|
||||
: item.StartsWith("weapon_", StringComparison.Ordinal) ? item[7..].Replace('_', ' ') : item;
|
||||
|
||||
internal static bool WeapEq(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Cached weapon-category sets (OrdinalIgnoreCase == WeapEq), so membership is O(1) instead of Concat-5-lists + ToList
|
||||
// + a linear .Any(WeapEq) per call. Rebuilt only when the Match config INSTANCE changes (!og_reload swaps the whole
|
||||
// Config object). CONTRACT: an in-place edit to Match.Rifles/Pistols/... would NOT invalidate this — only a Config swap does.
|
||||
private FrozenSet<string>? _primarySet, _pistolSet;
|
||||
private MatchConfig? _weaponSetCfg;
|
||||
private void EnsureWeaponSets()
|
||||
{
|
||||
var m = Config.Match;
|
||||
if (_primarySet is not null && ReferenceEquals(_weaponSetCfg, m)) return;
|
||||
_primarySet = m.Rifles.Concat(m.Smgs).Concat(m.Snipers).Concat(m.Shotguns).Concat(m.Heavy).ToFrozenSet(StringComparer.OrdinalIgnoreCase);
|
||||
_pistolSet = m.Pistols.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
|
||||
_weaponSetCfg = m;
|
||||
}
|
||||
private bool IsPrimaryWeapon(string w) { EnsureWeaponSets(); return _primarySet!.Contains(w); }
|
||||
private bool IsPistolWeapon(string w) { EnsureWeaponSets(); return _pistolSet!.Contains(w); }
|
||||
|
||||
// Level required to use a weapon (absent/0 = free). Prestige resets level, so this re-locks on prestige.
|
||||
private int UnlockLevel(string w) => Config.Match.WeaponUnlockLevel.TryGetValue(w, out var lvl) ? lvl : 0;
|
||||
|
||||
// Allowed = in the right category set AND the player has reached its unlock level.
|
||||
private bool IsAllowedPrimary(PlayerData pd, string? w) =>
|
||||
!string.IsNullOrEmpty(w) && IsPrimaryWeapon(w!) && pd.Level >= UnlockLevel(w!);
|
||||
private bool IsAllowedSecondary(PlayerData pd, string? w) =>
|
||||
!string.IsNullOrEmpty(w) && IsPistolWeapon(w!) && pd.Level >= UnlockLevel(w!);
|
||||
|
||||
// What the driver hands this player on spawn: saved choice if still allowed (unlocked), else config default.
|
||||
internal (string primary, string secondary) ResolveLoadout(CCSPlayerController p)
|
||||
{
|
||||
string primary = Config.Match.HumanPrimary, secondary = Config.Match.HumanSecondary;
|
||||
if (PdOf(p) is { } pd)
|
||||
{
|
||||
if (IsAllowedPrimary(pd, pd.PrimaryWeapon)) primary = pd.PrimaryWeapon!;
|
||||
if (IsAllowedSecondary(pd, pd.SecondaryWeapon)) secondary = pd.SecondaryWeapon!;
|
||||
}
|
||||
return (primary, secondary);
|
||||
}
|
||||
|
||||
// Which category command equips this weapon (for the unlock announcement).
|
||||
private string WeaponCategoryCmd(string w)
|
||||
{
|
||||
var m = Config.Match;
|
||||
if (m.Rifles.Any(x => WeapEq(x, w))) return "rifles";
|
||||
if (m.Smgs.Any(x => WeapEq(x, w))) return "smgs";
|
||||
if (m.Snipers.Any(x => WeapEq(x, w))) return "snipers";
|
||||
if (m.Shotguns.Any(x => WeapEq(x, w))) return "shotguns";
|
||||
if (m.Heavy.Any(x => WeapEq(x, w))) return "heavy";
|
||||
return "pistols";
|
||||
}
|
||||
|
||||
// ---- quick-buy combo: the keystrokes that reach a weapon in the shop (X -> Weapons -> category -> item) ----
|
||||
// The 8 reliable menu keys in slot order (key 4 = grenade slot and 5 = C4 can't be clean selectors).
|
||||
private static readonly char[] MenuKeys = ['1', '2', '3', '6', '7', '8', '9', '0'];
|
||||
// Category order on the Weapons screen — fixes each category's key. Item keys come from the JSON list order.
|
||||
public static readonly string[] CategoryOrder = ["Pistols", "Smgs", "Shotguns", "Rifles", "Heavy", "Snipers"];
|
||||
|
||||
private List<string> CategoryList(string name) => name switch
|
||||
{
|
||||
"Rifles" => Config.Match.Rifles,
|
||||
"Smgs" => Config.Match.Smgs,
|
||||
"Snipers" => Config.Match.Snipers,
|
||||
"Shotguns" => Config.Match.Shotguns,
|
||||
"Heavy" => Config.Match.Heavy,
|
||||
"Pistols" => Config.Match.Pistols,
|
||||
_ => [],
|
||||
};
|
||||
|
||||
public static char MenuKey(int pos) => pos >= 0 && pos < MenuKeys.Length ? MenuKeys[pos] : '?';
|
||||
|
||||
// Derived, never stored: '1' = Weapons branch at root, then the category's key, then the weapon's key.
|
||||
// Falls out of the JSON list order automatically, so it can never drift from the menu. "" if not found, OR if the item
|
||||
// sits past the world-menu's key range (idx >= MenuKeys.Length) — those overflow guns have no quick-buy combo and are
|
||||
// reached via the paginated !category chat menu, so we omit the combo rather than print an unreachable key ('?').
|
||||
private string QuickBuySequence(string weapon)
|
||||
{
|
||||
for (int c = 0; c < CategoryOrder.Length; c++)
|
||||
{
|
||||
int idx = CategoryList(CategoryOrder[c]).FindIndex(x => WeapEq(x, weapon));
|
||||
if (idx >= 0) return idx < MenuKeys.Length ? $"X{MenuKey(0)}{MenuKey(c)}{MenuKey(idx)}" : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Loud, distinct chat shout for any weapon(s) that unlock exactly at this level (called on level-up).
|
||||
private void AnnounceUnlocks(CCSPlayerController p, int level)
|
||||
{
|
||||
foreach (var kv in Config.Match.WeaponUnlockLevel)
|
||||
if (kv.Value == level)
|
||||
{
|
||||
string combo = QuickBuySequence(kv.Key);
|
||||
string tail = combo.Length > 0 ? $" or quick-buy {ChatColors.Gold}{combo}{ChatColors.Default}" : "";
|
||||
p.PrintToChat($" {ChatColors.Magenta}★ WEAPON UNLOCKED: {WeaponDisplayName(kv.Key)} {ChatColors.Default}— !{WeaponCategoryCmd(kv.Key)} to equip{tail}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- top menu + category commands ----
|
||||
[ConsoleCommand("css_guns", "Open the weapon selection menu")]
|
||||
[ConsoleCommand("css_weapons", "Open the weapon selection menu")]
|
||||
[ConsoleCommand("css_loadout", "Open the weapon selection menu")]
|
||||
[CommandHelper(0, "", CommandUsage.CLIENT_ONLY)]
|
||||
public void Cmd_Guns(CCSPlayerController? p, CommandInfo info)
|
||||
{
|
||||
if (p is not { IsValid: true }) return;
|
||||
if (!_driver.WeaponShopEnabled) { p.PrintToChat($" {ChatColors.Gold}[Outnumbered] Your weapon is set by the Gun Game ladder — climb it!"); return; }
|
||||
var m = Config.Match;
|
||||
var (cur1, cur2) = ResolveLoadout(p);
|
||||
p.PrintToChat($" {ChatColors.Gold}[Outnumbered] Loadout — {WeaponDisplayName(cur1)} / {WeaponDisplayName(cur2)} (pick a category)");
|
||||
p.PrintToChat($" {ChatColors.Lime} !rifles — Rifles ({m.Rifles.Count})");
|
||||
p.PrintToChat($" {ChatColors.Lime} !smgs — SMGs ({m.Smgs.Count})");
|
||||
p.PrintToChat($" {ChatColors.Lime} !snipers — Snipers ({m.Snipers.Count})");
|
||||
p.PrintToChat($" {ChatColors.Lime} !shotguns — Shotguns ({m.Shotguns.Count})");
|
||||
p.PrintToChat($" {ChatColors.Lime} !heavy — Machine Guns ({m.Heavy.Count})");
|
||||
p.PrintToChat($" {ChatColors.Lime} !pistols — Pistols ({m.Pistols.Count})");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_rifles", "Choose a rifle")]
|
||||
[CommandHelper(0, "", CommandUsage.CLIENT_ONLY)]
|
||||
public void Cmd_Rifles(CCSPlayerController? p, CommandInfo i) { if (p is { IsValid: true }) OpenCategory(p, "Rifles", Config.Match.Rifles, true); }
|
||||
|
||||
[ConsoleCommand("css_smgs", "Choose an SMG")]
|
||||
[CommandHelper(0, "", CommandUsage.CLIENT_ONLY)]
|
||||
public void Cmd_Smgs(CCSPlayerController? p, CommandInfo i) { if (p is { IsValid: true }) OpenCategory(p, "SMGs", Config.Match.Smgs, true); }
|
||||
|
||||
[ConsoleCommand("css_snipers", "Choose a sniper")]
|
||||
[CommandHelper(0, "", CommandUsage.CLIENT_ONLY)]
|
||||
public void Cmd_Snipers(CCSPlayerController? p, CommandInfo i) { if (p is { IsValid: true }) OpenCategory(p, "Snipers", Config.Match.Snipers, true); }
|
||||
|
||||
[ConsoleCommand("css_shotguns", "Choose a shotgun")]
|
||||
[CommandHelper(0, "", CommandUsage.CLIENT_ONLY)]
|
||||
public void Cmd_Shotguns(CCSPlayerController? p, CommandInfo i) { if (p is { IsValid: true }) OpenCategory(p, "Shotguns", Config.Match.Shotguns, true); }
|
||||
|
||||
[ConsoleCommand("css_heavy", "Choose a machine gun")]
|
||||
[CommandHelper(0, "", CommandUsage.CLIENT_ONLY)]
|
||||
public void Cmd_Heavy(CCSPlayerController? p, CommandInfo i) { if (p is { IsValid: true }) OpenCategory(p, "Heavy", Config.Match.Heavy, true); }
|
||||
|
||||
[ConsoleCommand("css_pistols", "Choose a pistol")]
|
||||
[ConsoleCommand("css_secondaries", "Choose a pistol")]
|
||||
[CommandHelper(0, "", CommandUsage.CLIENT_ONLY)]
|
||||
public void Cmd_Pistols(CCSPlayerController? p, CommandInfo i) { if (p is { IsValid: true }) OpenCategory(p, "Pistols", Config.Match.Pistols, false); }
|
||||
|
||||
private void OpenCategory(CCSPlayerController p, string title, List<string> list, bool primary)
|
||||
{
|
||||
if (!_driver.WeaponShopEnabled) { p.PrintToChat($" {ChatColors.Gold}[Outnumbered] Weapon selection is disabled in Gun Game."); return; }
|
||||
var pd = PdOf(p);
|
||||
if (pd is null) return;
|
||||
|
||||
var menu = new ChatMenu($"Choose {title}");
|
||||
foreach (var item in list)
|
||||
{
|
||||
int req = UnlockLevel(item);
|
||||
bool unlocked = pd.Level >= req;
|
||||
string label = unlocked ? WeaponDisplayName(item) : $"{WeaponDisplayName(item)} [Lv {req}]";
|
||||
menu.AddMenuOption(label, (pl, _) => SetWeapon(pl, primary, item), disabled: !unlocked);
|
||||
}
|
||||
menu.Open(p);
|
||||
}
|
||||
|
||||
private void SetWeapon(CCSPlayerController p, bool primary, string item)
|
||||
{
|
||||
var pd = PdOf(p);
|
||||
if (pd is null) return;
|
||||
|
||||
int req = UnlockLevel(item);
|
||||
if (pd.Level < req) { p.PrintToChat($"[Outnumbered] {WeaponDisplayName(item)} unlocks at level {req}."); return; }
|
||||
|
||||
if (primary) pd.PrimaryWeapon = item; else pd.SecondaryWeapon = item;
|
||||
pd.Dirty = true;
|
||||
|
||||
string slotName = primary ? "Primary" : "Secondary";
|
||||
if (p.PawnIsAlive)
|
||||
{
|
||||
// Rebuild both slots synchronously. RemoveItemBySlot's kill is DEFERRED 0.1s (would double-occupy the
|
||||
// slot / leave the player unarmed); RemoveWeapons() is synchronous (mirrors ApplyLoadout). Armor isn't
|
||||
// a weapon so it survives; an ability grenade (if any) reconciles back.
|
||||
var (pri, sec) = ResolveLoadout(p);
|
||||
p.RemoveWeapons();
|
||||
p.GiveNamedItem(EngineNames.WeaponKnife);
|
||||
if (!string.IsNullOrEmpty(pri)) p.GiveNamedItem(pri);
|
||||
if (!string.IsNullOrEmpty(sec)) p.GiveNamedItem(sec);
|
||||
GiveShopCarriers(p); // re-give the healthshot + zeus after the rebuild (armor isn't a weapon, so RemoveWeapons left it)
|
||||
p.PrintToChat($"[Outnumbered] {slotName}: {WeaponDisplayName(item)}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
p.PrintToChat($"[Outnumbered] {slotName}: {WeaponDisplayName(item)} (applies on respawn).");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue