initial commit
This commit is contained in:
commit
d701598350
67 changed files with 9351 additions and 0 deletions
181
Outnumbered/Progression.cs
Normal file
181
Outnumbered/Progression.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Menu;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using Outnumbered.Data;
|
||||
using Outnumbered.Domain;
|
||||
|
||||
namespace Outnumbered;
|
||||
|
||||
// XP -> levels -> points -> prestige, plus the !skills menu that spends points. The XP/level/prestige CURVES
|
||||
// live in Outnumbered.Domain.ProgressionModel; these are zero-math accessors. The stateful level-up loop + its side
|
||||
// effects (chat/sound/clan) stay engine-side below.
|
||||
public sealed partial class OutnumberedPlugin
|
||||
{
|
||||
private long XpToNext(int level) => ProgressionModel.XpToNext(level, Config.Progression);
|
||||
|
||||
private double PrestigeXpMultiplier(PlayerData pd) => ProgressionModel.PrestigeXpMultiplier(pd.Prestige, Config.Progression);
|
||||
|
||||
// Award XP (per-hit damage, kill bonus, or dev). Applies the XP-Boost stat + cumulative prestige boost +
|
||||
// handicap rate; handles level-ups. The fractional remainder is carried so tiny per-hit grants aren't lost.
|
||||
private void GrantXp(PlayerData pd, double baseAmount, CCSPlayerController? p)
|
||||
{
|
||||
if (baseAmount <= 0 || pd.Level >= Config.Progression.LevelCap) return;
|
||||
|
||||
double scaled = baseAmount
|
||||
* (1.0 + Eff(pd, StatKeys.XpBoost) / 100.0)
|
||||
* PrestigeXpMultiplier(pd)
|
||||
* HandicapXpMult(pd)
|
||||
+ pd.XpCarry;
|
||||
long gain = (long)Math.Floor(scaled);
|
||||
pd.XpCarry = scaled - gain; // keep the sub-1 remainder for the next hit
|
||||
if (gain <= 0) return;
|
||||
|
||||
pd.Xp += gain;
|
||||
pd.Dirty = true;
|
||||
ApplyXpToLevel(pd, p);
|
||||
}
|
||||
|
||||
// Resolve any pending level-ups from pd.Xp (awarding a point + firing the level-up cascade each), then clamp at
|
||||
// the cap. Shared by per-hit XP (GrantXp) and the survival per-wave grant (AddConvertedXp) so both behave alike.
|
||||
private void ApplyXpToLevel(PlayerData pd, CCSPlayerController? p)
|
||||
{
|
||||
long need;
|
||||
while (pd.Level < Config.Progression.LevelCap && pd.Xp >= (need = XpToNext(pd.Level)))
|
||||
{
|
||||
pd.Xp -= need; // same XpToNext(pd.Level) the condition just computed (pd.Level unchanged until ++ below)
|
||||
pd.Level++;
|
||||
pd.Points++;
|
||||
OnLevelUp(pd, p);
|
||||
}
|
||||
if (pd.Level >= Config.Progression.LevelCap) { pd.Xp = 0; pd.XpCarry = 0; } // clamp at cap
|
||||
}
|
||||
|
||||
// Route combat XP. In survival it's banked RAW into the CURRENT wave's accumulator (granted at wave clear x prestige
|
||||
// x waveMult; the handicap XP-mult is deliberately excluded there). In every other mode it grants now.
|
||||
internal void GrantCombatXp(PlayerData pd, double baseAmount, CCSPlayerController? p)
|
||||
{
|
||||
if (Draft is { } d) d.AccumulateWaveXp(pd, baseAmount); // survival: banked raw into THIS wave's accumulator
|
||||
else GrantXp(pd, baseAmount, p);
|
||||
}
|
||||
|
||||
// Add a pre-computed XP lump straight to the main table and resolve level-ups — NO per-hit multipliers re-applied
|
||||
// (the survival per-wave grant has already applied prestige x waveMult and deliberately excluded the handicap mult).
|
||||
internal void AddConvertedXp(PlayerData pd, long lump, CCSPlayerController? p)
|
||||
{
|
||||
if (lump <= 0 || pd.Level >= Config.Progression.LevelCap) return;
|
||||
pd.Xp += lump;
|
||||
pd.Dirty = true;
|
||||
ApplyXpToLevel(pd, p);
|
||||
}
|
||||
|
||||
private void OnLevelUp(PlayerData pd, CCSPlayerController? p)
|
||||
{
|
||||
if (p is not { IsValid: true }) return;
|
||||
if (pd.Level >= Config.Progression.LevelCap)
|
||||
p.PrintToChat($"[Outnumbered] MAX LEVEL! Type !prestige to reset for a permanent boost.");
|
||||
else
|
||||
p.PrintToChat($"[Outnumbered] Level {pd.Level}! +1 point — !skills to spend ({pd.Points} available).");
|
||||
ApplyClan(p, pd); // rank tag may have changed
|
||||
PlaySound(p, Config.Sounds.LevelUp);
|
||||
AnnounceUnlocks(p, pd.Level); // shout any weapon(s) that unlock at this exact level
|
||||
}
|
||||
|
||||
// Flat kill bonus on the lethal blow (the bulk of XP comes per-hit from damage; see OnPlayerHurt_Stats).
|
||||
private void GrantKillXp(CCSPlayerController attacker)
|
||||
{
|
||||
if (PdOf(attacker) is { } pd)
|
||||
GrantCombatXp(pd, Config.Progression.KillXpBonus, attacker); // survival: into the run accumulator
|
||||
}
|
||||
|
||||
// ---- !skills ----
|
||||
// Flat numbered legend (no menu): buy any stat directly with !s<n>, repeatable, no pagination.
|
||||
// (!1-!9 are reserved by CSSharp's menu key system, so skill commands use the 's' prefix.)
|
||||
private void OpenSkillMenu(CCSPlayerController p)
|
||||
{
|
||||
var pd = PdOf(p);
|
||||
if (pd is null) return;
|
||||
|
||||
p.PrintToChat($" {ChatColors.Gold}[Outnumbered] Skills — {pd.Points} pt | L{pd.Level} P {Roman(pd.Prestige)} (buy: !s1-!s{StatList.Length})");
|
||||
for (int i = 0; i < StatList.Length; i++)
|
||||
p.PrintToChat($" {ChatColors.Lime} " + SkillLegendSeg(pd, i));
|
||||
}
|
||||
|
||||
private string SkillLegendSeg(PlayerData pd, int i)
|
||||
{
|
||||
var (key, display) = StatList[i];
|
||||
return $"!s{i + 1} {display} [{LevelOf(pd, key)}/{DefFor(key).MaxLevel}]";
|
||||
}
|
||||
|
||||
// Registered from Load: !s1..!sN each buy the matching stat (BuyStat enforces points/cap).
|
||||
private void RegisterSkillCommands()
|
||||
{
|
||||
for (int i = 0; i < StatList.Length; i++)
|
||||
{
|
||||
int idx = i;
|
||||
AddCommand($"css_s{i + 1}", $"Buy skill #{i + 1}", (player, _) =>
|
||||
{
|
||||
if (player is { IsValid: true }) BuyStat(player, StatList[idx].Key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void BuyStat(CCSPlayerController p, string key)
|
||||
{
|
||||
var pd = PdOf(p);
|
||||
if (pd is null) return;
|
||||
|
||||
int lvl = LevelOf(pd, key);
|
||||
int max = DefFor(key).MaxLevel;
|
||||
if (pd.Points <= 0 || lvl >= max) return;
|
||||
|
||||
pd.Upgrades[key] = lvl + 1;
|
||||
pd.Points--;
|
||||
pd.Dirty = true;
|
||||
DeferReapplyCaps(p); // re-apply the new max HP/armor next frame (slot+SteamID-pinned)
|
||||
p.PrintToChat($"[Outnumbered] {key} -> {lvl + 1}/{max} ({pd.Points} point(s) left).");
|
||||
}
|
||||
|
||||
// ---- !prestige ----
|
||||
private void OpenPrestige(CCSPlayerController p)
|
||||
{
|
||||
var pd = PdOf(p);
|
||||
if (pd is null) return;
|
||||
|
||||
if (pd.Level < Config.Progression.LevelCap)
|
||||
{
|
||||
p.PrintToChat($"[Outnumbered] Reach level {Config.Progression.LevelCap} to prestige (you're {pd.Level}).");
|
||||
return;
|
||||
}
|
||||
if (pd.Prestige >= Config.Progression.PrestigeCap)
|
||||
{
|
||||
p.PrintToChat($"[Outnumbered] Max prestige ({Roman(pd.Prestige)}) — 100% complete!");
|
||||
return;
|
||||
}
|
||||
|
||||
var menu = new ChatMenu($"Prestige {Roman(pd.Prestige + 1)}? FULL RESET (level/points/stats)");
|
||||
menu.AddMenuOption("YES — prestige now", (player, _) => DoPrestige(player));
|
||||
menu.AddMenuOption("Cancel", (_, _) => { });
|
||||
menu.Open(p);
|
||||
}
|
||||
|
||||
private void DoPrestige(CCSPlayerController p)
|
||||
{
|
||||
var pd = PdOf(p);
|
||||
if (pd is null) return;
|
||||
if (pd.Level < Config.Progression.LevelCap || pd.Prestige >= Config.Progression.PrestigeCap) return;
|
||||
|
||||
pd.Prestige++;
|
||||
pd.Level = 1;
|
||||
pd.Xp = 0;
|
||||
pd.Points = 1;
|
||||
pd.Upgrades.Clear();
|
||||
pd.Dirty = true;
|
||||
|
||||
// Re-apply the default loadout (level 1 re-locks the high-tier guns) and reset HP/armor to base on the live pawn.
|
||||
DeferReapplyCaps(p, relockLoadout: true); // slot+SteamID-pinned so a within-frame slot-reuse can't inherit this reset
|
||||
ApplyClan(p, pd); // prestige + reset to L1 changes the tag
|
||||
PlaySound(p, Config.Sounds.Prestige);
|
||||
Server.PrintToChatAll($"[Outnumbered] {p.PlayerName} reached Prestige {Roman(pd.Prestige)}!");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue