diff --git a/MxValheim/EventSystem/EventHud.cs b/MxValheim/EventSystem/EventHud.cs deleted file mode 100644 index 4c1b2a9..0000000 --- a/MxValheim/EventSystem/EventHud.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Annotations; -using System.Windows.Controls; -using UnityEngine; -using UnityEngine.UI; -using Image = UnityEngine.UI.Image; - -namespace MxValheim.EventSystem -{ - internal class EventHud - { - private static GameObject _eventRoot; - private static Text _eventTitleText; - private static Text _eventProgressText; - private static Image _progressBar; - private static CanvasGroup _canvasGroup; - - public static void CreateEventHud() - { - // 1. Setup Root (Stacked under your KillFeed which is at -30) - _eventRoot = new GameObject("MxEventHUD_Root"); - UnityEngine.Object.DontDestroyOnLoad(_eventRoot); - - UnityEngine.Canvas c = _eventRoot.AddComponent(); - c.renderMode = RenderMode.ScreenSpaceOverlay; - c.sortingOrder = 9999; // Just behind killfeed - - CanvasScaler scaler = _eventRoot.AddComponent(); - scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; - scaler.referenceResolution = new Vector2(1920, 1080); - - _canvasGroup = _eventRoot.AddComponent(); - _canvasGroup.alpha = 0f; - - // 2. Main Panel (Positioned at -80 to sit below the KillFeed) - GameObject panel = new GameObject("Background", typeof(RectTransform), typeof(Image)); - panel.transform.SetParent(_eventRoot.transform, false); - Image panelImage = panel.GetComponent(); - panelImage.color = new UnityEngine.Color(0, 0, 0, 0.7f); - - // Find Valheim Sprite - foreach (Sprite s in Resources.FindObjectsOfTypeAll()) - { - if (s.name == "item_background") - { - panelImage.sprite = s; - panelImage.type = Image.Type.Sliced; - break; - } - } - - RectTransform pRect = panel.GetComponent(); - pRect.anchorMin = pRect.anchorMax = pRect.pivot = new Vector2(0.5f, 1f); - pRect.anchoredPosition = new Vector2(0, -80); // Lowered to avoid overlap - pRect.sizeDelta = new Vector2(450, 60); // Slightly taller for progress bar - - // 3. Vertical Layout for Title + Progress Bar - VerticalLayoutGroup layout = panel.AddComponent(); - layout.childAlignment = UnityEngine.TextAnchor.MiddleCenter; - layout.spacing = 5f; - layout.padding = new RectOffset(10, 10, 5, 5); - - // 4. Title Text - _eventTitleText = CreateText(panel.transform, "EVENT ACTIVE", 14, UnityEngine.Color.yellow); - - // 5. Progress Bar Background - GameObject barBg = new GameObject("BarBG", typeof(RectTransform), typeof(Image)); - barBg.transform.SetParent(panel.transform, false); - barBg.GetComponent().color = new UnityEngine.Color(0.2f, 0.2f, 0.2f, 0.8f); - barBg.GetComponent().sizeDelta = new Vector2(400, 10); - - // 6. Actual Progress Fill - GameObject fillObj = new GameObject("Fill", typeof(RectTransform), typeof(Image)); - fillObj.transform.SetParent(barBg.transform, false); - _progressBar = fillObj.GetComponent(); - _progressBar.color = UnityEngine.Color.red; - _progressBar.type = Image.Type.Filled; - _progressBar.fillMethod = Image.FillMethod.Horizontal; - - RectTransform fRect = _progressBar.GetComponent(); - fRect.anchorMin = Vector2.zero; - fRect.anchorMax = Vector2.one; - fRect.sizeDelta = Vector2.zero; - - // 7. Counter Text (e.g., 12/20) - _eventProgressText = CreateText(panel.transform, "0 / 0", 10, UnityEngine.Color.white); - - _eventRoot.SetActive(false); - } - - public static Text CreateText(Transform parent, string content, int size, UnityEngine.Color col) - { - GameObject txtObj = new GameObject("Text", typeof(RectTransform), typeof(Text)); - txtObj.transform.SetParent(parent, false); - Text t = txtObj.GetComponent(); - t.text = content; - t.fontSize = size; - t.color = col; - t.alignment = UnityEngine.TextAnchor.MiddleCenter; - t.font = GetValheimFont(); - return t; - } - - public static UnityEngine.Font GetValheimFont() - { - foreach (UnityEngine.Font f in Resources.FindObjectsOfTypeAll()) - if (f.name == "AveriaSerifLibre-Bold") return f; - return Resources.GetBuiltinResource("Arial.ttf"); - } - - public static void UpdateDisplay(string title, int current, int total, Color barColor) - { - if (_eventRoot == null) return; - _eventRoot.SetActive(true); - _canvasGroup.alpha = 1f; - - _eventTitleText.text = title.ToUpper(); - _eventProgressText.text = $"{current} / {total}"; - _progressBar.fillAmount = (float)current / total; - _progressBar.color = barColor; - } - - public static void Hide() => _canvasGroup.alpha = 0f; - } -} diff --git a/MxValheim/EventSystem/EventList.cs b/MxValheim/EventSystem/EventList.cs deleted file mode 100644 index c87c91b..0000000 --- a/MxValheim/EventSystem/EventList.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MxValheim.EventSystem -{ - internal class EventList - { - public static void SetupCustomRaid(RandEventSystem system) - { - // 1. Create the Event Data - RandomEvent myCustomRaid = new RandomEvent - { - m_name = "MxEvent_God1", - m_enabled = true, - m_duration = 600f, // 10 minutes - m_nearBaseOnly = true, // Must be near player structures - m_pauseIfNoPlayerInArea = true, - m_forceMusic = "Music_Boss_Moder", // Use specific music - m_startMessage = "The gods themselves challenge you...", - m_endMessage = "The gods are satisfied.", - m_forceEnvironment = "SnowStorm" // Force weather - }; - - // 2. Define what spawns during this event - SpawnSystem.SpawnData frostWraith = new SpawnSystem.SpawnData - { - m_name = "Frost Wraith", - m_prefab = ZNetScene.instance.GetPrefab("Wraith"), // Get the Wraith prefab - m_maxSpawned = 5, // Max alive at once - m_spawnInterval = 10f, // Try to spawn every 10s - m_spawnDistance = 30f, // Distance from player - m_spawnRadiusMax = 10f, - m_spawnRadiusMin = 5f, - m_groupSizeMin = 1, - m_groupSizeMax = 2, - m_enabled = true - }; - - myCustomRaid.m_spawn = new List { frostWraith }; - - // 3. Add to the game's master list - system.m_events.Add(myCustomRaid); - } - } -} diff --git a/MxValheim/EventSystem/Experience.cs b/MxValheim/EventSystem/Experience.cs new file mode 100644 index 0000000..8644367 --- /dev/null +++ b/MxValheim/EventSystem/Experience.cs @@ -0,0 +1,51 @@ +using MxValheim.Patch.HUD; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace MxValheim.EventSystem +{ + internal class Experience + { + public static int GetRequiredExpByLevel(int level) + { + return (level * 100); + } + + public static void AddExperience(string playerid, int amount) + { + ZRoutedRpc.instance.InvokeRoutedRPC(0, "RPC_RequestProfile", playerid); + int level = Profiles.playerLevel; + int exp = Profiles.playerExperience; + exp += amount; + ZRoutedRpc.instance.InvokeRoutedRPC(0, "RPC_ServerSetUserExperience", playerid, exp); + exp = Profiles.playerExperience; + int maxexp = GetRequiredExpByLevel(level); + UpdateProgressBar(playerid); + if (exp >= maxexp) + { + exp -= maxexp; + level++; + ZRoutedRpc.instance.InvokeRoutedRPC(0, "RPC_ServerSetUserLevel", playerid, level); + ZRoutedRpc.instance.InvokeRoutedRPC(0, "RPC_ServerSetUserExperience", playerid, exp); + UpdateProgressBar(playerid); + } + } + + public static void UpdateProgressBar(string playerid) + { + ZRoutedRpc.instance.InvokeRoutedRPC(0, "RPC_RequestProfile", playerid); + float expPercentage = ((float)Profiles.playerExperience / GetRequiredExpByLevel(Profiles.playerLevel)); + + if (Clock.m_barFill != null) + { + Clock.m_barFill.anchorMax = new Vector2(expPercentage, 1); + Clock.m_barFill.offsetMin = Vector2.zero; + Clock.m_barFill.offsetMax = Vector2.zero; + } + } + } +} diff --git a/MxValheim/EventSystem/Patch.cs b/MxValheim/EventSystem/Patch.cs index 159875e..769bc09 100644 --- a/MxValheim/EventSystem/Patch.cs +++ b/MxValheim/EventSystem/Patch.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using Splatform; using System; using System.Collections; using System.Collections.Generic; @@ -16,207 +17,24 @@ namespace MxValheim.EventSystem { internal class EventSystem_Patch { - public static GameObject _eventRoot; - public static Text _eventTitleText; - public static Text _eventProgressText; - public static Image _progressBar; - public static CanvasGroup _canvasGroup; - - // --- STATE DATA --- - public enum RaidPhase - { - None, - Scouts, - EliteGuard, - BossInbound, - Completed - } - public static RaidPhase CurrentPhase = RaidPhase.None; - public static int KillsInCurrentPhase = 0; - // --- 1. HUD CREATION (Your Style) --- - public static void CreateEventHud() - { - if (_eventRoot != null) return; - - _eventRoot = new GameObject("MxEventHUD_Root"); - UnityEngine.Object.DontDestroyOnLoad(_eventRoot); - - Canvas c = _eventRoot.AddComponent(); - c.renderMode = RenderMode.ScreenSpaceOverlay; - c.sortingOrder = 9999; - - CanvasScaler scaler = _eventRoot.AddComponent(); - scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; - scaler.referenceResolution = new Vector2(1920, 1080); - - _canvasGroup = _eventRoot.AddComponent(); - _canvasGroup.alpha = 0f; - - // Background Panel (Positioned -90 to sit below KillFeed at -30) - GameObject panel = new GameObject("Background", typeof(RectTransform), typeof(Image)); - panel.transform.SetParent(_eventRoot.transform, false); - Image panelImage = panel.GetComponent(); - panelImage.color = new Color(0, 0, 0, 0.7f); - - // Styling with Valheim Assets - foreach (Sprite s in Resources.FindObjectsOfTypeAll()) - { - if (s.name == "item_background") - { - panelImage.sprite = s; - panelImage.type = Image.Type.Sliced; - break; - } - } - - RectTransform pRect = panel.GetComponent(); - pRect.anchorMin = pRect.anchorMax = pRect.pivot = new Vector2(0.5f, 1f); - pRect.anchoredPosition = new Vector2(0, -90); // Stacks below your KillFeed - pRect.sizeDelta = new Vector2(400, 55); - - VerticalLayoutGroup layout = panel.AddComponent(); - layout.childAlignment = TextAnchor.MiddleCenter; - layout.spacing = 2f; - layout.padding = new RectOffset(10, 10, 5, 5); - - // Title - _eventTitleText = CreateText(panel.transform, "RAID STATUS", 14, Color.yellow); - - // Progress Bar - GameObject barBg = new GameObject("BarBG", typeof(RectTransform), typeof(Image)); - barBg.transform.SetParent(panel.transform, false); - barBg.GetComponent().color = new Color(0.1f, 0.1f, 0.1f, 0.9f); - barBg.GetComponent().sizeDelta = new Vector2(350, 8); - - GameObject fillObj = new GameObject("Fill", typeof(RectTransform), typeof(Image)); - fillObj.transform.SetParent(barBg.transform, false); - _progressBar = fillObj.GetComponent(); - _progressBar.color = Color.red; - _progressBar.type = Image.Type.Filled; - _progressBar.fillMethod = Image.FillMethod.Horizontal; - _progressBar.GetComponent().anchorMin = Vector2.zero; - _progressBar.GetComponent().anchorMax = Vector2.one; - _progressBar.GetComponent().sizeDelta = Vector2.zero; - - // Progress Text - _eventProgressText = CreateText(panel.transform, "0 / 0", 11, Color.white); - } - - private static Text CreateText(Transform parent, string content, int size, Color col) - { - GameObject txtObj = new GameObject("Text", typeof(Text)); - txtObj.transform.SetParent(parent, false); - Text t = txtObj.GetComponent(); - t.text = content; - t.fontSize = size; - t.color = col; - t.alignment = TextAnchor.MiddleCenter; - - Font valFont = null; - foreach (Font f in Resources.FindObjectsOfTypeAll()) - if (f.name == "AveriaSerifLibre-Bold") valFont = f; - t.font = valFont ?? Resources.GetBuiltinResource("Arial.ttf"); - - return t; - } - - // --- 2. WAVE LOGIC --- - public static void UpdateWave(bool reset = false) - { - Debug.Log($"MxEvent:UpdateWave Seen a wave start."); - if (reset) - { - CurrentPhase = RaidPhase.Scouts; - KillsInCurrentPhase = 0; - } - - int target = GetTargetForPhase(CurrentPhase); - - Color barColor = (CurrentPhase == RaidPhase.Scouts) ? Color.magenta : Color.red; - - UpdateDisplay($"PHASE: {CurrentPhase}", KillsInCurrentPhase, target, barColor); - } - - public static int GetTargetForPhase(RaidPhase phase) - { - switch (phase) - { - case RaidPhase.Scouts: return 5; // 5 kills to clear scouts - case RaidPhase.EliteGuard: return 10; // 10 kills for elite guard - case RaidPhase.BossInbound: return 1; // Just the boss - default: return 0; - } - } - - public static void AdvancePhase() - { - KillsInCurrentPhase = 0; // Reset counter for the new phase - - if (CurrentPhase == RaidPhase.Scouts) CurrentPhase = RaidPhase.EliteGuard; - else if (CurrentPhase == RaidPhase.EliteGuard) CurrentPhase = RaidPhase.BossInbound; - else CurrentPhase = RaidPhase.Completed; - - if (CurrentPhase == RaidPhase.Scouts) - { - // Spawn Eikthyr at the player's location - GameObject boarPrefab = ZNetScene.instance.GetPrefab("Boar"); - if (boarPrefab != null) - { - UnityEngine.Object.Instantiate(boarPrefab, Player.m_localPlayer.transform.position + Vector3.forward * 20f, Quaternion.identity); - } - } - - if (CurrentPhase == RaidPhase.BossInbound) - { - // Spawn Eikthyr at the player's location - GameObject bossPrefab = ZNetScene.instance.GetPrefab("Eikthyr"); - if (bossPrefab != null) - { - UnityEngine.Object.Instantiate(bossPrefab, Player.m_localPlayer.transform.position + Vector3.forward * 10f, Quaternion.identity); - } - } - - Debug.Log($"MxEvent: Advancing to {CurrentPhase}"); - - // Play a sound to notify the player (The 'Quest Update' sound) - Player.m_localPlayer?.Message(MessageHud.MessageType.Center, $"$inventory_newphase: {CurrentPhase}"); - - if (CurrentPhase == RaidPhase.Completed) - { - Debug.Log("MxEvent: All phases complete. Ending vanilla raid."); - EndRaidManually(); - } - } - - public static void UpdateDisplay(string title, int curr, int total, Color col) - { - Debug.Log($"MxEvent:UpdateDisplay Something reached my display update logic."); - if (_eventRoot == null) CreateEventHud(); - _eventRoot.SetActive(true); - _canvasGroup.alpha = 1f; - - _eventTitleText.text = title; - _eventProgressText.text = $"{curr} / {total}"; - _progressBar.fillAmount = (float)curr / total; - _progressBar.color = col; - } - - public static void EndRaidManually() - { - if (RandEventSystem.instance != null) - { - // This tells Valheim the time is up - RandomEvent activeEvent = RandEventSystem.instance.GetActiveEvent(); - if (activeEvent != null) - { - activeEvent.m_time = 0; - } - } - FadeOutHud(); // Start your fade out - } - // --- 3. HARMONY PATCHES --- + [HarmonyPatch(typeof(Player), nameof(Player.Awake))] + public static class PlayerAwakePatch + { + static void Postfix(Player __instance) + { + // Only trigger for the actual human player, not NPCs/Dummies + if (__instance == Player.m_localPlayer) + { + string pid = GetUserPlayerID(Player.m_localPlayer).ToString(); + ZLog.Log($"[ProfileSystem] Spawning finished. Requesting profile for {pid}"); + + ZRoutedRpc.instance.InvokeRoutedRPC(0, "RPC_RequestProfile", pid); + } + } + } + [HarmonyPatch(typeof(RandEventSystem), nameof(RandEventSystem.FixedUpdate))] public static class DebugRaidFrequency { @@ -234,32 +52,7 @@ namespace MxValheim.EventSystem { static void Prefix(RandEventSystem __instance) { - RandomEvent activeEvent = __instance.GetActiveEvent(); - // If we are in our custom phases and haven't reached "Completed" yet - if (activeEvent != null && CurrentPhase != RaidPhase.Completed) - { - // Valheim subtracts time in FixedUpdate. - // We add it back using the engine's global fixedDeltaTime. - activeEvent.m_time += Time.fixedDeltaTime; - } - } - } - - // Ensure HUD is ready when the game UI loads - [HarmonyPatch(typeof(Hud), nameof(Hud.Awake))] - [HarmonyPostfix] - static void InitHud() => CreateEventHud(); - - [HarmonyPatch(typeof(Hud), nameof(Hud.Update))] - [HarmonyPostfix] - static void CheckForExistingRaid() - { - // If the HUD isn't visible but a raid IS active, force it to show - if (CurrentPhase == RaidPhase.None && RandEventSystem.instance.GetActiveEvent() != null) - { - Debug.Log("MxEvent: Detected existing raid on HUD update. Synchronizing..."); - UpdateWave(true); } } @@ -282,13 +75,29 @@ namespace MxValheim.EventSystem _lastKnownEvent = activeEvent; // Force HUD Creation and Display - UpdateWave(true); + //UpdateWave(true); } else if (activeEvent == null && _lastKnownEvent != null) { Debug.Log("MxEvent: HUD Heartbeat detected Raid End."); + string pid = GetUserPlayerID(Player.m_localPlayer).ToString(); + Heightmap.Biome currentBiome = EnvMan.instance.GetCurrentBiome(); + Reward.GiveItemStatic("Coins",Reward.CalculateRewardCoin(Profiles.playerLevel, currentBiome)); + System.Random rand = new System.Random(); + Reward.GiveItemStatic("Feathers", rand.Next(1,10)); + if (rand.Next(100) < 50) + { + Reward.GiveItemRandom(Reward.GetRewardPrefab(Profiles.playerLevel, "food"), 1); + } + if (rand.Next(100) < 25) + { + Reward.GiveItemRandom(Reward.GetRewardPrefab(Profiles.playerLevel, "heal"), 1); + } + if (rand.Next(100) < 10) + { + Reward.GiveItemRandom(Reward.GetRewardPrefab(Profiles.playerLevel, "metal"), 1); + } _lastKnownEvent = null; - FadeOutHud(); } } } @@ -312,85 +121,41 @@ namespace MxValheim.EventSystem Debug.Log("MxEvent: Confirmed Active Event!"); Vector3 playerPos = Player.m_localPlayer.transform.position; + string pid = GetUserPlayerID(Player.m_localPlayer).ToString(); + Debug.Log($"EventSystem:ApplyDamage User {pid}"); float distance = Vector3.Distance(playerPos, activeEvent.m_pos); Debug.Log($"MxEvent: Distance: {distance}"); if (distance <= 96f) { - KillsInCurrentPhase++; - int target = GetTargetForPhase(CurrentPhase); + int level = __instance.GetLevel(); + int exp = + (level == 1) ? 2: + (level == 2) ? 3: + (level == 3) ? 4: + 5; - if (KillsInCurrentPhase >= target && CurrentPhase != RaidPhase.Completed) - { - AdvancePhase(); - } + Experience.AddExperience(pid, exp); - // Send the update + /* Send the update ZRoutedRpc.instance.InvokeRoutedRPC( ZRoutedRpc.Everybody, "RPC_UpdateRaidHUD", $"PHASE: {CurrentPhase}", KillsInCurrentPhase, GetTargetForPhase(CurrentPhase) - ); + );*/ } } } } - public static bool IsEventCreature(Character character) - { - ZNetView nview = character.GetComponent(); - if (nview == null || nview.GetZDO() == null) return false; - - // Layer 1: Check the networked boolean (The standard way) - if (nview.GetZDO().GetBool("eventcreature")) return true; - - // Layer 2: Check the BaseAI component (The local way) - BaseAI ai = character.GetComponent(); - if (ai != null) - { - bool isRaidMob = (bool)AccessTools.Field(typeof(BaseAI), "m_eventCreature").GetValue(ai); - if (isRaidMob) return true; - } - - return false; - } - // Hide HUD when raid ends [HarmonyPatch(typeof(RandEventSystem), nameof(RandEventSystem.ResetRandomEvent))] [HarmonyPostfix] static void OnRaidEnd() { - FadeOutHud(); - CurrentPhase = RaidPhase.None; - } - - private static IEnumerator FadeOutHud() - { - Debug.Log("MxEvent: Starting Fade Out..."); - - // Safety check for the component - if (_canvasGroup == null) - { - _eventRoot.SetActive(false); - yield break; - } - - float startAlpha = _canvasGroup.alpha; - float rate = 1.0f / 1.5f; // Fade over 1.5 seconds - float progress = 0.0f; - - while (progress < 1.0f) - { - progress += Time.deltaTime * rate; - _canvasGroup.alpha = Mathf.Lerp(startAlpha, 0, progress); - yield return null; - } - - _canvasGroup.alpha = 0; - _eventRoot.SetActive(false); // Fully disable to ensure it's gone - Debug.Log("MxEvent: HUD Hidden and Deactivated."); + } } } diff --git a/MxValheim/EventSystem/Profiles.cs b/MxValheim/EventSystem/Profiles.cs new file mode 100644 index 0000000..2c483f0 --- /dev/null +++ b/MxValheim/EventSystem/Profiles.cs @@ -0,0 +1,167 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MxValheim.EventSystem +{ + internal class Profiles + { + public class ProfileRoot + { + public string playerid { get; set; } + public string player_name { get; set; } + public int level { get; set; } + public int experience { get; set; } + } + + public static bool userExists = false; + public static int playerLevel = 1; + public static int playerExperience = 0; + + + public static void ServerUserExists(string playerid) + { + // Check to ensure this is actually the server running this + ZNet zn = new ZNet(); + if (!zn.IsDedicated()) return; + string profilePath = Path.Combine(MxValheimMod.internalDataEventSystemPath, $"{playerid}.json"); + + if (File.Exists(profilePath)) userExists = true; + } + + public static void ServerReceiveCreateUser(long sender, string playerid, string playerName) + { + // Check to ensure this is actually the server running this + ZNet zn = new ZNet(); + if (!zn.IsDedicated()) return; + + string profilePath = Path.Combine(MxValheimMod.internalDataEventSystemPath, $"{playerid}.json"); + + ProfileRoot pr = new ProfileRoot + { + playerid = playerid, + player_name = playerName, + level = 1, + experience = 0 + }; + + var raw = JsonConvert.SerializeObject(pr, Formatting.Indented); + File.WriteAllText(profilePath, raw); + } + + public static void ServerHandleProfileRequest(long sender, string playerid) + { + // Check to ensure this is actually the server running this + ZNet zn = new ZNet(); + if (!zn.IsDedicated()) return; + + ZNetPeer peer = ZNet.instance.GetPeer(sender); + + string profilePath = Path.Combine(MxValheimMod.internalDataEventSystemPath, $"{playerid}.json"); + + if (File.Exists(profilePath)) + { + string rawJson = File.ReadAllText(profilePath); + // Send ONLY to the 'sender' who requested it + ZRoutedRpc.instance.InvokeRoutedRPC(sender, "RPC_ClientReceiveProfile", rawJson); + } + else + { + ProfileRoot pr = new ProfileRoot + { + playerid = playerid, + player_name = peer.m_playerName, + level = 1, + experience = 0 + }; + + var raw = JsonConvert.SerializeObject(pr, Formatting.Indented); + File.WriteAllText(profilePath, raw); + } + } + + public static void RequestCreateUser(string playerid, Player player) + { + string playerName = player.GetHoverName(); + ZRoutedRpc.instance.InvokeRoutedRPC(0, "RPC_ServerCreateUser", playerid, playerName); + } + + public static void ClientLoadProfile(long sender, string json) + { + ProfileRoot loadedProfile = JsonConvert.DeserializeObject(json); + + playerLevel = loadedProfile.level; + playerExperience = loadedProfile.experience; + + //ZLog.Log($"[Client] Profile loaded! Level: {loadedProfile.level}"); + } + + public static void CreateUser(string playerid, Player player) + { + // We only send the raw data; we let the server handle the file I/O + string playerName = player?.GetHoverName() ?? "Unknown"; + + // Invoke the RPC on the Server (0 is the server ID) + ZRoutedRpc.instance.InvokeRoutedRPC(0, "RPC_ServerCreateUser", playerid, playerName); + } + + public static void ServerSetUserExperience(long sender, string playerid, int exp) + { + // Check to ensure this is actually the server running this + ZNet zn = new ZNet(); + if (!zn.IsDedicated()) return; + + string profilePath = Path.Combine(MxValheimMod.internalDataEventSystemPath, $"{playerid}.json"); + ProfileRoot pro = JsonConvert.DeserializeObject(File.ReadAllText(profilePath)); + ProfileRoot pr = new ProfileRoot + { + playerid = playerid, + player_name = pro.player_name, + level = pro.level, + experience = exp + }; + var raw = JsonConvert.SerializeObject(pr, Formatting.Indented); + File.WriteAllText(profilePath, raw); + ZRoutedRpc.instance.InvokeRoutedRPC(0, "RPC_RequestProfile", playerid); + } + + public static void SetUserExperience(string playerid, int exp) + { + string profilePath = Path.Combine(MxValheimMod.internalDataEventSystemPath, $"{playerid}.json"); + ProfileRoot pro = JsonConvert.DeserializeObject(File.ReadAllText(profilePath)); + ProfileRoot pr = new ProfileRoot + { + playerid = playerid, + player_name = pro.player_name, + level = pro.level, + experience = exp + }; + var raw = JsonConvert.SerializeObject(pr, Formatting.Indented); + File.WriteAllText(profilePath, raw); + } + + public static void ServerSetUserLevel(long sender, string playerid, int level) + { + // Check to ensure this is actually the server running this + ZNet zn = new ZNet(); + if (!zn.IsDedicated()) return; + + string profilePath = Path.Combine(MxValheimMod.internalDataEventSystemPath, $"{playerid}.json"); + ProfileRoot pro = JsonConvert.DeserializeObject(File.ReadAllText(profilePath)); + ProfileRoot pr = new ProfileRoot + { + playerid = playerid, + player_name = pro.player_name, + level = level, + experience = pro.experience + }; + var raw = JsonConvert.SerializeObject(pr, Formatting.Indented); + File.WriteAllText(profilePath, raw); + ZRoutedRpc.instance.InvokeRoutedRPC(0, "RPC_RequestProfile", playerid); + } + } +} diff --git a/MxValheim/EventSystem/Reward.cs b/MxValheim/EventSystem/Reward.cs new file mode 100644 index 0000000..5ece29a --- /dev/null +++ b/MxValheim/EventSystem/Reward.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace MxValheim.EventSystem +{ + internal class Reward + { + public static int CalculateRewardCoin(int level, Heightmap.Biome biome) + { + Dictionary biomeBase = new Dictionary() + { + { 1, 10 }, + { 2, 20 }, + { 4, 30 }, + { 8, 40 }, + { 16, 50 }, + { 512, 60 }, + { 64, 70 }, + }; + + int rewardCoin = biomeBase[((int)biome)] * level; + + return rewardCoin; + } + + public static string GetRewardPrefab(int level, string type) + { + string[] foodList = { "OnionSoup", "CarrotSoup", "DeerStew", "Salad", "SerpentStew", "WolfMeatSkewer" }; + string[] healList = { "MeadHealthMinor", "MeadHealthMedium", "MeadHealthMajor", "MeadHealthLingering" }; + string[] metalList = { "CopperOre", "TinOre", "IronScrap", "SilverOre", "BlackMetalScrap", "FlametalOreNew" }; + + System.Random rng = new System.Random(); + int foodTier = rng.Next(1, foodList.Length); + int healTier = rng.Next(1, healList.Length); + int metalTier = rng.Next(1, metalList.Length); + + if (type == "food") return foodList[foodTier]; + if (type == "heal") return healList[healTier]; + if (type == "metal") return metalList[metalTier]; + return "Wood"; + } + + public static void GiveItemRandom(string itemName, int qty) + { + System.Random rng = new System.Random(); + + Player player = Player.m_localPlayer; + if (player == null) return; + + GameObject prefab = ObjectDB.instance.GetItemPrefab(itemName); + if (prefab != null) + { + player.GetInventory().AddItem(itemName, qty, 1, 0, 0, ""); + + player.Message(MessageHud.MessageType.TopLeft, $"Received {qty}x {itemName}"); + } + else + { + Debug.LogWarning($"Item {itemName} not found in ObjectDB!"); + } + } + + public static void GiveItemStatic(string itemName, int qty) + { + Player player = Player.m_localPlayer; + if (player == null) return; + + GameObject prefab = ObjectDB.instance.GetItemPrefab(itemName); + if (prefab != null) + { + player.GetInventory().AddItem(itemName, qty, 1, 0, 0, ""); + + player.Message(MessageHud.MessageType.TopLeft, $"Received {qty}x {itemName}"); + } + else + { + Debug.LogWarning($"Item {itemName} not found in ObjectDB!"); + } + } + + + } +} diff --git a/MxValheim/EventSystem/WaveManager.cs b/MxValheim/EventSystem/WaveManager.cs deleted file mode 100644 index ad12401..0000000 --- a/MxValheim/EventSystem/WaveManager.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using UnityEngine; - -namespace MxValheim.EventSystem -{ - public enum RaidPhase { None, Scouts, Siege, Boss, Victory } - - public static class WaveManager - { - public static RaidPhase CurrentPhase = RaidPhase.None; - public static int KillsInCurrentPhase = 0; - public static int TotalInPhase = 20; - - public static void AdvanceWave() - { - KillsInCurrentPhase = 0; - CurrentPhase++; - - switch (CurrentPhase) - { - case RaidPhase.Scouts: - // - break; - case RaidPhase.Siege: - // - break; - case RaidPhase.Boss: - // - //SpawnBoss(); - break; - case RaidPhase.Victory: - // - break; - } - } - - public static void UpdateWave(bool reset = false) - { - if (reset) - { - CurrentPhase = RaidPhase.Scouts; - KillsInCurrentPhase = 0; - } - - int target = GetTargetForWave(CurrentPhase); - Color barColor = (CurrentPhase == RaidPhase.Boss) ? Color.magenta : Color.red; - - EventHud.UpdateDisplay($"PHASE: {CurrentPhase}", KillsInCurrentPhase, target, barColor); - } - - private static int GetTargetForWave(RaidPhase phase) - { - switch (phase) - { - case RaidPhase.Scouts: return 5; - case RaidPhase.Siege: return 15; - case RaidPhase.Boss: return 1; - default: return 0; - } - } - } -} diff --git a/MxValheim/MxValheim.cs b/MxValheim/MxValheim.cs index da6fa9e..484575a 100644 --- a/MxValheim/MxValheim.cs +++ b/MxValheim/MxValheim.cs @@ -6,6 +6,7 @@ using MxValheim.KillFeed; using MxValheim.Patch; using MxValheim.Patch.HUD; using Newtonsoft.Json; +using Splatform; using System; using System.Collections.Generic; using System.IO; @@ -40,11 +41,15 @@ public class MxValheimMod : BaseUnityPlugin public static string modPath = Path.Combine(Paths.PluginPath, "MxValheim"); public static string internalConfigsPath = Path.Combine(modPath, "Configs"); + public static string internalDataPath = Path.Combine(modPath, "Data"); + public static string internalDataEventSystemPath = Path.Combine(internalDataPath, "EventSystem"); public static string WeightConfigPath => Path.Combine(internalConfigsPath, "items_weight.json"); public static string AutoDoorConfigPath => Path.Combine(internalConfigsPath, "auto_doors.json"); public static DoorRoot CachedConfig; public static Dictionary WeightSettings = new Dictionary(); + public static bool initExpBar = true; + // Data structures public class KillData { @@ -80,8 +85,8 @@ public class MxValheimMod : BaseUnityPlugin { Instance = this; - Config_OreMultiplier = Config.Bind("General","OreMultiplier",3,"How many items should drop for every 1 ore/scrap found."); - Config_rangeMultiplier = Config.Bind("General", "CraftingRangeMultiplier",2.0f,"Multiplier for the workbench build/crafting range. Default is 2x."); + Config_OreMultiplier = Config.Bind("General", "OreMultiplier", 3, "How many items should drop for every 1 ore/scrap found."); + Config_rangeMultiplier = Config.Bind("General", "CraftingRangeMultiplier", 2.0f, "Multiplier for the workbench build/crafting range. Default is 2x."); Config_bowDrawSpeedBonusPerLevel = Config.Bind("General", "BowDrawSpeedBonusPercentPerLevel", 1.0f, "Shorten the bow draw speed by this percent for every bow upgrade level."); Config_rainDamage = Config.Bind("General", "RainDamage", true, "Set to true to stop rain damage, false to return to vanilla behavior."); Config_boatSpeed = Config.Bind("General", "BoatSpeedMultiplier", 2.0f, "Your boat/raft will move without wind at a speed multiplied by this value."); @@ -98,20 +103,6 @@ public class MxValheimMod : BaseUnityPlugin harmony.PatchAll(); } - public static void RPC_UpdateRaidHUD(long sender, string phaseName, int current, int total) - { - Color phaseColor; - switch (phaseName) - { - case "SCOUTS": phaseColor = Color.green; break; - case "ELITEGUARD": phaseColor = Color.yellow; break; - case "BOSSINBOUND": phaseColor = Color.red; break; - default: phaseColor = Color.white; break; - } - - EventSystem_Patch.UpdateDisplay(phaseName, current, total, phaseColor); - } - [HarmonyPatch(typeof(Localization), nameof(Localization.SetupLanguage))] public static class Localization_SetupLanguage_Patch { @@ -130,7 +121,7 @@ public class MxValheimMod : BaseUnityPlugin string text = __instance.m_input.text; if (text.ToLower() == "listicons") { - + } } } @@ -141,26 +132,30 @@ public class MxValheimMod : BaseUnityPlugin static void Postfix() { if (ZRoutedRpc.instance != null && kfp != null) - { + { // We use the explicit 'Method' delegate to avoid the ArgumentException ZRoutedRpc.instance.Register("RPC_MxKillMsg", new RoutedMethod.Method(kfp.OnReceiveKillMsg)); - Debug.Log("MxValheimMod: RPC Registered successfully with explicit delegate."); - } - - if (ZRoutedRpc.instance != null) - { - ZRoutedRpc.instance.Register("RPC_UpdateRaidHUD", RPC_UpdateRaidHUD); - Debug.Log("MxRaid: RPC Registered successfully."); - } - - EventSystem_Patch.CreateEventHud(); + ZRoutedRpc.instance.Register("RPC_ServerCreateUser", Profiles.ServerReceiveCreateUser); + ZRoutedRpc.instance.Register("RPC_RequestProfile", Profiles.ServerHandleProfileRequest); + ZRoutedRpc.instance.Register("RPC_ClientReceiveProfile", Profiles.ClientLoadProfile); + ZRoutedRpc.instance.Register("RPC_UpdateProgressBar", Profiles.ClientLoadProfile); + ZRoutedRpc.instance.Register("RPC_ServerSetUserExperience", Profiles.ServerSetUserExperience); + ZRoutedRpc.instance.Register("RPC_ServerSetUserLevel", Profiles.ServerSetUserLevel); + } } } void Update() { + // Only run if we are actually in the game world + if (Player.m_localPlayer != null) + { + string pid = GetUserPlayerID(Player.m_localPlayer).ToString(); + Experience.UpdateProgressBar(pid); + } + // Only run if we are actually in the game world if (Player.m_localPlayer != null) { @@ -179,6 +174,12 @@ public class MxValheimMod : BaseUnityPlugin { Clock clk = new Clock(); clk.CreateOverlay(); + if (initExpBar) + { + string pid = GetUserPlayerID(Player.m_localPlayer).ToString(); + Experience.UpdateProgressBar(pid); + initExpBar = false; + } } if (Clock.m_textComponent != null) @@ -186,13 +187,14 @@ public class MxValheimMod : BaseUnityPlugin Clock clk = new Clock(); string timeString = clk.GetFormattedTime(); int day = EnvMan.instance.GetDay(); - Clock.m_textComponent.text = $"{Localization.instance.Localize("$clock_string_day")} {day} | {timeString}"; + string pid = GetUserPlayerID(Player.m_localPlayer).ToString(); + Clock.m_textComponent.text = $"{Localization.instance.Localize("$clock_string_rank")} {Profiles.playerLevel} | {Localization.instance.Localize("$clock_string_day")} {day} | {timeString}"; } - } + } // Door Logic if (_doorQueueNview.Count > 0) - { + { if (_doorTimer > 0) { _doorTimer -= Time.deltaTime; @@ -203,7 +205,7 @@ public class MxValheimMod : BaseUnityPlugin dpatch.CloseNextDoor(_doorQueueDoor.Dequeue(), _doorQueueNview.Dequeue(), _doorQueueName.Dequeue()); _doorTimer = MxValheimMod.Config_autoDoorClose.Value; } - } + } // Door Logic ///////////// @@ -249,14 +251,12 @@ public class MxValheimMod : BaseUnityPlugin } // KillFeed Logic ///////////////// + } - if (RandEventSystem.instance.GetActiveEvent() == null) - { - EventSystem_Patch._canvasGroup.alpha = 0; - EventSystem_Patch._eventRoot.SetActive(false); - } - - } + public static long GetUserPlayerID(Player player) + { + return player.GetPlayerID(); + } public static void LoadLocalization() { diff --git a/MxValheim/MxValheim.csproj b/MxValheim/MxValheim.csproj index 1c84a64..730b7bb 100644 --- a/MxValheim/MxValheim.csproj +++ b/MxValheim/MxValheim.csproj @@ -35,9 +35,16 @@ E:\SteamLibrary\steamapps\common\Valheim\BepInEx\core\0Harmony.dll + + E:\SteamLibrary\steamapps\common\Valheim\Valheim_Data\Managed\assembly_googleanalytics.dll + E:\SteamLibrary\steamapps\common\Valheim\Valheim_Data\Managed\publicized_assemblies\assembly_guiutils_publicized.dll + + False + E:\SteamLibrary\steamapps\common\Valheim\Valheim_Data\Managed\assembly_utils.dll + E:\SteamLibrary\steamapps\common\Valheim\Valheim_Data\Managed\publicized_assemblies\assembly_valheim_publicized.dll @@ -91,10 +98,10 @@ - - + - + + @@ -109,7 +116,9 @@ + + diff --git a/MxValheim/Patch/HUD/Clock.cs b/MxValheim/Patch/HUD/Clock.cs index 81ead0f..bee2457 100644 --- a/MxValheim/Patch/HUD/Clock.cs +++ b/MxValheim/Patch/HUD/Clock.cs @@ -12,6 +12,7 @@ namespace MxValheim.Patch.HUD { public static GameObject m_canvasObject; public static Text m_textComponent; + public static RectTransform m_barFill; // This replaces the missing GetTimeString() method public string GetFormattedTime() @@ -29,68 +30,62 @@ namespace MxValheim.Patch.HUD public void CreateOverlay() { - // 1. Root Canvas m_canvasObject = new GameObject("ModdedCanvas"); Canvas canvas = m_canvasObject.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 999; - // 2. Background Panel + // --- CLOCK PANEL --- GameObject panelObj = new GameObject("TextBackground"); panelObj.transform.SetParent(m_canvasObject.transform, false); + panelObj.AddComponent().color = new Color(0, 0, 0, 0.5f); - Image panelImage = panelObj.AddComponent(); - panelImage.color = new Color(0, 0, 0, 0.5f); // 50% transparent black + ContentSizeFitter fitter = panelObj.AddComponent(); + fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + HorizontalLayoutGroup layout = panelObj.AddComponent(); + layout.padding = new RectOffset(15, 15, 5, 5); RectTransform panelRect = panelObj.GetComponent(); - panelRect.anchorMin = new Vector2(0.5f, 1.0f); // Top Center + panelRect.anchorMin = new Vector2(0.5f, 1.0f); panelRect.anchorMax = new Vector2(0.5f, 1.0f); panelRect.pivot = new Vector2(0.5f, 1.0f); - panelRect.anchoredPosition = new Vector2(0, -5); // 5px gap from top - panelRect.sizeDelta = new Vector2(180, 35); // Width and Height of the bar + panelRect.anchoredPosition = new Vector2(0, -5); - // 3. Text (Attached to Panel) + // --- TEXT --- GameObject textObj = new GameObject("ModdedText"); textObj.transform.SetParent(panelObj.transform, false); - m_textComponent = textObj.AddComponent(); - // Attempt to find Valheim's specific font, fallback to Arial if not found - Font valheimFont = null; - foreach (Font f in Resources.FindObjectsOfTypeAll()) - { - if (f.name == "AveriaSerifLibre-Bold") - { - valheimFont = f; - break; - } - } - - // Corrected fallback for older Unity versions used in Valheim - if (valheimFont == null) - { - valheimFont = Resources.GetBuiltinResource("Arial.ttf"); - } - - m_textComponent.font = valheimFont; + m_textComponent.font = Resources.GetBuiltinResource("Arial.ttf"); m_textComponent.fontSize = 18; m_textComponent.color = Color.white; - m_textComponent.alignment = TextAnchor.MiddleCenter; - m_textComponent.supportRichText = true; - // Add Shadow so it's visible in snow - var shadow = textObj.AddComponent(); - shadow.effectColor = Color.black; - shadow.effectDistance = new Vector2(2, 2); + // --- PROGRESS BAR CONTAINER --- + // This sits under the main rectangle + GameObject barBgObj = new GameObject("BarBackground"); + barBgObj.transform.SetParent(m_canvasObject.transform, false); + barBgObj.AddComponent().color = new Color(0.1f, 0.1f, 0.1f, 0.8f); // Dark background - // FIX: Correct way to make text fill the parent panel - RectTransform textRect = textObj.GetComponent(); - textRect.anchorMin = Vector2.zero; // (0, 0) - textRect.anchorMax = Vector2.one; // (1, 1) - textRect.pivot = new Vector2(0.5f, 0.5f); + RectTransform barBgRect = barBgObj.GetComponent(); + barBgRect.anchorMin = new Vector2(0.5f, 1.0f); + barBgRect.anchorMax = new Vector2(0.5f, 1.0f); + barBgRect.pivot = new Vector2(0.5f, 1.0f); + // Positioned below the clock panel (Clock is 35px high + 5px gap = -40) + barBgRect.anchoredPosition = new Vector2(0, -45); + barBgRect.sizeDelta = new Vector2(180, 8); // Match width of clock, small height - // Resetting these to zero ensures the text box matches the panel exactly - textRect.offsetMin = Vector2.zero; - textRect.offsetMax = Vector2.zero; + // --- PROGRESS BAR FILL --- + GameObject fillObj = new GameObject("BarFill"); + fillObj.transform.SetParent(barBgObj.transform, false); + fillObj.AddComponent().color = Color.yellow; // Classic Valheim XP color + + m_barFill = fillObj.GetComponent(); + m_barFill.anchorMin = new Vector2(0, 0); + m_barFill.anchorMax = new Vector2(0.5f, 1); // This X value (0.5) will be updated via code + m_barFill.pivot = new Vector2(0, 0.5f); + m_barFill.offsetMin = Vector2.zero; + m_barFill.offsetMax = Vector2.zero; } } } diff --git a/README.md b/README.md index 6fbd24b..33500cf 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Official **MxValheim Server** Mod. **This mod is created to be used Client/Server side. A lot of features are not working in solo mode.** ## Features +- Enhanced Event System with level and reward based on current biome. - Display the day and the formated time in-game. - Use your client language with built-in localization. - Kill Feed with custom UI showing player kill and death.