using System; using System.Linq; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; using System.Timers; using Terraria; using Terraria.ID; using TerrariaApi.Server; using TShockAPI; namespace JopaBridge { // Мост-коллаба: TShock-сервер Terraria <-> веб-игра "Планета Жопа" (mp_server.py на :8790). // События Terraria (заход/выход/босс) улетают в фид Планеты Жопа; команды /svodka /jopa тянут её статус. [ApiVersion(2, 1)] public class JopaBridge : TerrariaPlugin { public override string Name => "JopaBridge"; public override string Author => "Планета Жопа × Terraria"; public override string Description => "Мост между Terraria-сервером и веб-игрой Планета Жопа"; public override Version Version => new Version(1, 0, 0); private const string JOPA = "http://127.0.0.1:8790"; private static readonly HttpClient Http = new HttpClient { Timeout = TimeSpan.FromSeconds(4) }; private Timer _svodka; public JopaBridge(Main game) : base(game) { } public override void Initialize() { ServerApi.Hooks.ServerJoin.Register(this, OnJoin); ServerApi.Hooks.ServerLeave.Register(this, OnLeave); ServerApi.Hooks.NpcKilled.Register(this, OnNpcKilled); Commands.ChatCommands.Add(new Command(CmdJopa, "jopa")); Commands.ChatCommands.Add(new Command(CmdSvodka, "svodka")); Commands.ChatCommands.Add(new Command("jopabridge.gear", CmdGear, "gearset", "postml")); Commands.ChatCommands.Add(new Command("jopabridge.build", CmdHotdog, "hotdog", "хотдог")); _svodka = new Timer(5 * 60 * 1000) { AutoReset = true }; _svodka.Elapsed += (s, e) => Bcast("📡 СВОДКА: фронт Планеты Жопа держится. /jopa — связь миров · /svodka — сводка.", 120, 200, 255); _svodka.Start(); TShock.Log.ConsoleInfo("[JopaBridge] Мост Планета Жопа × Terraria поднят."); Push("server", "TShock-сервер JopaSMP на связи с фронтом"); } protected override void Dispose(bool disposing) { if (disposing) { ServerApi.Hooks.ServerJoin.Deregister(this, OnJoin); ServerApi.Hooks.ServerLeave.Deregister(this, OnLeave); ServerApi.Hooks.NpcKilled.Deregister(this, OnNpcKilled); _svodka?.Stop(); _svodka?.Dispose(); } base.Dispose(disposing); } private static string PlayerName(int who) { try { var p = TShock.Players[who]; return (p != null && !string.IsNullOrEmpty(p.Name)) ? p.Name : "боец"; } catch { return "боец"; } } private void OnJoin(JoinEventArgs args) { string n = PlayerName(args.Who); Bcast($"🪖 {n} десантировался на JopaSMP. Добро пожаловать на фронт.", 180, 255, 140); Push("join", $"{n} зашёл на Terraria-фронт JopaSMP"); } private void OnLeave(LeaveEventArgs args) { string n = PlayerName(args.Who); Push("leave", $"{n} покинул Terraria-фронт"); } private void OnNpcKilled(NpcKilledEventArgs args) { try { var npc = args.npc; if (npc == null || !npc.boss) return; string boss = string.IsNullOrEmpty(npc.FullName) ? "босс" : npc.FullName; Bcast($"🏆 На JopaSMP повержен {boss}! Слава фронту.", 255, 180, 80); Push("boss", $"Terraria: повержен босс «{boss}»"); } catch { } } private void CmdJopa(CommandArgs args) { args.Player.SendMessage("🌐 Планета Жопа × Terraria — миры на связи.", 120, 200, 255); args.Player.SendInfoMessage("Веб-игра (дрон-гонка/песочница) крутится на том же сервере. Твои события тут летят в её фид."); args.Player.SendInfoMessage("Команды: /svodka — сводка с фронта Планеты Жопа."); } private void CmdSvodka(CommandArgs args) { _ = Task.Run(async () => { try { var s = await Http.GetStringAsync(JOPA + "/health"); using var doc = JsonDocument.Parse(s); var r = doc.RootElement; int drones = r.TryGetProperty("players", out var p) ? p.GetInt32() : 0; int herd = r.TryGetProperty("herd", out var h) ? h.GetInt32() : 0; args.Player.SendMessage("📡 СВОДКА С ПЛАНЕТЫ ЖОПА", 120, 200, 255); args.Player.SendInfoMessage($" дронов онлайн: {drones} · стадо сфинктеров: {herd} голов"); args.Player.SendInfoMessage($" Terraria-фронт JopaSMP: {TShock.Utils.GetActivePlayerCount()} бойцов онлайн"); } catch { args.Player.SendErrorMessage("Планета Жопа не отвечает — фронт вне связи."); } }); } // пост-Мунлорд лоадауты: (item, stack, prefix) private static readonly (int id, int stack, int prefix)[] WARRIOR = { (ItemID.Zenith, 1, PrefixID.Legendary), (ItemID.SolarFlareHelmet, 1, 0), (ItemID.SolarFlareBreastplate, 1, 0), (ItemID.SolarFlareLeggings, 1, 0), (ItemID.LongRainbowTrailWings, 1, 0), // Celestial Starboard (внутреннее имя) (ItemID.AnkhShield, 1, PrefixID.Warding), (ItemID.MasterNinjaGear, 1, PrefixID.Menacing), (ItemID.FireGauntlet, 1, PrefixID.Menacing), (ItemID.CelestialShell, 1, PrefixID.Menacing), (ItemID.WarriorEmblem, 1, PrefixID.Menacing), (ItemID.CharmofMyths, 1, 0), (ItemID.GreaterHealingPotion, 40, 0), }; private static readonly (int id, int stack, int prefix)[] MAGE = { (ItemID.LastPrism, 1, PrefixID.Mythical), (ItemID.NebulaHelmet, 1, 0), (ItemID.NebulaBreastplate, 1, 0), (ItemID.NebulaLeggings, 1, 0), (ItemID.LongRainbowTrailWings, 1, 0), // Celestial Starboard (внутреннее имя) (ItemID.AnkhShield, 1, PrefixID.Warding), (ItemID.CelestialCuffs, 1, PrefixID.Menacing), (ItemID.CelestialEmblem, 1, PrefixID.Menacing), (ItemID.CelestialShell, 1, PrefixID.Menacing), (ItemID.CharmofMyths, 1, 0), (ItemID.SuperManaPotion, 40, 0), (ItemID.GreaterHealingPotion, 40, 0), }; private void CmdHotdog(CommandArgs args) { int cx = Main.spawnTileX, cy = Main.spawnTileY - 34; // парит над спавном const int HW = 26, HH = 11; void SetTile(int wx, int wy, int type) { if (wx < 5 || wy < 5 || wx >= Main.maxTilesX - 5 || wy >= Main.maxTilesY - 5) return; var t = Main.tile[wx, wy]; t.active(true); t.type = (ushort)type; t.slope(0); t.halfBrick(false); } for (int lx = -HW; lx <= HW; lx++) for (int ly = -HH; ly <= HH; ly++) { double nx = (double)lx / HW, ny = (double)ly / HH; // булка (эллипс) double sx = (double)lx / (HW * 1.14), sy = (double)ly / (HH * 0.34); // сосиска (тонкая, торчит по краям) if (sx * sx + sy * sy <= 1.0) SetTile(cx + lx, cy + ly, TileID.FleshBlock); else if (nx * nx + ny * ny <= 1.0) SetTile(cx + lx, cy + ly, TileID.Sandstone); } for (int lx = -HW + 2; lx <= HW - 2; lx++) // горчица-зигзаг поверх сосиски { double sx = (double)lx / (HW * 1.14); if (sx * sx > 0.92) continue; int wy = cy + (int)Math.Round(-HH * 0.12 + 2.4 * Math.Sin(lx * 0.55)); SetTile(cx + lx, wy, TileID.AmberGemspark); SetTile(cx + lx, wy - 1, TileID.AmberGemspark); } int x0 = cx - HW - 1, y0 = cy - HH - 1, W = 2 * HW + 3, H = 2 * HH + 3; for (int gx = x0; gx < x0 + W; gx += 40) for (int gy = y0; gy < y0 + H; gy += 40) TSPlayer.All.SendTileRect((short)gx, (short)gy, (byte)Math.Min(40, x0 + W - gx), (byte)Math.Min(40, y0 + H - gy)); args.Player.SendSuccessMessage("🌭 Гигантский хот-дог воздвигнут над спавном."); Bcast("🌭 Над спавном воздвигнут ГИГАНТСКИЙ ХОТ-ДОГ волей админа. Слава фронту.", 255, 200, 80); } private void CmdGear(CommandArgs args) { if (args.Parameters.Count < 2) { args.Player.SendErrorMessage("Использование: /gearset <игрок> "); return; } var found = TSPlayer.FindByNameOrID(args.Parameters[0]); if (found.Count == 0) { args.Player.SendErrorMessage("Игрок не найден."); return; } if (found.Count > 1) { args.Player.SendMultipleMatchError(found.Select(p => p.Name)); return; } var target = found[0]; string cls = args.Parameters[1].ToLowerInvariant(); (int id, int stack, int prefix)[] set; string label; if (cls.StartsWith("mag") || cls == "маг") { set = MAGE; label = "маг · Nebula + Last Prism"; } else if (cls.StartsWith("war") || cls.StartsWith("mel") || cls == "воин") { set = WARRIOR; label = "воин · Solar Flare + Zenith"; } else { args.Player.SendErrorMessage("Класс: mage или warrior."); return; } foreach (var it in set) target.GiveItem(it.id, it.stack, it.prefix); target.SendSuccessMessage($"⚔️ Админ выдал тебе пост-Мунлорд сет: {label}."); args.Player.SendSuccessMessage($"Выдал {target.Name} пост-Мунлорд сет: {label}."); Bcast($"⚔️ {target.Name} экипирован пост-Мунлорд сетом ({label}) волей админа.", 255, 200, 80); } private static void Bcast(string msg, byte r, byte g, byte b) { try { TShock.Utils.Broadcast(msg, r, g, b); } catch { } } private static void Push(string kind, string msg) { _ = Task.Run(async () => { try { string json = JsonSerializer.Serialize(new { t = "jopa_event", kind, msg }); await Http.PostAsync(JOPA + "/jopa_event", new StringContent(json, Encoding.UTF8, "application/json")); } catch { } }); } } }