feat: Add NPC spawner and spawn rate multiplier

This commit is contained in:
VAC committed 2026-10-15 21:47:06 -03:00
1 parent 5bf19308dc
commit 55150f3c47
11 files changed
+193 -3

No files matched your search

+2
View File
@@ -28,6 +28,7 @@ public static class Hotkeys
new(InterfaceGroup, "Menu", "Open / close menu", () => WindowManager.Toggle(WindowManager.Menu)), new(InterfaceGroup, "Menu", "Open / close menu", () => WindowManager.Toggle(WindowManager.Menu)),
new(InterfaceGroup, "ItemBrowser", "Item Browser", () => WindowManager.Toggle(WindowManager.Items)), new(InterfaceGroup, "ItemBrowser", "Item Browser", () => WindowManager.Toggle(WindowManager.Items)),
new(InterfaceGroup, "BuffBrowser", "Buff Browser", () => WindowManager.Toggle(WindowManager.Buffs)), new(InterfaceGroup, "BuffBrowser", "Buff Browser", () => WindowManager.Toggle(WindowManager.Buffs)),
new(InterfaceGroup, "NpcSpawner", "NPC Spawner", () => WindowManager.Toggle(WindowManager.Npcs)),
Toggle("Player", "GodMode", "God Mode", () => Settings.GodMode, v => Settings.GodMode = v), Toggle("Player", "GodMode", "God Mode", () => Settings.GodMode, v => Settings.GodMode = v),
Toggle("Player", "AutoHeal", "Auto Heal", () => Settings.AutoHeal, v => Settings.AutoHeal = v), Toggle("Player", "AutoHeal", "Auto Heal", () => Settings.AutoHeal, v => Settings.AutoHeal = v),
@@ -69,6 +70,7 @@ public static class Hotkeys
Toggle("Visuals", "Shine", "Shine", () => Settings.Shine, v => Settings.Shine = v), Toggle("Visuals", "Shine", "Shine", () => Settings.Shine, v => Settings.Shine = v),
Toggle("World", "FreezeTime", "Freeze Time", () => Settings.FreezeTime, v => Settings.FreezeTime = v), Toggle("World", "FreezeTime", "Freeze Time", () => Settings.FreezeTime, v => Settings.FreezeTime = v),
Toggle("World", "SpawnRate", "Spawn Rate", () => Settings.SpawnRateBoost, v => Settings.SpawnRateBoost = v),
new("World", "SavePosition", "Save Current Position", Waypoints.AddHere), new("World", "SavePosition", "Save Current Position", Waypoints.AddHere),
]; ];
+2
View File
@@ -104,6 +104,8 @@ public static partial class Settings
[nameof(BiomeSight)] = "Vision.BiomeSight", [nameof(BiomeSight)] = "Vision.BiomeSight",
[nameof(FreezeTime)] = "World.FreezeTime", [nameof(FreezeTime)] = "World.FreezeTime",
[nameof(SpawnRateBoost)] = "World.SpawnRate.Enabled",
[nameof(SpawnRateMult)] = "World.SpawnRate.Multiplier",
[nameof(LogBallistics)] = "Ballistics.Log", [nameof(LogBallistics)] = "Ballistics.Log",
}; };
+2 -1
View File
@@ -48,7 +48,8 @@ public static partial class Settings
public static Color ColLocal = Color.White, ColPlayers = new(70, 200, 255), ColHostile = new(235, 60, 60), ColCritters = new(200, 170, 120), ColTown = new(80, 220, 110), ColBoss = new(255, 120, 40), ColItems = new(255, 210, 70), ColProjHostile = new(255, 80, 200), ColProjFriendly = new(120, 160, 255), ColWaypoint = new(220, 90, 255); public static Color ColLocal = Color.White, ColPlayers = new(70, 200, 255), ColHostile = new(235, 60, 60), ColCritters = new(200, 170, 120), ColTown = new(80, 220, 110), ColBoss = new(255, 120, 40), ColItems = new(255, 210, 70), ColProjHostile = new(255, 80, 200), ColProjFriendly = new(120, 160, 255), ColWaypoint = new(220, 90, 255);
public static bool Spelunker, Hunter, Shine, NightOwl, DangerSense, BiomeSight; public static bool Spelunker, Hunter, Shine, NightOwl, DangerSense, BiomeSight;
public static bool FreezeTime; public static bool FreezeTime, SpawnRateBoost;
public static float SpawnRateMult = 5f;
public static bool LogBallistics = true; public static bool LogBallistics = true;
+59
View File
@@ -0,0 +1,59 @@
using System;
using Terraria;
using Terraria.DataStructures;
using Terraria.ID;
using TerraSharp.Core;
namespace TerraSharp.Features;
public static class NpcSpawner
{
const float SpawnDistance = 10 * 16f;
const int MultiplayerClient = 1;
#if TMODLOADER
static readonly EntitySource_DebugCommand Source = new(nameof(NpcSpawner));
#else
static readonly EntitySource_DebugCommand Source = new();
#endif
public static bool Multiplayer => Main.netMode == MultiplayerClient;
// The server only honors summon requests for these types.
public static bool CanSummon(int type) => type >= 0 && type < NPCID.Sets.MPAllowedEnemies.Length && NPCID.Sets.MPAllowedEnemies[type];
public static bool Available(int type) => !Multiplayer || CanSummon(type);
public static void Spawn(int type, int count)
{
if (Multiplayer)
{
if (CanSummon(type))
NetMessage.SendData(MessageID.SpawnBossUseLicenseStartEvent, -1, -1, null, Main.myPlayer, type);
return;
}
Player player = Main.LocalPlayer;
int x = (int)(player.Center.X + player.direction * SpawnDistance);
int y = (int)player.Bottom.Y;
for (int i = 0; i < count; i++)
if (NPC.NewNPC(Source, x, y, type) >= Main.maxNPCs)
break;
}
// Spawner.GetSpawnRate postfix; spawnRate is the 1-in-N chance per tick.
public static void ScaleSpawnRate(ref int spawnRate, ref int maxSpawns)
{
if (!Settings.SpawnRateBoost)
return;
if (Settings.SpawnRateMult <= 0f)
{
maxSpawns = 0;
return;
}
spawnRate = Math.Max(1, (int)(spawnRate / Settings.SpawnRateMult));
maxSpawns = (int)Math.Ceiling(maxSpawns * Settings.SpawnRateMult);
}
}
+1
View File
@@ -21,6 +21,7 @@ internal sealed class HookRegistry : IDisposable
MainPatches.Install(hooks); MainPatches.Install(hooks);
PlayerPatches.Install(hooks); PlayerPatches.Install(hooks);
NpcPatches.Install(hooks); NpcPatches.Install(hooks);
SpawnRatePatch.Install(hooks);
ItemCheckShootPatch.Install(hooks); ItemCheckShootPatch.Install(hooks);
SandboxGuards.Install(hooks); SandboxGuards.Install(hooks);
return hooks; return hooks;
+53
View File
@@ -0,0 +1,53 @@
using System;
using Terraria;
using TerraSharp.Core;
using TerraSharp.Features;
#if TMODLOADER
using Terraria.ModLoader;
#endif
namespace TerraSharp.Patches;
// Natural spawns run only in single player and on the server.
internal static class SpawnRatePatch
{
#if TMODLOADER
// tModLoader's NPC.SpawnNPC passes the final rate through NPCLoader.EditSpawnRate.
delegate void Original(Player player, ref int spawnRate, ref int maxSpawns);
delegate void Hooked(Original original, Player player, ref int spawnRate, ref int maxSpawns);
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(NPCLoader), nameof(NPCLoader.EditSpawnRate), (Hooked)EditSpawnRate);
static void EditSpawnRate(Original original, Player player, ref int spawnRate, ref int maxSpawns)
{
original(player, ref spawnRate, ref maxSpawns);
Scale(ref spawnRate, ref maxSpawns);
}
#else
delegate void Original(NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns);
delegate void Hooked(Original original, NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns);
internal static void Install(HookRegistry hooks) => hooks.Add(typeof(NPC.Spawner), "GetSpawnRate", (Hooked)GetSpawnRate);
static void GetSpawnRate(Original original, NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns)
{
original(self, player, out spawnRate, out maxSpawns);
Scale(ref spawnRate, ref maxSpawns);
}
#endif
static void Scale(ref int spawnRate, ref int maxSpawns)
{
if (!Plugin.Active)
return;
try
{
NpcSpawner.ScaleSpawnRate(ref spawnRate, ref maxSpawns);
}
catch (Exception e)
{
Log.Error("Spawn rate", e);
}
}
}
+2
View File
@@ -27,6 +27,8 @@ public sealed class IdIndex(Func<int> count, Func<int, string?> name)
public string Name(int id) => id > 0 && id < Names.Length ? Names[id] : ""; public string Name(int id) => id > 0 && id < Names.Length ? Names[id] : "";
public void Refresh() => _lastQuery = null;
public void Filter(string query, Func<int, bool> fits) public void Filter(string query, Func<int, bool> fits)
{ {
query = query.Trim(); query = query.Trim();
+7
View File
@@ -228,6 +228,13 @@ public sealed partial class MainWindow
if (WorldFeatures.InvasionName is { } active && Buttons($"Stop {active}") == 0) if (WorldFeatures.InvasionName is { } active && Buttons($"Stop {active}") == 0)
WorldFeatures.StopInvasion(); WorldFeatures.StopInvasion();
Group("Spawning");
Note(NpcSpawner.Multiplayer ? "Multiplayer: boss summons only, spawn rate is ignored." : "Spawn rate only affects single player.");
if (Toggle("Spawn Rate", ref Settings.SpawnRateBoost))
Sub(() => Slider("Multiplier", ref Settings.SpawnRateMult, 0f, 20f, 5f, "x{0:0.0}", v => v < 0.05f ? "No spawns" : null));
if (Buttons(WindowManager.Npcs.Open ? "Close NPC Spawner" : "NPC Spawner") == 0)
WindowManager.Toggle(WindowManager.Npcs);
Group("Waypoints"); Group("Waypoints");
Note("Saved per world."); Note("Saved per world.");
if (Buttons("Save Current Position") == 0) if (Buttons("Save Current Position") == 0)
+1 -1
View File
@@ -35,7 +35,7 @@ public sealed partial class MainWindow : Window
new Page("Combat", CombatPage, () => Count(Settings.RapidAttack, Settings.AutoSwing, Settings.DamageBoost, Settings.NoManaCost, Settings.NoSpread, Settings.Aimbot, Settings.AutoAttack, Settings.KillAura, Settings.HitMagnet, Settings.FreezeEnemies, Settings.WeakenEnemies)), new Page("Combat", CombatPage, () => Count(Settings.RapidAttack, Settings.AutoSwing, Settings.DamageBoost, Settings.NoManaCost, Settings.NoSpread, Settings.Aimbot, Settings.AutoAttack, Settings.KillAura, Settings.HitMagnet, Settings.FreezeEnemies, Settings.WeakenEnemies)),
new Page("Items", ItemsPage, () => Count(Settings.InstantBreak, Settings.InfItems, Settings.ItemVacuum, Settings.AutoFish, Settings.Sonar)), new Page("Items", ItemsPage, () => Count(Settings.InstantBreak, Settings.InfItems, Settings.ItemVacuum, Settings.AutoFish, Settings.Sonar)),
new Page("Visuals", VisualsPage, () => Count(Settings.Esp, Settings.Spelunker, Settings.Hunter, Settings.Shine, Settings.NightOwl, Settings.DangerSense, Settings.BiomeSight)), new Page("Visuals", VisualsPage, () => Count(Settings.Esp, Settings.Spelunker, Settings.Hunter, Settings.Shine, Settings.NightOwl, Settings.DangerSense, Settings.BiomeSight)),
new Page("World", WorldPage, () => Count(Settings.FreezeTime)), new Page("World", WorldPage, () => Count(Settings.FreezeTime, Settings.SpawnRateBoost)),
new Page("Keys", KeysPage, () => 0), new Page("Keys", KeysPage, () => 0),
]; ];
+62
View File
@@ -0,0 +1,62 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
#if TMODLOADER
using Terraria.ModLoader;
#endif
using TerraSharp.Features;
namespace TerraSharp.UI;
public sealed class NpcBrowser() : GridBrowser("npcs", "NPC Spawner", new Vector2(660, 140), 532, 520, "Search NPCs by name or ID", "NPCs", new IdIndex(() => NpcCount, Lang.GetNPCNameValue), Categories)
{
const int Batch = 10;
#if TMODLOADER
static int NpcCount => NPCLoader.NPCCount;
#else
static int NpcCount => NPCID.Count;
#endif
static readonly BrowserCategory[] Categories = [new("All", NpcSpawner.Available), new("Bosses", type => NpcSpawner.Available(type) && IsBoss(type)), new("Enemies", type => NpcSpawner.Available(type) && Sample(type) is { friendly: false, townNPC: false } && !IsBoss(type) && !IsCritter(type)), new("Town", type => NpcSpawner.Available(type) && Sample(type) is { townNPC: true }), new("Critters", type => NpcSpawner.Available(type) && IsCritter(type))];
bool _multiplayer;
static NPC? Sample(int type) => ContentSamples.NpcsByNetId.TryGetValue(type, out NPC? npc) ? npc : null;
static bool IsBoss(int type) => Sample(type) is { boss: true };
static bool IsCritter(int type) => NPCID.Sets.CountsAsCritter[type] || Main.npcCatchable[type];
protected override void DrawContent(Rectangle body)
{
if (_multiplayer != NpcSpawner.Multiplayer)
{
_multiplayer = NpcSpawner.Multiplayer;
Index.Refresh();
}
DrawSearchGrid(body, body.Y, body.Bottom - 30);
string hint = _multiplayer ? "Multiplayer: only boss summons the server accepts" : $"Click: spawn in front of you Shift click: spawn {Batch}";
Render.TextMid(Widgets.SpriteBatch, hint, new Rectangle(body.X + 4, body.Bottom - 26, body.Width, 26), Widgets.Dim, 0.72f);
}
protected override void DrawSlot(int type, Rectangle bounds, bool hot)
{
Widgets.Slot(bounds, hot);
Main.instance.LoadNPC(type);
if (GameTextures.Get("Npc", type) is { } texture)
Widgets.Icon(texture, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), GameTextures.Frame(texture, 1, Main.npcFrameCount[type], 0, 0));
if (!hot)
return;
string action = _multiplayer ? "Click to summon" : "Click to spawn";
Widgets.Tooltip = $"{Index.Name(type)}\n{(IsBoss(type) ? "Boss" : "NPC")} #{type} {action}";
if (!Widgets.Click)
return;
NpcSpawner.Spawn(type, Widgets.Shift ? Batch : 1);
Widgets.Sound(12);
}
}
+2 -1
View File
@@ -13,11 +13,12 @@ public static class WindowManager
public static readonly MainWindow Menu = new(); public static readonly MainWindow Menu = new();
public static readonly ItemBrowser Items = new(); public static readonly ItemBrowser Items = new();
public static readonly BuffBrowser Buffs = new(); public static readonly BuffBrowser Buffs = new();
public static readonly NpcBrowser Npcs = new();
const int TitleHeight = 40; const int TitleHeight = 40;
const int MaxInputLength = 40; const int MaxInputLength = 40;
static readonly List<Window> Stack = [Items, Buffs, Menu]; static readonly List<Window> Stack = [Items, Buffs, Npcs, Menu];
static Window? _dragging; static Window? _dragging;
static Vector2 _dragOffset; static Vector2 _dragOffset;