using UnityEngine.EventSystems;
public class ClickDebug : MonoBehaviour
if (EventSystem.current.IsPointerOverGameObject())
if (Input.GetMouseButtonDown(0))
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
Debug.Log($"Hit object: {hit.collider.gameObject.name} on layer: {LayerMask.LayerToName(hit.collider.gameObject.layer)}");
ItemPickup itemPickup = hit.collider.GetComponent<ItemPickup>();
Debug.Log($"Found ItemPickup: {itemPickup.item.itemName}");
ResourceNode resourceNode = hit.collider.GetComponent<ResourceNode>();
if (resourceNode != null)
Debug.Log($"Found ResourceNode: {resourceNode.nodeName}");
Debug.Log("No interactable component on this object");
using UnityEngine.EventSystems;
public class ClickIndicator : MonoBehaviour
public GameObject clickMarkerPrefab;
public float markerLifetime = 0.5f;
public LayerMask clickableLayers;
public LayerMask groundLayer;
private Camera mainCamera;
mainCamera = Camera.main;
Debug.LogError("Canvas is not assigned in ClickIndicator script!");
if (Input.GetMouseButtonDown(0))
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit, Mathf.Infinity, clickableLayers))
if (Physics.Raycast(ray, out hit, Mathf.Infinity, groundLayer))
CreateClickMarker(hit.point);
private void CreateClickMarker(Vector3 worldPosition)
GameObject marker = Instantiate(clickMarkerPrefab, canvas.transform);
Vector2 screenPoint = mainCamera.WorldToScreenPoint(worldPosition);
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.GetComponent<RectTransform>(),
RectTransform markerRect = marker.GetComponent<RectTransform>();
markerRect.localPosition = localPoint;
Destroy(marker, markerLifetime);
using UnityEngine.EventSystems;
public class ClickToMove : MonoBehaviour
[Header("Movement Settings")]
private NavMeshAgent navMeshAgent;
public GameObject destinationMarkerPrefab;
private GameObject destinationMarker;
public float interactionRange = 1f;
[Header("Layer Settings")]
public LayerMask clickableLayers;
public LayerMask groundLayer;
private ResourceNode targetResourceNode;
private Woodcutting woodcuttingSkill;
private Mining miningSkill;
private CombatHandler combatHandler;
private bool isMovingToDestination = false;
private bool isContextMenuMove = false;
navMeshAgent = GetComponent<NavMeshAgent>();
woodcuttingSkill = GetComponent<Woodcutting>();
miningSkill = GetComponent<Mining>();
combatHandler = GetComponent<CombatHandler>();
InitializeDestinationMarker();
Debug.Log($"ClickToMove initialized. NavMeshAgent: {navMeshAgent != null}, Ground Layer: {LayerMask.LayerToName(groundLayer)}");
private void InitializeDestinationMarker()
if (destinationMarkerPrefab != null)
destinationMarker = Instantiate(destinationMarkerPrefab);
destinationMarker.SetActive(false);
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
if (combatHandler != null && combatHandler.InCombat)
if (Input.GetMouseButtonDown(0))
if (targetResourceNode != null)
CheckResourceProximity();
CheckDestinationReached();
private void HandleMouseClick()
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit, Mathf.Infinity, clickableLayers))
Debug.Log($"Clicked on object: {hit.collider.gameObject.name} on layer: {LayerMask.LayerToName(hit.collider.gameObject.layer)}");
WorldInteractable interactable = hit.collider.GetComponent<WorldInteractable>();
if (interactable != null)
StopMovementAndActions();
ResourceNode resourceNode = hit.collider.GetComponent<ResourceNode>();
if (resourceNode != null)
targetResourceNode = resourceNode;
MoveToPosition(hit.point);
if (Physics.Raycast(ray, out hit, Mathf.Infinity, groundLayer))
Debug.Log($"Clicked on ground at position: {hit.point}");
StopMovementAndActions();
targetResourceNode = null;
MoveToPosition(hit.point);
private void MoveToPosition(Vector3 position)
if (navMeshAgent == null)
Debug.LogError("NavMeshAgent is null!");
if (NavMesh.SamplePosition(position, out NavMeshHit navHit, 2f, NavMesh.AllAreas))
Debug.Log($"Moving to position: {navHit.position}");
navMeshAgent.stoppingDistance = targetResourceNode != null ? interactionRange : 0.1f;
navMeshAgent.SetDestination(navHit.position);
isMovingToDestination = true;
if (destinationMarker != null)
destinationMarker.transform.position = navHit.position + new Vector3(0, 0.1f, 0);
destinationMarker.SetActive(true);
Debug.LogWarning("Could not find valid NavMesh position!");
private void CheckResourceProximity()
if (targetResourceNode == null || !targetResourceNode.IsValidForGathering())
StopMovementAndActions();
float distanceToResource = Vector3.Distance(transform.position, targetResourceNode.transform.position);
if (distanceToResource <= interactionRange)
navMeshAgent.isStopped = true;
private void CheckDestinationReached()
if (!isMovingToDestination || navMeshAgent.pathStatus == NavMeshPathStatus.PathInvalid)
if (!navMeshAgent.pathPending)
if (navMeshAgent.remainingDistance <= navMeshAgent.stoppingDistance)
if (!navMeshAgent.hasPath || navMeshAgent.velocity.sqrMagnitude == 0f)
isMovingToDestination = false;
private void AttemptGather()
if (targetResourceNode is ChoppableTree && woodcuttingSkill != null)
woodcuttingSkill.SetTarget(targetResourceNode);
else if (targetResourceNode is MineableRock && miningSkill != null)
miningSkill.SetTarget(targetResourceNode);
public void StopMovementAndActions()
if (navMeshAgent != null)
navMeshAgent.ResetPath();
targetResourceNode = null;
woodcuttingSkill?.StopAction();
miningSkill?.StopAction();
private void HideDestinationMarker()
if (destinationMarker != null)
destinationMarker.SetActive(false);
isMovingToDestination = false;
public void MoveToTarget(Vector3 targetPosition, bool fromContextMenu = false)
StopMovementAndActions();
isContextMenuMove = fromContextMenu;
MoveToPosition(targetPosition);
private void OnDrawGizmosSelected()
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, interactionRange);
public bool IsGatheringInProgress()
return (woodcuttingSkill != null && woodcuttingSkill.IsPerformingAction) ||
(miningSkill != null && miningSkill.IsPerformingAction);
public class CombatBonuses
public int spellweavingAttack;
public int spellweavingDefense;
public int rangeStrengthBonus;
public int spellweavingDamage;
public int divinityBonus;
public float attackSpeed = 2.4f;
public AttackStyle attackStyle;
public void ResetBonuses()
public int GetAttackBonus(AttackStyle style)
AttackStyle.Stab => stabAttack,
AttackStyle.Slash => slashAttack,
AttackStyle.Crush => crushAttack,
AttackStyle.Spellweaving => spellweavingAttack,
AttackStyle.Range => rangeAttack,
public int GetDefenseBonus(AttackStyle style)
AttackStyle.Stab => stabDefense,
AttackStyle.Slash => slashDefense,
AttackStyle.Crush => crushDefense,
AttackStyle.Spellweaving => spellweavingDefense,
AttackStyle.Range => rangeDefense,
public enum CombatTrainingStyle
public static class CombatCalculator
private const float MIN_PLAYER_HIT = 10f;
private const float COMBAT_TRIANGLE_BONUS = 1.5f;
private const float COMBAT_TRIANGLE_PENALTY = 0.6f;
public static float CalculateAccuracy(ICombatant attacker, ICombatant defender, WeaponAttackStyle style)
float attackerRoll = CalculateAttackRoll(attacker, style);
float defenderRoll = CalculateDefenceRoll(defender, attacker.GetCurrentCombatStyle());
float triangleMultiplier = GetCombatTriangleMultiplier(
attacker.GetCurrentCombatStyle(),
defender.GetCurrentCombatStyle()
float finalAccuracy = (attackerRoll * triangleMultiplier) /
(attackerRoll * triangleMultiplier + defenderRoll);
return Mathf.Clamp01(finalAccuracy);
private static float CalculateAttackRoll(ICombatant attacker, WeaponAttackStyle style)
CombatStats stats = attacker.Stats;
float effectiveLevel = stats.GetAttackSkillLevel(style.attackStyle);
float equipmentBonus = stats.combatBonuses.GetAttackBonus(style.attackStyle);
return effectiveLevel * (1 + equipmentBonus/100f) * style.accuracyModifier;
private static float CalculateDefenceRoll(ICombatant defender, AttackStyle attackStyle)
CombatStats stats = defender.Stats;
float fortitudeLevel = stats.Fortitude;
float defenseBonus = stats.GetDefenseBonus(attackStyle);
return fortitudeLevel * (1 + defenseBonus/100f);
public static int CalculateDamage(ICombatant attacker, bool isPlayerAttacking)
int maxHit = attacker.Stats.CalculateMaxHit(attacker.GetCurrentCombatStyle());
float randomFactor = Random.value;
Mathf.FloorToInt(MIN_PLAYER_HIT + (maxHit - MIN_PLAYER_HIT) * randomFactor)
return Mathf.FloorToInt(maxHit * randomFactor);
private static float GetCombatTriangleMultiplier(AttackStyle attackerStyle, AttackStyle defenderStyle)
if ((attackerStyle == AttackStyle.Spellweaving && defenderStyle == AttackStyle.Range) ||
(attackerStyle == AttackStyle.Range && defenderStyle == AttackStyle.Stab) ||
(attackerStyle == AttackStyle.Stab && defenderStyle == AttackStyle.Spellweaving))
return COMBAT_TRIANGLE_BONUS;
if ((attackerStyle == AttackStyle.Range && defenderStyle == AttackStyle.Spellweaving) ||
(attackerStyle == AttackStyle.Stab && defenderStyle == AttackStyle.Range) ||
(attackerStyle == AttackStyle.Spellweaving && defenderStyle == AttackStyle.Stab))
return COMBAT_TRIANGLE_PENALTY;
[CreateAssetMenu(fileName = "New Combat Config", menuName = "Combat/Combat Configuration")]
public class CombatConfig : ScriptableObject
public AttackStyle attackStyle;
public CombatTrainingStyle trainingStyle;
public bool splitXPWithFortitude;
public float accuracyModifier = 1f;
public float damageModifier = 1f;
public float experienceMultiplier = 4f;
[Header("Primary Combat Configuration")]
public WeaponStyle primaryStyle;
public float baseAttackInterval = 2.4f;
public float weaponSpeedModifier = 1f;
[Header("Combat Experience Settings")]
public float vitalityXPMultiplier = 1.33f;
public float vitalityXPPerDamage = 50.54f;
[Header("Combat Triangle Settings")]
public float strongAgainstMultiplier = 1.5f;
public float weakAgainstMultiplier = 0.6f;
[Header("Base Combat Settings")]
public int minimumHit = 10;
public float baseRegenRate = 0.01f;
public float maxAccuracy = 0.99f;
public float GetAttackInterval() => baseAttackInterval * weaponSpeedModifier;
public float CalculateVitalityXP(float damageDealt, AttackStyle style)
float xpMultiplier = style switch
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush => 0.25f,
AttackStyle.Range => 0.333f,
AttackStyle.Spellweaving => 1f,
return damageDealt * vitalityXPPerDamage * xpMultiplier;
public float GetCombatTriangleMultiplier(AttackStyle attackerStyle, AttackStyle defenderStyle)
if ((attackerStyle == AttackStyle.Spellweaving && defenderStyle == AttackStyle.Range) ||
(attackerStyle == AttackStyle.Range && defenderStyle == AttackStyle.Stab) ||
(attackerStyle == AttackStyle.Stab && defenderStyle == AttackStyle.Spellweaving))
return strongAgainstMultiplier;
if ((attackerStyle == AttackStyle.Range && defenderStyle == AttackStyle.Spellweaving) ||
(attackerStyle == AttackStyle.Stab && defenderStyle == AttackStyle.Range) ||
(attackerStyle == AttackStyle.Spellweaving && defenderStyle == AttackStyle.Stab))
return weakAgainstMultiplier;
public int CalculateMaxHit(int skillLevel, int equipmentBonus, AttackStyle style)
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush or AttackStyle.Range =>
Mathf.FloorToInt(0.5f + (skillLevel * (equipmentBonus + 64)) / 640f),
AttackStyle.Spellweaving =>
Mathf.FloorToInt(10 * (1 + equipmentBonus / 100f)),
[CreateAssetMenu(fileName = "New Combat Equipment", menuName = "Combat/Equipment")]
public class CombatEquipment : Item
[Header("Combat Requirements")]
public int requiredLevel = 1;
public int requiredDivinity = 1;
public CombatBonuses bonuses = new CombatBonuses();
[Header("Equipment Properties")]
public GameObject equipmentModel;
public AttackStyle defaultAttackStyle = AttackStyle.None;
public CombatTrainingStyle defaultTrainingStyle = CombatTrainingStyle.Precision;
public bool splitXPWithFortitude = false;
private void OnValidate()
itemType = ItemType.Equipment;
private void ValidateRequirements()
if (requiredLevel < 1) requiredLevel = 1;
if (requiredDivinity < 1) requiredDivinity = 1;
public bool CheckRequirements(PlayerSkills skills)
if (skills == null) return false;
string requiredSkill = defaultAttackStyle switch
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush => "Precision",
AttackStyle.Range => "Range",
AttackStyle.Spellweaving => "Spellweaving",
if (!string.IsNullOrEmpty(requiredSkill))
var skill = skills.GetSkill(requiredSkill);
if (skill == null || skill.CurrentLevel < requiredLevel)
if (requiredDivinity > 1)
var divinity = skills.GetSkill("Divinity");
if (divinity == null || divinity.CurrentLevel < requiredDivinity)
public static class CombatExperience
public static void AwardCombatExperience(ICombatant attacker, int damage, CombatWeaponData weapon)
PlayerSkills skills = (attacker as MonoBehaviour)?.GetComponent<PlayerSkills>();
if (skills == null) return;
float baseXP = damage * weapon.experienceMultiplier;
switch (weapon.attackStyle)
case AttackStyle.Stab or AttackStyle.Slash:
skills.AddExperience("Precision", weapon.splitXPWithFortitude ? baseXP * 0.5f : baseXP);
skills.AddExperience("Might", weapon.splitXPWithFortitude ? baseXP * 0.5f : baseXP);
skills.AddExperience("Range", weapon.splitXPWithFortitude ? baseXP * 0.5f : baseXP);
case AttackStyle.Spellweaving:
skills.AddExperience("Spellweaving", weapon.splitXPWithFortitude ? baseXP * 0.5f : baseXP);
if (weapon.splitXPWithFortitude)
skills.AddExperience("Fortitude", baseXP * 0.5f);
float vitalityXP = CalculateVitalityXP(damage, weapon.attackStyle);
skills.AddExperience("Vitality", vitalityXP);
private static float CalculateVitalityXP(float damage, AttackStyle style)
float baseXP = damage * 50.54f;
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush => baseXP * 0.25f,
AttackStyle.Range => baseXP * 0.333f,
AttackStyle.Spellweaving => baseXP,
using System.Collections;
public class CombatHandler : MonoBehaviour
[Header("Combat Settings")]
public float attackRange = 2f;
public float rotationSpeed = 10f;
private ICombatant attacker;
private ICombatant target;
private float nextAttackTime;
private ClickToMove movementController;
private Animator animator;
private RegenerationManager regenManager;
[Header("Combat Animations")]
[SerializeField] private string attackAnimationTrigger = "PlayerAttack";
[SerializeField] private string inCombatBoolParam = "IsInCombat";
[SerializeField] private string weaponTypeParam = "WeaponType";
[SerializeField] private string attackStyleParam = "AttackStyle";
[SerializeField] private float attackAnimationDuration = 0.8f;
[Header("Combat Effects")]
public ParticleSystem hitEffect;
public AudioClip[] hitSounds;
public AudioClip[] missSounds;
private AudioSource audioSource;
public bool InCombat => inCombat;
public ICombatant CurrentTarget => target;
private void InitializeComponents()
attacker = GetComponent<ICombatant>();
movementController = GetComponent<ClickToMove>();
animator = GetComponent<Animator>();
audioSource = gameObject.AddComponent<AudioSource>();
regenManager = GetComponent<RegenerationManager>();
Debug.LogError("No ICombatant implementation found on GameObject!");
private void ValidateAnimator()
bool hasAttackTrigger = false;
bool hasInCombatBool = false;
bool hasWeaponType = false;
bool hasAttackStyle = false;
foreach (AnimatorControllerParameter param in animator.parameters)
case var name when name == attackAnimationTrigger:
case var name when name == inCombatBoolParam:
case var name when name == weaponTypeParam:
case var name when name == attackStyleParam:
if (!hasAttackTrigger) Debug.LogError($"Missing animation parameter: {attackAnimationTrigger}");
if (!hasInCombatBool) Debug.LogError($"Missing animation parameter: {inCombatBoolParam}");
if (!hasWeaponType) Debug.LogError($"Missing animation parameter: {weaponTypeParam}");
if (!hasAttackStyle) Debug.LogError($"Missing animation parameter: {attackStyleParam}");
Debug.LogError("No Animator component found on GameObject!");
if (!inCombat || target == null) return;
float distanceToTarget = Vector3.Distance(transform.position, target.transform.position);
if (distanceToTarget > attackRange)
else if (movementController != null)
movementController.StopMovementAndActions();
if (Time.time >= nextAttackTime)
StartCoroutine(PerformAttackSequence());
private void UpdateAnimatorWeaponType(WeaponData weapon)
if (animator != null && weapon != null)
animator.SetInteger(weaponTypeParam, (int)weapon.weaponType);
animator.SetInteger(attackStyleParam, (int)weapon.defaultAttackStyle);
private void RequestMoveToTarget()
if (target == null || movementController == null) return;
movementController.MoveToTarget(target.transform.position);
private void FaceTarget()
if (target == null) return;
Vector3 direction = (target.transform.position - transform.position).normalized;
Quaternion lookRotation = Quaternion.LookRotation(new Vector3(direction.x, 0, direction.z));
transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * rotationSpeed);
private IEnumerator PerformAttackSequence()
if (target == null || !target.IsAlive)
animator.SetTrigger(attackAnimationTrigger);
yield return new WaitForSeconds(attackAnimationDuration);
if (target != null && target.IsAlive)
nextAttackTime = Time.time + attacker.GetAttackSpeed();
public void StartCombat(ICombatant newTarget)
if (newTarget == null || !newTarget.IsAlive)
Debug.LogWarning("Attempted to start combat with invalid target");
nextAttackTime = Time.time;
if (regenManager != null)
regenManager.PauseRegeneration(1.0f);
animator.SetBool(inCombatBoolParam, true);
if (attacker is PlayerCombatant playerCombatant)
var equipment = FindFirstObjectByType<EquipmentManager>();
var weapon = equipment?.GetEquippedItem(EquipmentSlot.MainHand) as WeaponData;
animator.SetInteger(weaponTypeParam, (int)weapon.weaponType);
animator.SetInteger(attackStyleParam, (int)weapon.defaultAttackStyle);
EnhancedChatbox.Instance?.AddMessage($"Engaging in combat with {newTarget.GetDisplayName()}");
if (regenManager != null)
regenManager.StartRegeneration();
animator.ResetTrigger(attackAnimationTrigger);
animator.SetBool(inCombatBoolParam, false);
animator.SetInteger(weaponTypeParam, 0);
animator.SetInteger(attackStyleParam, 0);
public void PlayHitEffect(Vector3 position)
ParticleSystem effect = Instantiate(hitEffect, position, Quaternion.identity);
Destroy(effect.gameObject, effect.main.duration);
public void PlayCombatSound(bool isHit)
if (audioSource == null) return;
AudioClip[] clips = isHit ? hitSounds : missSounds;
if (clips != null && clips.Length > 0)
AudioClip clip = clips[Random.Range(0, clips.Length)];
audioSource.PlayOneShot(clip);
private void OnValidate()
if (string.IsNullOrEmpty(attackAnimationTrigger))
attackAnimationTrigger = "PlayerAttack";
if (string.IsNullOrEmpty(inCombatBoolParam))
inCombatBoolParam = "IsInCombat";
if (string.IsNullOrEmpty(weaponTypeParam))
weaponTypeParam = "WeaponType";
if (string.IsNullOrEmpty(attackStyleParam))
attackStyleParam = "AttackStyle";
public class CombatPanel : MonoBehaviour
[Header("Main References")]
[SerializeField] private EquipmentManager equipmentManager;
[SerializeField] private PlayerSkills playerSkills;
[Header("Panel Elements")]
[SerializeField] private TextMeshProUGUI combatLevelText;
[SerializeField] private TextMeshProUGUI currentWeaponText;
[SerializeField] private TextMeshProUGUI attackStyleText;
[Header("Combat Style UI")]
[SerializeField] private GameObject styleToggleContainer;
[SerializeField] private Toggle precisionToggle;
[SerializeField] private Toggle mightToggle;
[SerializeField] private Toggle fortitudeToggle;
[Header("Combat Stats Display")]
[SerializeField] private TextMeshProUGUI attackBonusText;
[SerializeField] private TextMeshProUGUI strengthBonusText;
[SerializeField] private TextMeshProUGUI defenseBonusText;
private WeaponData currentWeapon;
private CombatTrainingStyle currentTrainingStyle;
var equipmentUI = GetComponentInParent<EquipmentUI>();
equipmentUI.SetCombatPanel(this);
private void InitializeReferences()
if (equipmentManager == null)
equipmentManager = EquipmentManager.Instance;
if (playerSkills == null)
playerSkills = FindFirstObjectByType<PlayerSkills>();
private void SetupEventListeners()
if (equipmentManager != null)
equipmentManager.OnEquipmentChanged += HandleEquipmentChanged;
private void SetupToggleGroup()
ToggleGroup group = styleToggleContainer.GetComponent<ToggleGroup>();
group = styleToggleContainer.AddComponent<ToggleGroup>();
precisionToggle.group = group;
mightToggle.group = group;
fortitudeToggle.group = group;
private void SetupStyleToggles()
precisionToggle.onValueChanged.AddListener((isOn) => {
if (isOn) UpdateTrainingStyle(CombatTrainingStyle.Precision);
mightToggle.onValueChanged.AddListener((isOn) => {
if (isOn) UpdateTrainingStyle(CombatTrainingStyle.Might);
fortitudeToggle.onValueChanged.AddListener((isOn) => {
if (isOn) UpdateTrainingStyle(CombatTrainingStyle.Fortitude);
private void HandleEquipmentChanged(EquipmentSlot slot, Item item)
if (slot == EquipmentSlot.MainHand)
currentWeapon = item as WeaponData;
private void UpdateTrainingStyle(CombatTrainingStyle newStyle)
currentTrainingStyle = newStyle;
if (currentWeapon != null)
private void UpdateAvailableStyles()
if (currentWeapon == null)
precisionToggle.gameObject.SetActive(false);
mightToggle.gameObject.SetActive(false);
fortitudeToggle.gameObject.SetActive(false);
bool canTrainPrecision = currentWeapon.defaultAttackStyle == AttackStyle.Stab ||
currentWeapon.defaultAttackStyle == AttackStyle.Slash;
bool canTrainMight = currentWeapon.defaultAttackStyle == AttackStyle.Crush;
bool canTrainFortitude = true;
precisionToggle.gameObject.SetActive(canTrainPrecision);
mightToggle.gameObject.SetActive(canTrainMight);
fortitudeToggle.gameObject.SetActive(canTrainFortitude);
if (canTrainPrecision) precisionToggle.isOn = true;
else if (canTrainMight) mightToggle.isOn = true;
else fortitudeToggle.isOn = true;
if (playerSkills != null)
int combatLevel = playerSkills.CalculateCombatLevel();
combatLevelText.text = $"Combat Level: {combatLevel}";
if (currentWeapon != null)
currentWeaponText.text = currentWeapon.itemName;
attackStyleText.text = $"Style: {currentWeapon.defaultAttackStyle}";
attackBonusText.text = $"Attack: +{currentWeapon.bonuses.GetAttackBonus(currentWeapon.defaultAttackStyle)}";
strengthBonusText.text = $"Might: +{currentWeapon.bonuses.mightBonus}";
defenseBonusText.text = $"Fortitude: +{currentWeapon.bonuses.GetDefenseBonus(currentWeapon.defaultAttackStyle)}";
currentWeaponText.text = "Unarmed";
attackStyleText.text = "Style: Punch";
attackBonusText.text = "Attack: +0";
strengthBonusText.text = "Might: +0";
defenseBonusText.text = "Fortitude: +0";
if (equipmentManager != null)
equipmentManager.OnEquipmentChanged -= HandleEquipmentChanged;
using System.Collections.Generic;
public int Vitality { get; private set; }
public int MaxVitality { get; private set; }
public int Precision { get; private set; }
public int Might { get; private set; }
public int Fortitude { get; private set; }
public int Range { get; private set; }
public int Spellweaving { get; private set; }
public int Divinity { get; private set; }
public CombatBonuses combatBonuses = new CombatBonuses();
public bool IsAlive => Vitality > 0;
public CombatStats(int vitality = 100, int precision = 1, int might = 1,
int fortitude = 1, int range = 1, int spellweaving = 1, int divinity = 1)
Spellweaving = spellweaving;
public int CombatLevel =>
Mathf.Min(240, (Fortitude + Vitality + (Divinity / 2) +
Mathf.Max(Mathf.Max(Precision + Might, Range), Spellweaving)) / 5);
public int GetDefenseBonus(AttackStyle style) => combatBonuses.GetDefenseBonus(style);
public int CalculateMaxHit(AttackStyle style)
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush =>
Mathf.FloorToInt(0.5f + (Might * (combatBonuses.mightBonus + 64)) / 640f),
Mathf.FloorToInt(0.5f + (Range * (combatBonuses.rangeStrengthBonus + 64)) / 640f),
AttackStyle.Spellweaving =>
Mathf.FloorToInt(10 * (1 + combatBonuses.spellweavingDamage / 100f)),
public float CalculateHitChance(AttackStyle style, CombatStats targetStats)
float effectiveAccuracy = GetAttackSkillLevel(style) + combatBonuses.GetAttackBonus(style);
float effectiveFortitude = targetStats.Fortitude + targetStats.GetDefenseBonus(style);
return effectiveAccuracy / (effectiveAccuracy + effectiveFortitude);
public int GetAttackSkillLevel(AttackStyle style)
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush => Precision,
AttackStyle.Range => Range,
AttackStyle.Spellweaving => Spellweaving,
public float CalculateVitalityXP(float damage, AttackStyle style)
float baseXP = damage * 50.54f;
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush => baseXP * 0.25f,
AttackStyle.Range => baseXP * 0.333f,
AttackStyle.Spellweaving => baseXP,
public float GetBaseRegen() => MaxVitality * 0.01f;
public void ModifyVitality(int amount)
Vitality = Mathf.Clamp(Vitality + amount, 0, MaxVitality);
using System.Collections.Generic;
public class CombatUI : MonoBehaviour
[Header("UI References")]
public RectTransform combatPanel;
public TextMeshProUGUI combatLevelText;
public TextMeshProUGUI weaponNameText;
public TextMeshProUGUI attackStyleText;
public TextMeshProUGUI trainingStyleText;
[Header("Training Style Controls")]
public Button cycleTrainingButton;
public Image trainingStyleIcon;
public TextMeshProUGUI attackBonusText;
public TextMeshProUGUI strengthBonusText;
public TextMeshProUGUI defenseBonusText;
private PlayerSkills playerSkills;
private EquipmentManager equipmentManager;
private WeaponData currentWeapon;
private int currentTrainingStyleIndex = 0;
private void InitializeReferences()
playerSkills = FindFirstObjectByType<PlayerSkills>();
equipmentManager = EquipmentManager.Instance;
if (cycleTrainingButton != null)
cycleTrainingButton.onClick.AddListener(CycleTrainingStyle);
private void SetupEventListeners()
if (equipmentManager != null)
equipmentManager.OnEquipmentChanged += HandleEquipmentChanged;
private void HandleEquipmentChanged(EquipmentSlot slot, Item item)
if (slot == EquipmentSlot.MainHand)
currentWeapon = item as WeaponData;
currentTrainingStyleIndex = 0;
if (playerSkills == null || equipmentManager == null) return;
int combatLevel = playerSkills.CalculateCombatLevel();
combatLevelText.text = $"Combat Level: {combatLevel}";
if (currentWeapon != null)
weaponNameText.text = currentWeapon.itemName;
attackStyleText.text = $"Attack Style: {currentWeapon.defaultAttackStyle}";
var currentStyle = currentWeapon.availableTrainingStyles[currentTrainingStyleIndex];
trainingStyleText.text = $"Training: {currentStyle}";
weaponNameText.text = "Unarmed";
attackStyleText.text = "Attack Style: Punch";
trainingStyleText.text = "Training: Precision";
private void UpdateCombatStats()
if (currentWeapon == null) return;
var bonuses = currentWeapon.bonuses;
attackBonusText.text = $"Attack Bonus: +{bonuses.GetAttackBonus(currentWeapon.defaultAttackStyle)}";
switch (currentWeapon.defaultAttackStyle)
case AttackStyle.Spellweaving:
strengthBonusText.text = $"Magic Damage: +{bonuses.spellweavingDamage}%";
strengthBonusText.text = $"Range Strength: +{bonuses.rangeStrengthBonus}";
strengthBonusText.text = $"Might Bonus: +{bonuses.mightBonus}";
defenseBonusText.text = $"Defense Bonus: +{bonuses.GetDefenseBonus(currentWeapon.defaultAttackStyle)}";
public void CycleTrainingStyle()
if (currentWeapon == null || currentWeapon.availableTrainingStyles.Length <= 1) return;
currentTrainingStyleIndex = (currentTrainingStyleIndex + 1) % currentWeapon.availableTrainingStyles.Length;
if (equipmentManager != null)
equipmentManager.OnEquipmentChanged -= HandleEquipmentChanged;
using System.Collections.Generic;
[CreateAssetMenu(fileName = "New Weapon", menuName = "Combat/Weapon")]
public class CombatWeapon : ScriptableObject
public string weaponName;
public WeaponType weaponType;
public AttackStyle defaultAttackStyle;
public int strengthBonus;
public float attackSpeed = 2.4f;
public int requiredLevel;
public EquipmentSlot equipSlot = EquipmentSlot.MainHand;
public float GetDPS(CombatStats stats)
float accuracy = CalculateAccuracy(stats);
float avgDamage = CalculateAverageDamage(stats);
return (accuracy * avgDamage) / attackSpeed;
private float CalculateAccuracy(CombatStats stats)
float effectiveLevel = stats.GetAttackSkillLevel(defaultAttackStyle);
return (effectiveLevel * (1 + attackBonus/100f));
private float CalculateAverageDamage(CombatStats stats)
switch (defaultAttackStyle)
baseDamage = stats.Might * (1 + strengthBonus/100f);
baseDamage = stats.Range * (1 + strengthBonus/100f);
case AttackStyle.Spellweaving:
baseDamage = stats.Spellweaving * (1 + strengthBonus/100f);
[CreateAssetMenu(fileName = "New Weapon", menuName = "Combat/Weapon Data")]
public class CombatWeaponData : CombatEquipment
[Header("Weapon Specific")]
public WeaponType weaponType;
public AttackStyle attackStyle;
public CombatTrainingStyle trainingStyle;
public float attackSpeed = 2.4f;
public float baseAccuracy = 1f;
public WeaponAttackStyle[] availableAttackStyles;
public float experienceMultiplier = 4f;
using System.Collections;
using System.Collections.Generic;
public class ContextMenu : MonoBehaviour
public static ContextMenu Instance { get; private set; }
[Header("UI References")]
public GameObject menuPanel;
public GameObject optionPrefab;
public RectTransform menuContainer;
[Header("Menu Settings")]
public float minWidth = 100f;
public float maxWidth = 300f;
public float optionHeight = 25f;
public float horizontalPadding = 20f;
public float verticalPadding = 10f;
public float verticalSpacing = 2f;
[Header("Visual Settings")]
public Color defaultTextColor = Color.white;
public Color hoverTextColor = Color.grey;
public int canvasSortingOrder = 31074;
private List<GameObject> currentOptions = new List<GameObject>();
private RectTransform panelRect;
private Canvas menuCanvas;
private bool IsVisible => menuPanel != null && menuPanel.activeSelf;
public Vector2 CurrentPosition => panelRect != null ? panelRect.position : Vector2.zero;
public int CurrentSortingOrder => menuCanvas != null ? menuCanvas.sortingOrder : 0;
DontDestroyOnLoad(gameObject);
if (menuPanel.activeSelf && Input.GetMouseButtonDown(0))
if (!IsClickingMenuItem())
private void InitializeCanvas()
menuCanvas = GetComponent<Canvas>();
menuCanvas = gameObject.AddComponent<Canvas>();
var raycaster = GetComponent<GraphicRaycaster>();
raycaster = gameObject.AddComponent<GraphicRaycaster>();
raycaster.enabled = true;
menuCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
menuCanvas.sortingOrder = 31074;
menuCanvas.overrideSorting = true;
panelRect = menuPanel.GetComponent<RectTransform>();
var blocker = menuPanel.GetComponent<CanvasGroup>();
blocker = menuPanel.AddComponent<CanvasGroup>();
blocker.blocksRaycasts = true;
blocker.interactable = true;
menuPanel.SetActive(false);
Debug.Log($"Context Menu initialized - Canvas Sort Order: {menuCanvas.sortingOrder}");
public void ShowMenu(Vector2 screenPosition, List<ContextMenuOption> options)
Debug.LogError("Menu Panel is not assigned!");
Debug.Log($"ShowMenu called with {options.Count} options at {screenPosition}");
menuCanvas.overrideSorting = true;
menuCanvas.sortingOrder = canvasSortingOrder;
Debug.Log($"Set canvas sorting order to {menuCanvas.sortingOrder}");
foreach (var option in options)
GameObject optionObj = CreateOptionObject(option);
TMP_Text text = optionObj.GetComponentInChildren<TMP_Text>();
float textWidth = text.preferredWidth;
maxTextWidth = Mathf.Max(maxTextWidth, textWidth);
float totalWidth = Mathf.Clamp(maxTextWidth + (horizontalPadding * 2), minWidth, maxWidth);
float menuHeight = optionHeight * options.Count + (verticalSpacing * (options.Count - 1));
if (menuContainer != null)
menuContainer.sizeDelta = new Vector2(totalWidth, menuHeight);
for (int i = 0; i < currentOptions.Count; i++)
RectTransform rect = currentOptions[i].GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(totalWidth - (horizontalPadding * 2), optionHeight);
rect.anchoredPosition = new Vector2(0, -i * (optionHeight + verticalSpacing));
TMP_Text text = currentOptions[i].GetComponentInChildren<TMP_Text>();
text.margin = new Vector4(horizontalPadding, 0, horizontalPadding, 0);
text.alignment = TextAlignmentOptions.Left;
Vector2 adjustedPosition = screenPosition;
Vector2 screenSize = new Vector2(Screen.width, Screen.height);
if (adjustedPosition.x + totalWidth > screenSize.x)
adjustedPosition.x = screenSize.x - totalWidth;
if (adjustedPosition.x < 0)
if (adjustedPosition.y + menuHeight > screenSize.y)
adjustedPosition.y = screenSize.y - menuHeight;
if (adjustedPosition.y < 0)
panelRect.position = adjustedPosition;
panelRect.sizeDelta = new Vector2(totalWidth, menuHeight);
menuPanel.transform.SetAsLastSibling();
menuPanel.SetActive(true);
StartCoroutine(ValidateMenuPosition());
Debug.Log($"Menu shown at {adjustedPosition}, Size: {totalWidth}x{menuHeight}");
private IEnumerator ValidateMenuPosition()
Vector3[] corners = new Vector3[4];
panelRect.GetWorldCorners(corners);
bool isPartiallyOffscreen = false;
foreach (Vector3 corner in corners)
Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(null, corner);
if (screenPoint.x < 0 || screenPoint.x > Screen.width ||
screenPoint.y < 0 || screenPoint.y > Screen.height)
isPartiallyOffscreen = true;
Debug.LogWarning($"Menu is partially offscreen at position {panelRect.position}");
if (!isPartiallyOffscreen)
Debug.Log("Menu position validated - fully on screen");
private void ValidateTransform()
RectTransform rect = GetComponent<RectTransform>();
if (rect.localPosition.z != 0)
Debug.LogWarning("ContextMenu Z position is not 0, this might cause layering issues");
rect.localPosition = new Vector3(rect.localPosition.x, rect.localPosition.y, 0);
private void ValidateEventSystem()
var raycaster = GetComponentInParent<GraphicRaycaster>();
if (raycaster == null || !raycaster.enabled)
Debug.LogError("GraphicRaycaster missing or disabled in ContextMenu hierarchy");
public void ForceToFront()
Canvas canvas = GetComponentInParent<Canvas>();
int highestOrder = FindObjectsByType<Canvas>(FindObjectsSortMode.None)
.Max(c => c.sortingOrder);
canvas.sortingOrder = highestOrder + 1;
Debug.Log($"Force context menu to front: {canvas.sortingOrder}");
private GameObject CreateOptionObject(ContextMenuOption option)
GameObject optionObj = Instantiate(optionPrefab, menuContainer);
currentOptions.Add(optionObj);
RectTransform rect = optionObj.GetComponent<RectTransform>();
rect.localScale = Vector3.one;
Button button = optionObj.GetComponent<Button>();
TMP_Text text = optionObj.GetComponentInChildren<TMP_Text>();
text.color = defaultTextColor;
text.textWrappingMode = TextWrappingModes.NoWrap;
text.overflowMode = TextOverflowModes.Ellipsis;
text.margin = new Vector4(horizontalPadding, 0, horizontalPadding, 0);
text.alignment = TextAlignmentOptions.Left;
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() => {
var colors = button.colors;
colors.normalColor = Color.clear;
colors.highlightedColor = new Color(1, 1, 1, 0.1f);
private void ClearOptions()
foreach (var option in currentOptions)
private Vector2 ClampMenuPosition(Vector2 position, int optionCount)
Vector2 screenSize = new Vector2(Screen.width, Screen.height);
float totalWidth = menuContainer.sizeDelta.x;
float menuHeight = optionHeight * optionCount;
position.x = Mathf.Clamp(position.x, 0, screenSize.x - totalWidth);
if (position.y - menuHeight < 0)
position.y += menuHeight;
if (position.y > screenSize.y)
position.y = screenSize.y;
if (Time.time - showTime < 0.1f) return;
menuPanel.SetActive(false);
public bool IsClickingMenuItem()
if (!menuPanel.activeSelf) return false;
return RectTransformUtility.RectangleContainsScreenPoint(
if (!menuPanel || !menuPanel.activeSelf) return;
RectTransform rect = menuPanel.GetComponent<RectTransform>();
Vector3[] corners = new Vector3[4];
rect.GetWorldCorners(corners);
Gizmos.color = Color.yellow;
for (int i = 0; i < 4; i++)
Gizmos.DrawLine(corners[i], corners[(i + 1) % 4]);
UnityEditor.Handles.Label(corners[0],
$"Sort Order: {menuCanvas.sortingOrder}");
public class ContextMenuOption
public string Text { get; set; }
public System.Action Action { get; set; }
public ContextMenuOption(string text, System.Action action)
using UnityEngine.EventSystems;
public class ContextMenuValidator : MonoBehaviour
public void ValidateSetup()
var contextMenu = GetComponent<ContextMenu>();
Debug.LogError("Missing ContextMenu component!");
var canvas = GetComponent<Canvas>();
Debug.LogError("Missing Canvas component!");
if (canvas.renderMode != RenderMode.ScreenSpaceOverlay)
Debug.LogWarning($"Canvas render mode is {canvas.renderMode}, should be ScreenSpaceOverlay");
if (canvas.sortingOrder < 100)
Debug.LogWarning($"Canvas sorting order is {canvas.sortingOrder}, should be at least 100");
var raycaster = GetComponent<GraphicRaycaster>();
Debug.LogError("Missing GraphicRaycaster component!");
if (contextMenu.menuPanel == null)
Debug.LogError("MenuPanel reference is missing!");
var panelCanvas = contextMenu.menuPanel.GetComponent<CanvasGroup>();
Debug.LogWarning("MenuPanel should have a CanvasGroup component for proper interaction");
if (contextMenu.optionPrefab == null)
Debug.LogError("Option prefab reference is missing!");
ValidateOptionPrefab(contextMenu.optionPrefab);
var eventSystem = FindFirstObjectByType<EventSystem>();
Debug.LogError("No EventSystem found in scene!");
private void ValidateOptionPrefab(GameObject prefab)
var button = prefab.GetComponent<Button>();
Debug.LogError("Option prefab missing Button component!");
var text = prefab.GetComponentInChildren<TMP_Text>();
Debug.LogError("Option prefab missing TMP_Text component!");
public class CursorManager : MonoBehaviour
public static CursorManager Instance { get; private set; }
[Header("Cursor Textures")]
public Texture2D defaultCursor;
public Texture2D interactiveCursor;
public Texture2D combatCursor;
public Texture2D harvestCursor;
DontDestroyOnLoad(gameObject);
public void SetDefaultCursor()
Cursor.SetCursor(defaultCursor, Vector2.zero, CursorMode.Auto);
using UnityEngine.EventSystems;
public class DragDropHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
private RectTransform draggedIcon;
private CanvasGroup canvasGroup;
private Vector2 originalPosition;
private Transform originalParent;
private int originalSlotIndex;
private InventoryUI inventoryUI;
private InventorySlotUI originalSlot;
private GameObject draggedItemGO;
canvas = GetComponentInParent<Canvas>();
canvas = FindFirstObjectByType<Canvas>();
inventoryUI = FindFirstObjectByType<InventoryUI>();
originalSlot = GetComponentInParent<InventorySlotUI>();
public void OnBeginDrag(PointerEventData eventData)
if (originalSlot == null || originalSlot.itemIcon == null || !originalSlot.itemIcon.enabled)
draggedItemGO = new GameObject("DraggedItem");
draggedIcon = draggedItemGO.AddComponent<RectTransform>();
var image = draggedItemGO.AddComponent<Image>();
image.sprite = originalSlot.itemIcon.sprite;
image.raycastTarget = false;
draggedIcon.sizeDelta = originalSlot.itemIcon.rectTransform.sizeDelta;
draggedItemGO.transform.SetParent(canvas.transform);
draggedItemGO.transform.SetAsLastSibling();
originalSlotIndex = originalSlot.slotIndex;
originalSlot.itemIcon.color = new Color(1, 1, 1, 0.5f);
Debug.Log($"Started dragging item from slot {originalSlotIndex}");
public void OnDrag(PointerEventData eventData)
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.GetComponent<RectTransform>(),
eventData.pressEventCamera,
draggedIcon.localPosition = localPoint;
public void OnEndDrag(PointerEventData eventData)
if (draggedItemGO != null)
if (originalSlot != null && originalSlot.itemIcon != null)
originalSlot.itemIcon.color = Color.white;
InventorySlotUI targetSlot = null;
if (eventData.pointerEnter != null)
targetSlot = eventData.pointerEnter.GetComponent<InventorySlotUI>();
targetSlot = eventData.pointerEnter.GetComponentInParent<InventorySlotUI>();
if (targetSlot != null && originalSlotIndex >= 0 && targetSlot.slotIndex != originalSlotIndex)
Debug.Log($"Swapping items between slots {originalSlotIndex} and {targetSlot.slotIndex}");
inventoryUI.SwapItems(originalSlotIndex, targetSlot.slotIndex);
Debug.Log($"No valid target slot found or same slot");
public class DroppedItem : MonoBehaviour
public Item item { get; private set; }
public int quantity { get; private set; }
private float despawnTime;
private MeshRenderer meshRenderer;
public void Initialize(Item _item, int _quantity, float lifetime)
despawnTime = Time.time + lifetime;
private void SetupVisuals()
GameObject visual = GameObject.CreatePrimitive(PrimitiveType.Cube);
visual.transform.SetParent(transform);
visual.transform.localPosition = Vector3.zero;
visual.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);
meshRenderer = visual.GetComponent<MeshRenderer>();
if (meshRenderer && item.itemIcon)
Material mat = new Material(Shader.Find("Standard"));
mat.mainTexture = item.itemIcon.texture;
meshRenderer.material = mat;
private void SpawnDroppedItem(Vector3 position)
GameObject player = GameObject.FindGameObjectWithTag("Player");
Vector3 dropPos = player.transform.position +
(player.transform.forward * 1f) +
GameObject droppedObj = new GameObject($"Dropped_{item.itemName}");
droppedObj.transform.position = dropPos;
var itemComponent = droppedObj.AddComponent<DroppedItem>();
itemComponent.Initialize(item, 1, 180f);
Debug.Log($"Spawned dropped item: {item.itemName} at {dropPos}");
private void SetupCollider()
BoxCollider col = gameObject.AddComponent<BoxCollider>();
col.size = new Vector3(1f, 1f, 1f);
Rigidbody rb = gameObject.AddComponent<Rigidbody>();
private void OnTriggerEnter(Collider other)
if (other.CompareTag("Player"))
if (Inventory.Instance.AddItem(item, quantity))
EnhancedChatbox.Instance?.AddMessage($"Picked up: {item.itemName}");
EnhancedChatbox.Instance?.AddMessage("Inventory is full!");
if (Time.time >= despawnTime)
using System.Collections.Generic;
public class EnhancedChatbox : MonoBehaviour
public static EnhancedChatbox Instance { get; private set; }
public class MessageStyle
public string prefix = "";
public Color textColor = Color.white;
public float displayDuration = 5f;
public bool isBold = false;
public bool isPermanent = false;
[Header("UI References")]
public TMP_Text chatText;
public int maxMessages = 20;
[Header("Message Styles")]
public MessageStyle defaultStyle;
public MessageStyle levelUpStyle;
public MessageStyle systemStyle;
private Queue<ChatMessage> messageQueue = new Queue<ChatMessage>();
private class ChatMessage
public ChatMessage(string text, MessageType type, bool isPermanent = false)
this.timestamp = Time.time;
this.isPermanent = isPermanent;
AddMessage("Welcome to RVAMP!", MessageType.System, true);
public void AddMessage(string message, MessageType type = MessageType.Default, bool isPermanent = false)
if (messageQueue.Count >= maxMessages)
RemoveOldestNonPermanentMessage();
messageQueue.Enqueue(new ChatMessage(message, type, isPermanent));
private void RemoveOldestNonPermanentMessage()
var tempQueue = new Queue<ChatMessage>();
while (messageQueue.Count > 0)
var message = messageQueue.Dequeue();
if (!message.isPermanent && !removed)
tempQueue.Enqueue(message);
messageQueue = tempQueue;
private void UpdateChatDisplay()
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach (var message in messageQueue)
string formattedMessage = FormatMessage(message);
sb.AppendLine(formattedMessage);
chatText.text = sb.ToString();
private string FormatMessage(ChatMessage message)
MessageStyle style = GetStyleForType(message.type);
string formattedText = message.text;
formattedText = $"<b>{formattedText}</b>";
if (!string.IsNullOrEmpty(style.prefix))
formattedText = $"{style.prefix}{formattedText}";
return $"<color=#{ColorUtility.ToHtmlStringRGB(style.textColor)}>{formattedText}</color>";
private MessageStyle GetStyleForType(MessageType type)
MessageType.LevelUp => levelUpStyle,
MessageType.System => systemStyle,
using System.Collections.Generic;
using System.Collections;
public class EquipmentManager : MonoBehaviour
public static EquipmentManager Instance { get; private set; }
[Header("Equipment Setup")]
public Transform equipmentContainer;
public Transform rightHandMount;
public Transform leftHandMount;
public Transform backMount;
public Transform headMount;
public Transform chestMount;
public Transform legsMount;
public Transform glovesMount;
public Transform bootsMount;
public Transform quiverMount;
[Header("Equipment Offsets")]
[SerializeField] private Vector3 mainHandPositionOffset = Vector3.zero;
[SerializeField] private Vector3 mainHandRotationOffset = new Vector3(0, 90, 0);
[SerializeField] private Vector3 mainHandScale = Vector3.one;
[SerializeField] private Vector3 offHandPositionOffset = Vector3.zero;
[SerializeField] private Vector3 offHandRotationOffset = new Vector3(0, 90, 0);
[SerializeField] private Vector3 offHandScale = Vector3.one;
[SerializeField] private Vector3 backPositionOffset = Vector3.zero;
[SerializeField] private Vector3 backRotationOffset = Vector3.zero;
[SerializeField] private Vector3 backScale = Vector3.one;
[SerializeField] private Vector3 armorPositionOffset = Vector3.zero;
[SerializeField] private Vector3 armorRotationOffset = Vector3.zero;
[SerializeField] private Vector3 armorScale = Vector3.one;
[SerializeField] private Animator playerAnimator;
private Dictionary<EquipmentSlot, Item> equippedItems = new Dictionary<EquipmentSlot, Item>();
private Dictionary<EquipmentSlot, GameObject> equippedModels = new Dictionary<EquipmentSlot, GameObject>();
private EquipmentUI equipmentUI;
private Animator characterAnimator;
public System.Action<EquipmentSlot, Item> OnEquipmentChanged;
DontDestroyOnLoad(gameObject);
equipmentUI = FindFirstObjectByType<EquipmentUI>();
playerAnimator = GetComponent<Animator>();
Debug.LogWarning("EquipmentUI not found in scene");
private void ValidateMountPoints()
if (rightHandMount == null) Debug.LogError("Right hand mount point is missing!");
if (leftHandMount == null) Debug.LogError("Left hand mount point is missing!");
if (backMount == null) Debug.LogError("Back mount point is missing!");
private void ValidateEquipmentUI()
var equipmentUI = FindFirstObjectByType<EquipmentUI>();
equipmentUI.ValidateAllSlots();
private void InitializeSlots()
foreach (EquipmentSlot slot in System.Enum.GetValues(typeof(EquipmentSlot)))
if (slot != EquipmentSlot.None)
equippedItems[slot] = null;
equippedModels[slot] = null;
private Transform GetMountPointForSlot(EquipmentSlot slot)
EquipmentSlot.MainHand => rightHandMount,
EquipmentSlot.OffHand => leftHandMount,
EquipmentSlot.Back => backMount,
EquipmentSlot.Head => headMount,
EquipmentSlot.Chest => chestMount,
EquipmentSlot.Legs => legsMount,
EquipmentSlot.Gloves => glovesMount,
EquipmentSlot.Boots => bootsMount,
EquipmentSlot.Quiver => quiverMount,
private Vector3 GetPositionOffsetForSlot(EquipmentSlot slot)
EquipmentSlot.MainHand => mainHandPositionOffset,
EquipmentSlot.OffHand => offHandPositionOffset,
EquipmentSlot.Back => backPositionOffset,
EquipmentSlot.Head or EquipmentSlot.Chest or EquipmentSlot.Legs
or EquipmentSlot.Gloves or EquipmentSlot.Boots => armorPositionOffset,
private Vector3 GetRotationOffsetForSlot(EquipmentSlot slot)
EquipmentSlot.MainHand => mainHandRotationOffset,
EquipmentSlot.OffHand => offHandRotationOffset,
EquipmentSlot.Back => backRotationOffset,
EquipmentSlot.Head or EquipmentSlot.Chest or EquipmentSlot.Legs
or EquipmentSlot.Gloves or EquipmentSlot.Boots => armorRotationOffset,
private Vector3 GetScaleForSlot(EquipmentSlot slot)
EquipmentSlot.MainHand => mainHandScale,
EquipmentSlot.OffHand => offHandScale,
EquipmentSlot.Back => backScale,
EquipmentSlot.Head or EquipmentSlot.Chest or EquipmentSlot.Legs
or EquipmentSlot.Gloves or EquipmentSlot.Boots => armorScale,
public bool EquipItem(Item item)
if (item == null || item.equipSlot == EquipmentSlot.None) return false;
if (item is CombatEquipment combatEquip)
string requiredSkill = combatEquip.defaultAttackStyle switch
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush => "Precision",
AttackStyle.Range => "Range",
AttackStyle.Spellweaving => "Spellweaving",
if (!string.IsNullOrEmpty(requiredSkill))
PlayerSkills playerSkills = FindFirstObjectByType<PlayerSkills>();
Skill skill = playerSkills?.GetSkill(requiredSkill);
if (skill == null || skill.CurrentLevel < combatEquip.requiredLevel)
EnhancedChatbox.Instance?.AddMessage(
$"You need {combatEquip.requiredLevel} {requiredSkill} to equip this item.");
if (equippedItems.ContainsKey(item.equipSlot) && equippedItems[item.equipSlot] != null)
UnequipItem(item.equipSlot);
if (item is CombatEquipment equipment && equipment.equipmentModel != null)
Transform mountPoint = GetMountPointForSlot(item.equipSlot);
if (equippedModels.ContainsKey(item.equipSlot) && equippedModels[item.equipSlot] != null)
Destroy(equippedModels[item.equipSlot]);
GameObject model = Instantiate(equipment.equipmentModel, mountPoint);
model.transform.localPosition = GetPositionOffsetForSlot(item.equipSlot);
model.transform.localRotation = Quaternion.Euler(GetRotationOffsetForSlot(item.equipSlot));
model.transform.localScale = GetScaleForSlot(item.equipSlot);
equippedModels[item.equipSlot] = model;
equippedItems[item.equipSlot] = item;
OnEquipmentChanged?.Invoke(item.equipSlot, item);
if (item is WeaponData weapon && playerAnimator != null)
playerAnimator.SetInteger("WeaponType", (int)weapon.weaponType);
playerAnimator.SetInteger("AttackStyle", (int)weapon.defaultAttackStyle);
equipmentUI.UpdateEquipmentSlot(item.equipSlot, item);
StartCoroutine(ValidateUIAfterEquip(item.equipSlot));
catch (System.Exception e)
Debug.LogError($"Error equipping item: {e.Message}\n{e.StackTrace}");
public Item UnequipItem(EquipmentSlot slot)
if (!equippedItems.ContainsKey(slot))
Item item = equippedItems[slot];
Debug.Log($"Unequipping {item.itemName} from slot {slot}");
Item unequippedItem = item;
if (equippedModels.ContainsKey(slot) && equippedModels[slot] != null)
Destroy(equippedModels[slot]);
equippedModels[slot] = null;
equippedItems[slot] = null;
OnEquipmentChanged?.Invoke(slot, null);
if (characterAnimator != null && slot == EquipmentSlot.MainHand)
characterAnimator.SetInteger("WeaponType", 0);
characterAnimator.SetInteger("CombatStyle", 0);
if (Inventory.Instance != null)
bool added = Inventory.Instance.AddItem(unequippedItem, 1);
Debug.LogWarning($"Failed to add unequipped item {unequippedItem.itemName} to inventory");
Debug.Log($"Successfully returned {unequippedItem.itemName} to inventory");
equipmentUI.UpdateEquipmentSlot(slot, null);
private IEnumerator ValidateUIAfterEquip(EquipmentSlot slot)
equipmentUI.UpdateEquipmentSlot(slot, equippedItems[slot]);
equipmentUI.ValidateAllSlots();
var equippedItem = equippedItems[slot];
Debug.Log($"Post-equip validation - Slot: {slot}, Item: {equippedItem?.itemName ?? "null"}, " +
$"Has Icon: {equippedItem?.itemIcon != null}, UI Found: {equipmentUI != null}");
public void ValidateEquipmentState()
Debug.Log("=== Equipment State Validation ===");
foreach (var kvp in equippedItems)
Debug.Log($"Slot {slot}: {(item != null ? $"{item.itemName} (Icon: {item.itemIcon != null})" : "empty")}");
if (item != null && item.itemIcon == null)
Debug.LogError($"Item {item.itemName} in slot {slot} has no icon!");
private void UpdateAnimatorWeaponType(WeaponData weapon)
if (characterAnimator != null && weapon != null)
characterAnimator.SetInteger("WeaponType", (int)weapon.weaponType);
characterAnimator.SetInteger("AttackStyle", (int)weapon.defaultAttackStyle);
public void HandleEquipmentChanged(EquipmentSlot slot, Item item)
if (item is WeaponData weapon)
UpdateAnimatorWeaponType(weapon);
OnEquipmentChanged?.Invoke(slot, item);
private void HandleWeaponEquip(WeaponData weapon)
if (characterAnimator != null)
characterAnimator.SetInteger("WeaponType", (int)weapon.weaponType);
characterAnimator.SetInteger("AttackStyle", (int)weapon.defaultAttackStyle);
private void HandleWeaponChange(WeaponData weapon)
if (playerAnimator != null)
playerAnimator.SetInteger("WeaponType", (int)weapon.weaponType);
playerAnimator.SetInteger("AttackStyle", (int)weapon.defaultAttackStyle);
public Item GetEquippedItem(EquipmentSlot slot)
return equippedItems.TryGetValue(slot, out Item item) ? item : null;
public bool HasItemEquipped(ItemType itemType)
return equippedItems.Values.Any(item => item != null && item.itemType == itemType);
public bool HasSpecificItemEquipped(Item item)
return equippedItems.Values.Contains(item);
public Dictionary<EquipmentSlot, Item> GetAllEquippedItems()
return new Dictionary<EquipmentSlot, Item>(equippedItems);
public float GetTotalMightBonus()
return equippedItems.Values
.Where(item => item is CombatEquipment)
.Sum(item => ((CombatEquipment)item).bonuses.mightBonus);
public float GetTotalFortitudeBonus()
return equippedItems.Values
.Where(item => item is CombatEquipment)
.Sum(item => ((CombatEquipment)item).bonuses.GetDefenseBonus(AttackStyle.None));
public float GetTotalPrecisionBonus()
return equippedItems.Values
.Where(item => item is CombatEquipment)
.Sum(item => ((CombatEquipment)item).bonuses.GetAttackBonus(AttackStyle.Stab));
public void LogEquipmentPositions()
foreach (var kvp in equippedModels)
Debug.Log($"Equipment in slot {kvp.Key}: " +
$"Position = {kvp.Value.transform.localPosition}, " +
$"Rotation = {kvp.Value.transform.localRotation.eulerAngles}, " +
$"Scale = {kvp.Value.transform.localScale}");
public class EquipmentPositionAdjuster : MonoBehaviour
[Header("Equipment Transform")]
public Vector3 positionOffset;
public Vector3 rotationOffset;
public Vector3 scale = Vector3.one;
[Header("Equipment Type")]
public EquipmentSlot slotType = EquipmentSlot.MainHand;
[Header("Adjustment Speed")]
public float positionStep = 0.1f;
public float rotationStep = 5f;
public float scaleStep = 0.1f;
private Transform targetEquipment;
if (targetEquipment == null)
string mountPointName = slotType switch
EquipmentSlot.MainHand => "RightHandMount",
EquipmentSlot.OffHand => "LeftHandMount",
EquipmentSlot.Back => "BackMount",
if (!string.IsNullOrEmpty(mountPointName))
var mountPoint = GameObject.Find(mountPointName);
if (mountPoint != null && mountPoint.transform.childCount > 0)
targetEquipment = mountPoint.transform.GetChild(0);
if (targetEquipment != null)
targetEquipment.localPosition = positionOffset;
targetEquipment.localRotation = Quaternion.Euler(rotationOffset);
targetEquipment.localScale = scale;
public void CopyValuesToEquipmentManager()
var equipManager = FindFirstObjectByType<EquipmentManager>();
if (equipManager != null)
SerializedObject serializedObject = new SerializedObject(equipManager);
string positionProperty = slotType switch
EquipmentSlot.MainHand => "mainHandPositionOffset",
EquipmentSlot.OffHand => "offHandPositionOffset",
EquipmentSlot.Back => "backPositionOffset",
string rotationProperty = slotType switch
EquipmentSlot.MainHand => "mainHandRotationOffset",
EquipmentSlot.OffHand => "offHandRotationOffset",
EquipmentSlot.Back => "backRotationOffset",
string scaleProperty = slotType switch
EquipmentSlot.MainHand => "mainHandScale",
EquipmentSlot.OffHand => "offHandScale",
EquipmentSlot.Back => "backScale",
if (!string.IsNullOrEmpty(positionProperty))
var positionProp = serializedObject.FindProperty(positionProperty);
var rotationProp = serializedObject.FindProperty(rotationProperty);
var scaleProp = serializedObject.FindProperty(scaleProperty);
if (positionProp != null) positionProp.vector3Value = positionOffset;
if (rotationProp != null) rotationProp.vector3Value = rotationOffset;
if (scaleProp != null) scaleProp.vector3Value = scale;
serializedObject.ApplyModifiedProperties();
Debug.Log($"Copied values to EquipmentManager for slot {slotType}");
[CustomEditor(typeof(EquipmentPositionAdjuster))]
public class EquipmentPositionAdjusterEditor : Editor
public override void OnInspectorGUI()
EquipmentPositionAdjuster adjuster = (EquipmentPositionAdjuster)target;
if (GUILayout.Button("Copy Values to EquipmentManager"))
adjuster.CopyValuesToEquipmentManager();
"1. Select the slot type you want to adjust\n" +
"2. Adjust values in play mode to see real-time changes\n" +
"3. Click 'Copy Values' when satisfied to save to EquipmentManager",
public enum EquipmentSlot
using UnityEngine.EventSystems;
public class EquipmentSlotComponent : MonoBehaviour
[Header("Slot Configuration")]
public EquipmentSlot slotType;
public string displayName;
[Header("UI Components")]
public Image slotBackground;
public TextMeshProUGUI slotNameText;
public TextMeshProUGUI requirementText;
public GameObject highlightEffect;
public Sprite emptySlotSprite;
public void Initialize(EquipmentSlot type, string displayName)
if (slotNameText != null)
slotNameText.text = displayName;
itemIcon.enabled = false;
if (highlightEffect != null)
highlightEffect.SetActive(false);
if (requirementText != null)
requirementText.text = "";
private void ValidateComponents()
if (slotBackground == null)
slotBackground = GetComponent<Image>();
itemIcon = transform.Find("ItemIcon")?.GetComponent<Image>();
if (slotNameText == null)
slotNameText = transform.Find("SlotName")?.GetComponent<TextMeshProUGUI>();
if (requirementText == null)
requirementText = transform.Find("RequirementText")?.GetComponent<TextMeshProUGUI>();
if (GetComponent<Button>() == null)
gameObject.AddComponent<Button>();
private void InitializeSlot()
if (slotNameText != null)
slotNameText.text = displayName;
itemIcon.enabled = false;
if (requirementText != null)
requirementText.text = "";
if (highlightEffect != null)
highlightEffect.SetActive(false);
if (slotBackground != null && emptySlotSprite != null)
slotBackground.sprite = emptySlotSprite;
Debug.Log($"Initialized equipment slot: {displayName} ({slotType})");
public void UpdateSlot(Item item)
itemIcon.sprite = item.itemIcon;
itemIcon.color = Color.white;
if (requirementText != null && item is CombatEquipment combatEquip)
requirementText.text = $"Req: {combatEquip.requiredLevel} {combatEquip.defaultAttackStyle}";
requirementText.enabled = true;
Debug.Log($"Clearing slot {slotType}");
itemIcon.enabled = false;
if (requirementText != null)
requirementText.text = "";
requirementText.enabled = false;
if (highlightEffect != null)
highlightEffect.SetActive(false);
private void OnValidate()
if (string.IsNullOrEmpty(displayName))
displayName = slotType.ToString();
using System.Collections;
using System.Collections.Generic;
public class EquipmentUI : MonoBehaviour
[Header("Required References")]
[SerializeField] private RectTransform slotsContainer;
[SerializeField] private GameObject equipmentSlotPrefab;
[SerializeField] private GameObject combatPanelPrefab;
[SerializeField] private Transform combatPanelContainer;
[Header("Equipment Stats")]
[SerializeField] private TextMeshProUGUI attackBonusText;
[SerializeField] private TextMeshProUGUI strengthBonusText;
[SerializeField] private TextMeshProUGUI defenceBonusText;
[SerializeField] private TextMeshProUGUI combatStyleText;
[Header("Layout Settings")]
[SerializeField] private float slotSpacing = 10f;
[SerializeField] private Vector2 slotSize = new Vector2(60, 60);
private Dictionary<EquipmentSlot, EquipmentSlotComponent> equipmentSlots = new Dictionary<EquipmentSlot, EquipmentSlotComponent>();
private GridLayoutGroup gridLayout;
private EquipmentManager equipmentManager;
private EnhancedChatbox chatbox;
private bool isInitialized = false;
private CombatPanel combatPanel;
private readonly Dictionary<EquipmentSlot, string> slotNames = new Dictionary<EquipmentSlot, string>()
{ EquipmentSlot.Head, "Head" },
{ EquipmentSlot.Necklace, "Necklace" },
{ EquipmentSlot.Back, "Back" },
{ EquipmentSlot.Chest, "Chest" },
{ EquipmentSlot.MainHand, "Main Hand" },
{ EquipmentSlot.OffHand, "Off Hand" },
{ EquipmentSlot.Ring1, "Ring" },
{ EquipmentSlot.Ring2, "Specialty" },
{ EquipmentSlot.Gloves, "Hands" },
{ EquipmentSlot.Legs, "Legs" },
{ EquipmentSlot.Boots, "Feet" },
{ EquipmentSlot.Quiver, "Ammo" }
if (slotsContainer == null)
slotsContainer = transform.Find("SlotsContainer") as RectTransform;
public void EnsureInitialized()
if (isInitialized) return;
if (!ValidateReferences())
Debug.LogError($"EquipmentUI on {gameObject.name}: Missing required references!");
private void InitializeImmediate()
equipmentManager = EquipmentManager.Instance;
chatbox = EnhancedChatbox.Instance;
if (equipmentManager != null)
equipmentManager.OnEquipmentChanged += HandleEquipmentChanged;
Debug.Log($"EquipmentUI initialized successfully on {gameObject.name}");
catch (System.Exception e)
Debug.LogError($"Error initializing EquipmentUI: {e.Message}\n{e.StackTrace}");
private void InitializeCombatPanel()
if (combatPanelPrefab != null && combatPanelContainer != null)
GameObject panelObj = Instantiate(combatPanelPrefab, combatPanelContainer);
combatPanel = panelObj.GetComponent<CombatPanel>();
Debug.LogError("CombatPanel component not found on prefab!");
private bool ValidateReferences()
if (slotsContainer == null)
Debug.LogError($"EquipmentUI on {gameObject.name}: SlotsContainer is null!");
if (equipmentSlotPrefab == null)
Debug.LogError($"EquipmentUI on {gameObject.name}: EquipmentSlotPrefab is null!");
private IEnumerator InitializeCoroutine()
equipmentManager = EquipmentManager.Instance;
chatbox = EnhancedChatbox.Instance;
if (equipmentManager != null)
equipmentManager.OnEquipmentChanged += HandleEquipmentChanged;
Debug.Log($"EquipmentUI initialized successfully on {gameObject.name}");
catch (System.Exception e)
Debug.LogError($"Error initializing EquipmentUI: {e.Message}\n{e.StackTrace}");
private void InitializeLayout()
if (slotsContainer == null)
throw new System.NullReferenceException("SlotsContainer is null during InitializeLayout");
var existingLayout = slotsContainer.GetComponent<GridLayoutGroup>();
if (existingLayout != null)
DestroyImmediate(existingLayout);
gridLayout = slotsContainer.gameObject.AddComponent<GridLayoutGroup>();
gridLayout.cellSize = slotSize;
gridLayout.spacing = new Vector2(slotSpacing, slotSpacing);
gridLayout.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
gridLayout.constraintCount = 3;
var fitter = slotsContainer.gameObject.AddComponent<ContentSizeFitter>();
fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
private void CreateEquipmentSlots()
foreach (Transform child in slotsContainer)
Destroy(child.gameObject);
foreach (var slotType in slotLayout)
if (slotType == EquipmentSlot.None)
GameObject slotObj = Instantiate(equipmentSlotPrefab, slotsContainer);
var slotComponent = slotObj.GetComponent<EquipmentSlotComponent>();
if (slotComponent != null)
string displayName = slotNames.TryGetValue(slotType, out string name) ? name : "Equipment";
slotComponent.Initialize(slotType, displayName);
equipmentSlots[slotType] = slotComponent;
Button button = slotObj.GetComponent<Button>();
var slotTypeCopy = slotType;
button.onClick.AddListener(() => HandleSlotClick(slotTypeCopy));
private void CreateEmptySpacer()
GameObject spacer = new GameObject("Spacer");
spacer.transform.SetParent(slotsContainer, false);
RectTransform rt = spacer.AddComponent<RectTransform>();
private void HandleSlotClick(EquipmentSlot slot)
if (equipmentManager == null) return;
var equippedItem = equipmentManager.GetEquippedItem(slot);
if (equippedItem != null)
if (Inventory.Instance != null && !Inventory.Instance.IsFull())
if (equipmentSlots.TryGetValue(slot, out var slotComponent))
slotComponent.ClearSlot();
equipmentManager.UnequipItem(slot);
chatbox?.AddMessage($"Unequipped {equippedItem.itemName}");
chatbox?.AddMessage("Not enough inventory space to unequip item.");
private void HandleEquipmentChanged(EquipmentSlot slot, Item item)
UpdateEquipmentSlot(slot, item);
public void SetCombatPanel(CombatPanel panel)
public void UpdateEquipmentSlot(EquipmentSlot slot, Item item)
Debug.Log($"UpdateEquipmentSlot called for {slot} with item: {item?.itemName ?? "null"}");
if (equipmentSlots.TryGetValue(slot, out var slotComponent))
if (slotComponent == null)
Debug.LogError($"Slot component is null for slot {slot}");
slotComponent.UpdateSlot(item);
if (slot == EquipmentSlot.MainHand && combatPanel != null)
LayoutRebuilder.ForceRebuildLayoutImmediate(slotComponent.GetComponent<RectTransform>());
catch (System.Exception e)
Debug.LogError($"Error updating equipment slot {slot}: {e.Message}\n{e.StackTrace}");
Debug.LogError($"No slot component found for equipment slot: {slot}");
public void ValidateAllSlots()
foreach (var kvp in equipmentSlots)
var component = kvp.Value;
Debug.LogError($"Null component found for slot {slot}");
if (component.itemIcon == null)
Debug.LogError($"Null itemIcon reference in slot {slot}");
var equippedItem = EquipmentManager.Instance?.GetEquippedItem(slot);
if (equippedItem != null)
UpdateEquipmentSlot(slot, equippedItem);
private void UpdateBonusDisplay()
if (equipmentManager == null) return;
AttackStyle? mainhandStyle = null;
var equippedItems = equipmentManager.GetAllEquippedItems();
foreach (var kvp in equippedItems)
if (kvp.Value is CombatEquipment combatEquip)
var bonuses = combatEquip.bonuses;
totalAttack += bonuses.GetAttackBonus(combatEquip.defaultAttackStyle);
totalMight += bonuses.mightBonus;
totalFortitude += bonuses.GetDefenseBonus(combatEquip.defaultAttackStyle);
if (kvp.Key == EquipmentSlot.MainHand)
mainhandStyle = combatEquip.defaultAttackStyle;
if (attackBonusText != null)
attackBonusText.text = $"Attack: +{totalAttack}";
if (strengthBonusText != null)
strengthBonusText.text = $"Might: +{totalMight}";
if (defenceBonusText != null)
defenceBonusText.text = $"Fortitude: +{totalFortitude}";
if (combatStyleText != null)
combatStyleText.text = $"Style: {mainhandStyle?.ToString() ?? "None"}";
private void UpdateAllSlots()
if (equipmentManager == null) return;
foreach (var slot in equipmentSlots)
var item = equipmentManager.GetEquippedItem(slot.Key);
slot.Value.UpdateSlot(item);
if (equipmentManager != null)
equipmentManager.OnEquipmentChanged -= HandleEquipmentChanged;
private void OnValidate()
if (!Application.isPlaying) return;
if (enabled && (slotsContainer == null || equipmentSlotPrefab == null))
Debug.LogWarning("EquipmentUI: Missing required references!");
public static class ExperienceTable
private static readonly double[] xpTable;
public const int MAX_LEVEL = 300;
public const int MAX_VIRTUAL_LEVEL = 500;
private const double BASE_XP = 83;
private const double INITIAL_SCALE = 1.065;
private const double MID_SCALE = 1.085;
private const double HIGH_SCALE = 1.095;
private const double VIRTUAL_SCALE = 1.0;
private const int MEDIUM_MILESTONE = 50;
private const int HIGH_MILESTONE = 99;
private const double TARGET_MAX_XP = 1_000_000_000;
private const double VIRTUAL_TARGET_XP = 2_000_000_000;
xpTable = new double[MAX_VIRTUAL_LEVEL + 1];
private static void GenerateXPTable()
for (int level = 1; level < MEDIUM_MILESTONE; level++)
xp += BASE_XP * Math.Pow(INITIAL_SCALE, level - 1);
xpTable[level + 1] = Math.Floor(xp);
for (int level = MEDIUM_MILESTONE; level < HIGH_MILESTONE; level++)
double multiplier = 1.0 + (level - MEDIUM_MILESTONE) * 0.015;
xp += BASE_XP * Math.Pow(MID_SCALE, level - 1) * multiplier;
xpTable[level + 1] = Math.Floor(xp);
double remainingLevels = MAX_LEVEL - HIGH_MILESTONE;
double xpAt99 = xpTable[HIGH_MILESTONE];
double remainingXP = TARGET_MAX_XP - xpAt99;
for (int level = HIGH_MILESTONE; level < MAX_LEVEL; level++)
double progressionRatio = (level - HIGH_MILESTONE) / remainingLevels;
double scaledMultiplier = Math.Pow(HIGH_SCALE, progressionRatio * 2);
double levelXP = (remainingXP / remainingLevels) * scaledMultiplier;
xpTable[level + 1] = Math.Floor(xp);
double virtualStartXP = TARGET_MAX_XP;
double virtualRemainingXP = VIRTUAL_TARGET_XP - virtualStartXP;
for (int level = MAX_LEVEL; level < MAX_VIRTUAL_LEVEL; level++)
xp = virtualStartXP + (virtualRemainingXP * Math.Pow((level - MAX_LEVEL) / (double)(MAX_VIRTUAL_LEVEL - MAX_LEVEL), VIRTUAL_SCALE));
xpTable[level + 1] = Math.Floor(xp);
public static int CalculateCombatLevel(int fortitude, int vitality, int divinity, int highestCombatSkill)
double combatLevel = (fortitude + vitality + (divinity / 2.0) + highestCombatSkill) / 5.0;
return Math.Min(240, (int)Math.Floor(combatLevel));
public static float CalculateVitalityXP(float damageDealt, AttackStyle style)
float baseXP = damageDealt * 50.54f;
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush => baseXP * 0.25f,
AttackStyle.Range => baseXP * 0.333f,
AttackStyle.Spellweaving => baseXP,
private static void LogMilestones()
Debug.Log("=== XP Table Milestones ===");
int[] milestones = { 2, 10, 25, 50, 75, 99, 150, 200, 250, 300, 400, 500 };
foreach (int level in milestones)
if (level > MAX_VIRTUAL_LEVEL) break;
double xpForLevel = xpTable[level];
double xpForPrevious = xpTable[level - 1];
double xpDifference = xpForLevel - xpForPrevious;
Debug.Log($"Level {level,-3}: {FormatXP(xpForLevel),-10} XP " +
$"(+{FormatXP(xpDifference)} from previous)");
if (level == 99 || level == 150 || level == 200 || level == 250 || level == 300 || level == 500)
Debug.Log($"Percentage of max XP: {(xpForLevel / VIRTUAL_TARGET_XP * 100):F2}%");
public static string FormatXP(double xp)
return $"{xp / 1_000_000_000:N2}B";
return $"{xp / 1_000_000:N2}M";
return $"{xp / 1_000:N2}K";
public static int GetLevel(double xp)
for (int level = 1; level < xpTable.Length; level++)
if (xp < xpTable[level]) return level - 1;
return MAX_VIRTUAL_LEVEL;
public static double GetXPForLevel(int level)
if (level <= 0) return 0;
if (level > MAX_VIRTUAL_LEVEL) return xpTable[MAX_VIRTUAL_LEVEL];
public static double GetXPToNextLevel(int currentLevel, double currentXP)
if (currentLevel >= MAX_VIRTUAL_LEVEL) return 0;
return xpTable[currentLevel + 1] - currentXP;
public static float GetProgressToNextLevel(double currentXP)
int currentLevel = GetLevel(currentXP);
if (currentLevel >= MAX_VIRTUAL_LEVEL) return 1f;
double levelStartXP = GetXPForLevel(currentLevel);
double levelEndXP = GetXPForLevel(currentLevel + 1);
return (float)((currentXP - levelStartXP) / (levelEndXP - levelStartXP));
public static string GetXPRateInfo(int currentLevel, double xpPerAction, double actionsPerHour)
double xpPerHour = xpPerAction * actionsPerHour;
double xpToNextLevel = GetXPToNextLevel(currentLevel, GetXPForLevel(currentLevel));
double hoursToNextLevel = xpToNextLevel / xpPerHour;
return $"XP Rate: {FormatXP(xpPerHour)}/hr\n" +
$"Time to level: {(hoursToNextLevel >= 1 ? $"{hoursToNextLevel:F1} hours" : $"{hoursToNextLevel * 60:F0} minutes")}";
public interface ICombatant
CombatStats Stats { get; }
bool IsAlive => Stats.Vitality > 0;
Transform transform { get; }
void TakeDamage(int damage, AttackStyle style);
void Attack(ICombatant target);
AttackStyle GetCurrentCombatStyle();
[RequireComponent(typeof(Collider))]
public class InteractiveObject : MonoBehaviour
[Header("Highlight Settings")]
public Color hoverColor = new Color(1f, 1f, 1f, 0.3f);
public float outlineWidth = 0.02f;
private Material originalMaterial;
private Material highlightMaterial;
private Renderer objectRenderer;
private bool isHovered = false;
objectRenderer = GetComponent<Renderer>();
if (objectRenderer != null)
originalMaterial = objectRenderer.material;
highlightMaterial = new Material(Shader.Find("Standard"));
highlightMaterial.color = hoverColor;
public void SetupInteractiveObject(GameObject obj)
var collider = obj.GetComponent<Collider>() ?? obj.AddComponent<BoxCollider>();
var worldInteractable = obj.GetComponent<WorldInteractable>() ?? obj.AddComponent<WorldInteractable>();
var interactive = obj.GetComponent<InteractiveObject>() ?? obj.AddComponent<InteractiveObject>();
collider.isTrigger = true;
worldInteractable.objectName = obj.name;
var renderer = obj.GetComponent<Renderer>();
Debug.LogWarning($"Object {obj.name} needs a Renderer for highlight effects to work");
private void OnMouseEnter()
if (objectRenderer != null && !isHovered)
Cursor.SetCursor(CursorManager.Instance.interactiveCursor, Vector2.zero, CursorMode.Auto);
Material[] materials = objectRenderer.materials;
System.Array.Resize(ref materials, materials.Length + 1);
materials[materials.Length - 1] = highlightMaterial;
objectRenderer.materials = materials;
private void OnMouseExit()
if (objectRenderer != null && isHovered)
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
Material[] materials = objectRenderer.materials;
System.Array.Resize(ref materials, materials.Length - 1);
objectRenderer.materials = materials;
if (highlightMaterial != null)
Destroy(highlightMaterial);
using System.Collections.Generic;
public class Inventory : MonoBehaviour
public static Inventory Instance { get; private set; }
private InventoryUI inventoryUI;
public int maxSlots = 32;
public List<InventorySlot> slots = new List<InventorySlot>();
public event System.Action OnInventoryChanged;
DontDestroyOnLoad(gameObject);
private void FindInventoryUI()
inventoryUI = FindFirstObjectByType<InventoryUI>();
Debug.LogError("InventoryUI not found! Please ensure it exists in the scene.");
Debug.Log("Successfully found InventoryUI reference");
private void InitializeInventory()
for (int i = 0; i < maxSlots; i++)
slots.Add(new InventorySlot());
public bool AddItem(Item item, int quantity)
Debug.LogError("Attempted to add null item to inventory!");
Debug.Log($"Attempting to add {quantity}x {item.itemName} to inventory");
foreach (var slot in slots)
if (slot.Item == item && slot.Quantity < item.maxStack)
int availableSpace = item.maxStack - slot.Quantity;
int addQuantity = Mathf.Min(quantity, availableSpace);
slot.AddQuantity(addQuantity);
NotifyInventoryChanged();
foreach (var slot in slots)
slot.SetSlot(item, quantity);
Debug.Log($"Added {item.itemName} to empty slot");
NotifyInventoryChanged();
EnhancedChatbox.Instance?.AddMessage("Inventory is full!", EnhancedChatbox.MessageType.System);
public void ForceUIUpdate()
Debug.Log("Forcing inventory UI update");
inventoryUI = FindFirstObjectByType<InventoryUI>();
public int GetItemCount(Item item)
foreach (var slot in slots)
public bool HasItem(Item item, int quantity = 1)
return GetItemCount(item) >= quantity;
public void RemoveItem(Item item, int quantity)
int remainingToRemove = quantity;
foreach (var slot in slots)
if (slot.Quantity >= remainingToRemove)
slot.AddQuantity(-remainingToRemove);
NotifyInventoryChanged();
remainingToRemove -= slot.Quantity;
NotifyInventoryChanged();
return !slots.Exists(slot => slot.IsEmpty);
private void NotifyInventoryChanged()
Debug.Log("Inventory changed, notifying listeners");
OnInventoryChanged?.Invoke();
Debug.Log("Updating InventoryUI after change");
Debug.LogError("InventoryUI still not found during notification!");
var ui = GameObject.FindObjectOfType<InventoryUI>();
Debug.Log("Found InventoryUI using alternative method");
public Tool GetToolOfType(ToolType toolType)
foreach (var slot in slots)
if (slot.Item is Tool tool && tool.toolType == toolType)
using UnityEngine.EventSystems;
using System.Collections.Generic;
public class InventoryItemUI : MonoBehaviour, IPointerClickHandler
public Item item { get; private set; }
private InventorySlot slot;
private Inventory inventory;
public void Initialize(Item newItem, InventorySlot newSlot)
inventory = FindFirstObjectByType<Inventory>();
public void OnPointerClick(PointerEventData eventData)
if (item == null) return;
if (eventData.button == PointerEventData.InputButton.Left)
else if (eventData.button == PointerEventData.InputButton.Right)
private void HandleLeftClick()
if (item == null) return;
else if (item.CanConsume)
else if (item.itemType == ItemType.Resource)
private void ShowContextMenu()
if (item == null || ContextMenu.Instance == null) return;
List<ContextMenuOption> options = new List<ContextMenuOption>();
options.Add(new ContextMenuOption("Equip", EquipItem));
options.Add(new ContextMenuOption("Use", ConsumeItem));
options.Add(new ContextMenuOption("Examine", ExamineItem));
options.Add(new ContextMenuOption("Drop", DropItem));
ContextMenu.Instance.ShowMenu(Input.mousePosition, options);
if (item == null) return;
Debug.Log($"Attempting to equip: {item.itemName}");
if (item is CombatEquipment || item.CanEquip)
equipped = EquipmentManager.Instance.EquipItem(item);
inventory.RemoveItem(item, 1);
EnhancedChatbox.Instance?.AddMessage($"Equipped {item.itemName}");
Debug.Log($"Successfully equipped {item.itemName}");
EnhancedChatbox.Instance?.AddMessage($"Cannot equip {item.itemName}");
Debug.Log($"Failed to equip {item.itemName}");
catch (System.Exception e)
Debug.LogError($"Error equipping item: {e.Message}\n{e.StackTrace}");
EnhancedChatbox.Instance?.AddMessage("Error equipping item");
private void ConsumeItem()
if (item == null || !item.CanConsume) return;
Debug.Log($"Using item: {item.itemName}");
inventory.RemoveItem(item, 1);
EnhancedChatbox.Instance?.AddMessage($"You used {item.itemName}.");
private void ExamineItem()
if (item == null) return;
EnhancedChatbox.Instance?.AddMessage($"It's a {item.itemName}. {item.description}");
if (item == null) return;
GameObject player = GameObject.FindGameObjectWithTag("Player");
Vector3 dropPos = player.transform.position - player.transform.forward;
SpawnDroppedItem(dropPos);
inventory.RemoveItem(item, 1);
EnhancedChatbox.Instance?.AddMessage($"You dropped {item.itemName}");
private void SpawnDroppedItem(Vector3 position)
GameObject droppedObj = new GameObject($"Dropped_{item.itemName}");
droppedObj.transform.position = position;
var itemComponent = droppedObj.AddComponent<DroppedItem>();
itemComponent.Initialize(item, 1, 180f);
Debug.Log($"Spawned dropped item: {item.itemName} at {position}");
public class InventorySlot
[SerializeField] private Item _item;
[SerializeField] private int _quantity;
public Item Item => _item;
public int Quantity => _quantity;
public bool IsEmpty => _item == null;
public InventorySlot(Item item, int quantity)
public void SetSlot(Item item, int quantity)
Debug.Log($"Setting slot with item: {item?.itemName ?? "null"} x{quantity}");
_quantity = Mathf.Max(0, quantity);
public void AddQuantity(int amount)
_quantity = Mathf.Max(0, _quantity + amount);
if (_quantity == 0) _item = null;
public bool CanAddItem(Item item, int amount)
return IsEmpty || (_item == item && _item.isStackable);
using UnityEngine.EventSystems;
using System.Collections.Generic;
public class InventorySlotUI : MonoBehaviour, IPointerClickHandler
public TMP_Text quantityText;
private Item _currentItem;
private int _currentQuantity;
private Inventory inventoryRef;
private EquipmentManager equipmentManager;
public Item CurrentItem => _currentItem;
public int CurrentQuantity => _currentQuantity;
inventoryRef = Inventory.Instance;
if (inventoryRef == null)
Debug.LogError("Inventory Instance not found!");
equipmentManager = EquipmentManager.Instance;
if (equipmentManager == null)
Debug.LogError("EquipmentManager Instance not found!");
public void OnPointerClick(PointerEventData eventData)
if (_currentItem == null) return;
if (eventData.button == PointerEventData.InputButton.Left)
else if (eventData.button == PointerEventData.InputButton.Right)
private void HandleLeftClick()
if (_currentItem == null) return;
if (_currentItem.CanEquip)
else if (_currentItem.CanConsume)
private void ShowContextMenu()
if (_currentItem == null || ContextMenu.Instance == null)
Debug.LogWarning("Cannot show context menu: " +
(_currentItem == null ? "No item selected" : "ContextMenu not found in scene"));
List<ContextMenuOption> options = new List<ContextMenuOption>();
if (_currentItem.CanEquip)
options.Add(new ContextMenuOption("Equip", () => HandleEquip()));
if (_currentItem.CanConsume)
options.Add(new ContextMenuOption("Use", () => HandleConsume()));
options.Add(new ContextMenuOption("Drop", () => HandleDrop()));
options.Add(new ContextMenuOption("Examine", () => HandleExamine()));
ContextMenu.Instance.ShowMenu(Input.mousePosition, options);
public void HandleEquip()
if (_currentItem == null || equipmentManager == null)
Debug.LogWarning($"Cannot equip - Item: {_currentItem != null}, EquipManager: {equipmentManager != null}");
if (!_currentItem.CanEquip)
Debug.LogWarning($"Item {_currentItem.itemName} cannot be equipped");
Debug.Log($"Attempting to equip {_currentItem.itemName}");
Item itemToEquip = _currentItem;
inventoryRef.RemoveItem(_currentItem, 1);
bool equipped = equipmentManager.EquipItem(itemToEquip);
EnhancedChatbox.Instance?.AddMessage($"Equipped {itemToEquip.itemName}");
ContextMenu.Instance?.HideMenu();
inventoryRef.AddItem(itemToEquip, 1);
EnhancedChatbox.Instance?.AddMessage($"Cannot equip {itemToEquip.itemName}");
private void HandleConsume()
if (_currentItem == null || !_currentItem.CanConsume) return;
Debug.Log($"Using {_currentItem.itemName}");
inventoryRef.RemoveItem(_currentItem, 1);
EnhancedChatbox.Instance?.AddMessage($"You used {_currentItem.itemName}");
private void HandleDrop()
if (_currentItem == null) return;
GameObject player = GameObject.FindGameObjectWithTag("Player");
Vector3 dropPos = player.transform.position - player.transform.forward;
SpawnDroppedItem(dropPos);
inventoryRef.RemoveItem(_currentItem, 1);
EnhancedChatbox.Instance?.AddMessage($"Dropped {_currentItem.itemName}");
private void HandleExamine()
if (_currentItem == null) return;
EnhancedChatbox.Instance?.AddMessage($"It's a {_currentItem.itemName}. {_currentItem.description}");
public void UpdateSlotUI(Item item, int quantity)
Debug.Log($"Updating slot {slotIndex} UI with item: {item?.itemName ?? "null"} x{quantity}");
_currentQuantity = quantity;
Debug.LogError($"ItemIcon is null on slot {slotIndex}");
if (item.itemIcon == null)
Debug.LogError($"Item {item.itemName} has no icon sprite assigned!");
itemIcon.sprite = item.itemIcon;
itemIcon.color = Color.white;
if (quantityText != null)
quantityText.text = item.isStackable && quantity > 1 ? quantity.ToString() : "";
quantityText.enabled = item.isStackable && quantity > 1;
Debug.Log($"Updated slot {slotIndex} with {item.itemName}, sprite: {(item.itemIcon != null)}, enabled: {itemIcon.enabled}");
itemIcon.enabled = false;
if (quantityText != null)
quantityText.enabled = false;
Debug.Log($"Cleared slot {slotIndex}");
private void SpawnDroppedItem(Vector3 position)
GameObject droppedObj = new GameObject($"Dropped_{_currentItem.itemName}");
droppedObj.transform.position = position;
var itemComponent = droppedObj.AddComponent<DroppedItem>();
itemComponent.Initialize(_currentItem, 1, 180f);
using System.Collections.Generic;
public class InventoryUI : MonoBehaviour
public GameObject slotPrefab;
public Transform slotParent;
private Inventory inventory;
private InventorySlotUI[] slotUIs;
DontDestroyOnLoad(gameObject);
Debug.Log("InventoryUI starting initialization");
inventory = Inventory.Instance;
Debug.LogError("Inventory instance not found!");
private void InitializeSlots()
foreach (Transform child in slotParent)
Destroy(child.gameObject);
slotUIs = new InventorySlotUI[inventory.slots.Count];
for (int i = 0; i < inventory.slots.Count; i++)
GameObject newSlot = Instantiate(slotPrefab, slotParent);
InventorySlotUI slotUI = newSlot.GetComponent<InventorySlotUI>();
public void ValidateSlots()
Debug.Log("Validating inventory slots...");
foreach (var slotUI in slotUIs)
Debug.LogError("Found null slot UI!");
if (slotUI.itemIcon == null)
Debug.LogError($"Slot {slotUI.slotIndex} has null itemIcon reference!");
var currentItem = slotUI.CurrentItem;
Debug.Log($"Slot {slotUI.slotIndex}: Item={currentItem.itemName}, " +
$"Icon Enabled={slotUI.itemIcon.enabled}, " +
$"Has Sprite={slotUI.itemIcon.sprite != null}");
Debug.LogError("Cannot update UI - Inventory reference is null!");
inventory = Inventory.Instance;
if (inventory == null) return;
Debug.Log("Updating Inventory UI");
if (slotUIs == null || slotUIs.Length != inventory.slots.Count)
Debug.Log("Reinitializing slots due to mismatch");
foreach (var slotUI in slotUIs)
int index = System.Array.IndexOf(slotUIs, slotUI);
var slot = inventory.slots[index];
slotUI.UpdateSlotUI(slot.Item, slot.Quantity);
public void SwapItems(int slotIndexA, int slotIndexB)
if (slotIndexA == slotIndexB) return;
if (slotIndexA < 0 || slotIndexB < 0 ||
slotIndexA >= inventory.slots.Count ||
slotIndexB >= inventory.slots.Count)
Debug.LogWarning("Invalid slot indices for swap operation");
var tempSlot = inventory.slots[slotIndexA];
inventory.slots[slotIndexA] = inventory.slots[slotIndexB];
inventory.slots[slotIndexB] = tempSlot;
slotUIs[slotIndexA].UpdateSlotUI(inventory.slots[slotIndexA].Item, inventory.slots[slotIndexA].Quantity);
slotUIs[slotIndexB].UpdateSlotUI(inventory.slots[slotIndexB].Item, inventory.slots[slotIndexB].Quantity);
Debug.Log($"Swapped items in slots {slotIndexA} and {slotIndexB}");
string SkillName { get; }
int CurrentLevel { get; }
double CurrentXP { get; }
void AddExperience(double xp);
bool HasRequirement(int level);
[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
public class Item : ScriptableObject
public ItemType itemType = ItemType.Default;
public bool isStackable = false;
public string description;
public ToolType toolType;
public EquipmentSlot equipSlot = EquipmentSlot.None;
public bool CanEquip => itemType == ItemType.Equipment || itemType == ItemType.Tool;
public bool CanConsume => itemType == ItemType.Consumable;
public bool IsGatheringTool => itemType == ItemType.Tool;
using System.Collections.Generic;
public class ItemDatabase : MonoBehaviour
public static ItemDatabase Instance { get; private set; }
private Dictionary<string, Item> itemDictionary = new Dictionary<string, Item>();
DontDestroyOnLoad(gameObject);
private void InitializeDatabase()
AddItemsToDictionary(ores);
AddItemsToDictionary(logs);
AddItemsToDictionary(gems);
AddItemsToDictionary(tools);
private void AddItemsToDictionary(List<Item> items)
foreach (var item in items)
if (!itemDictionary.ContainsKey(item.itemName))
itemDictionary[item.itemName] = item;
public Item GetItem(string itemName)
if (itemDictionary.TryGetValue(itemName, out Item item))
Debug.LogWarning($"Item not found in database: {itemName}");
[RequireComponent(typeof(ItemPickup))]
public class ItemHighlight : MonoBehaviour
private Material originalMaterial;
private MeshRenderer meshRenderer;
private ItemPickup itemPickup;
private Color highlightColor = new Color(1f, 0.8f, 0.2f, 1f);
private Color normalColor = Color.white;
private bool isHighlighted = false;
[SerializeField] private float highlightDistance = 5f;
meshRenderer = GetComponent<MeshRenderer>();
itemPickup = GetComponent<ItemPickup>();
if (meshRenderer != null)
originalMaterial = new Material(meshRenderer.material);
meshRenderer.material = originalMaterial;
if (meshRenderer != null)
float pulse = (Mathf.Sin(Time.time * 4f) + 1f) * 0.5f;
meshRenderer.material.color = Color.Lerp(normalColor, highlightColor, pulse);
meshRenderer.material.color = normalColor;
GameObject player = GameObject.FindGameObjectWithTag("Player");
float distance = Vector3.Distance(transform.position, player.transform.position);
return distance <= highlightDistance;
if (originalMaterial != null)
Destroy(originalMaterial);
using System.Collections;
[RequireComponent(typeof(Collider))]
public class ItemPickup : MonoBehaviour
public float pickupRange = 3f;
private GameObject player;
private Inventory playerInventory;
private NavMeshAgent playerAgent;
private bool isInitialized = false;
private bool isMovingToItem = false;
private WorldInteractable worldInteractable;
StartCoroutine(InitializePickup());
private void SetupInteraction()
worldInteractable = gameObject.AddComponent<WorldInteractable>();
worldInteractable.objectName = item.itemName;
worldInteractable.AddCustomOption($"Pick up {item.itemName}" + (quantity > 1 ? $" x{quantity}" : ""),
var clickToMove = FindFirstObjectByType<ClickToMove>();
clickToMove.MoveToTarget(transform.position);
() => item != null && !isMovingToItem,
worldInteractable.examineText = item.description;
private IEnumerator InitializePickup()
playerAgent = player.GetComponent<NavMeshAgent>();
yield return new WaitForSeconds(0.5f);
private bool FindPlayer()
player = GameObject.FindGameObjectWithTag("Player");
playerInventory = player.GetComponent<Inventory>();
if (playerInventory != null)
Debug.Log("Player and Inventory found successfully.");
Debug.LogWarning("Player found, but it doesn't have an Inventory component!");
Debug.Log("Components on player: " + string.Join(", ", player.GetComponents<Component>().Select(c => c.GetType().Name)));
playerInventory = player.GetComponentInChildren<Inventory>();
if (playerInventory != null)
Debug.Log("Inventory found in child object.");
Debug.LogWarning("Player not found. Will retry.");
float distance = Vector3.Distance(transform.position, player.transform.position);
if (distance <= pickupRange)
private void HandleItemClick()
float distance = Vector3.Distance(transform.position, player.transform.position);
if (distance <= pickupRange)
private void MovePlayerToItem()
playerAgent.SetDestination(transform.position);
Debug.Log($"Moving to item: {item.itemName}");
Debug.LogWarning("Player NavMeshAgent not found. Cannot move to item.");
private void AttemptPickup()
Debug.Log($"Attempting pickup of {item.itemName}");
if (playerInventory != null)
if (playerInventory.AddItem(item, quantity))
EnhancedChatbox.Instance?.AddMessage($"Picked up: {item.itemName}");
Debug.Log($"Successfully picked up {item.itemName}");
EnhancedChatbox.Instance?.AddMessage("Inventory is full!");
Debug.Log($"Pickup failed: Inventory full. Item: {item.itemName}");
Debug.LogError("PlayerInventory is still null! Reinitializing...");
StartCoroutine(InitializePickup());
public class LevelingSystem : MonoBehaviour
public int currentLevel = 1;
public int maxLevel = 300;
public int currentXP = 0;
public int xpToNextLevel = 100;
public float xpGrowthFactor = 1.2f;
public delegate void OnLevelChange(int level, int currentXP, int xpToNextLevel);
public event OnLevelChange OnLevelChangeEvent;
public void AddExperience(int xp)
if (currentLevel >= maxLevel)
Debug.Log("Max level reached!");
while (currentXP >= xpToNextLevel && currentLevel < maxLevel)
currentXP -= xpToNextLevel;
xpToNextLevel = Mathf.FloorToInt(xpToNextLevel * xpGrowthFactor);
Debug.Log($"Level Up! Current Level: {currentLevel}, XP for Next Level: {xpToNextLevel}");
OnLevelChangeEvent?.Invoke(currentLevel, currentXP, xpToNextLevel);
using System.Collections;
public class MineableRock : ResourceNode
[Header("Mining Specific")]
public int gemChance = 5;
[Header("Mining Effects")]
public ParticleSystem miningParticles;
public GameObject depleteEffect;
private Material originalMaterial;
protected override void Initialize()
requiredToolType = ToolType.Pickaxe;
SetGatheringEffect(miningParticles);
Debug.Log($"[Rock-{oreType}] Initialized with requiredToolType: {requiredToolType}");
public override bool Gather(Tool tool)
Debug.Log($"[Rock-{oreType}] Attempting to gather with tool: {(tool != null ? tool.itemName : "null")}");
if (tool == null || tool.toolType != ToolType.Pickaxe)
Debug.Log($"[Rock-{oreType}] Invalid tool for mining");
if (!HasResources() || IsRespawning)
Debug.Log($"[Rock-{oreType}] No resources or is respawning");
Debug.Log($"[Rock-{oreType}] Base gather failed");
public bool Mine(Tool tool)
private void CheckForGems(Tool tool)
float bonusChance = gemChance;
bonusChance *= tool.GetEfficiencyMultiplier(1);
if (Random.value < bonusChance / 100f)
Debug.Log("Found a gem!");
protected override IEnumerator FadeOut()
if (depleteEffect != null)
Instantiate(depleteEffect, transform.position, Quaternion.identity);
while (elapsed < duration)
elapsed += Time.deltaTime;
if (resourceRenderer != null && resourceRenderer.material != null)
Color newColor = resourceRenderer.material.color;
newColor.a = 1 - (elapsed / duration);
resourceRenderer.material.color = newColor;
resourceRenderer.enabled = false;
resourceCollider.enabled = false;
protected override IEnumerator FadeIn()
resourceRenderer.enabled = true;
resourceCollider.enabled = true;
while (elapsed < duration)
elapsed += Time.deltaTime;
if (resourceRenderer != null && resourceRenderer.material != null)
Color newColor = resourceRenderer.material.color;
newColor.a = elapsed / duration;
resourceRenderer.material.color = newColor;
public class Mining : BaseSkillAction
[Header("Mining Specific")]
public ParticleSystem miningParticles;
private MineableRock currentRock;
protected override void Start()
requiredSkillName = "Mining";
requiredToolType = ToolType.Pickaxe;
Debug.Log("[Mining] Initialized mining skill action.");
public override void SetTarget(ResourceNode target)
if (target is MineableRock rock)
Debug.Log($"[Mining] Targeting rock: {rock.name}");
Debug.LogError("[Mining] Invalid target type. Expected MineableRock.");
protected override string GetAnimationTrigger() => "isMining";
protected virtual void PlayMiningEffect()
if (miningParticles != null)
Debug.Log("Playing mining particles effect.");
protected override bool ProcessAction()
Debug.Log($"[Mining] Processing action with target: {currentTarget?.name}");
if (currentTarget != null && currentTarget.IsRespawning)
Debug.Log($"[{requiredSkillName}] Cannot gather - resource is respawning");
Debug.Log("[Mining] Current rock is null, attempting to set from target");
currentRock = currentTarget as MineableRock;
Debug.Log("[Mining] Failed to set current rock from target");
if (!currentRock.HasResources())
Debug.Log("[Mining] Rock depleted!");
Tool currentPickaxe = inventory.GetToolOfType(requiredToolType) as Tool;
if (currentPickaxe == null)
Debug.Log("[Mining] No pickaxe found in inventory!");
NotifyPlayer("You need a pickaxe to mine.");
if (currentRock.Gather(currentPickaxe))
Debug.Log("[Mining] Successfully mined rock.");
inventory.AddItem(currentRock.primaryResource, 1);
playerSkills.AddExperience(requiredSkillName, currentRock.baseXP);
Debug.Log("[Mining] Failed to mine rock.");
public override void StopAction()
SetAnimationState(false);
public class MusicTrigger : MonoBehaviour
public AudioSource musicSource;
public AudioClip areaMusic;
gameObject.layer = LayerMask.NameToLayer("Ignore Raycast");
private void OnTriggerEnter(Collider other)
if (other.CompareTag("Player"))
if (musicSource != null && areaMusic != null)
musicSource.clip = areaMusic;
Debug.LogWarning("MusicSource or AreaMusic not assigned.");
public class PlayerAnimationController : MonoBehaviour
private Animator animator;
public TMP_Text runButtonText;
private NavMeshAgent navMeshAgent;
public bool isRunning = false;
animator = GetComponent<Animator>();
navMeshAgent = GetComponent<NavMeshAgent>();
Debug.LogError("Animator not found! Ensure an Animator is attached to the player.");
if (navMeshAgent == null)
Debug.LogError("NavMeshAgent not found! Ensure a NavMeshAgent is attached to the player.");
HandleMovementAnimations();
private void HandleMovementAnimations()
float speed = navMeshAgent.velocity.magnitude;
bool isMoving = speed > 0.1f;
animator.SetBool("isWalking", isMoving);
animator.SetFloat("Speed", speed);
if (!navMeshAgent.pathPending && navMeshAgent.remainingDistance <= navMeshAgent.stoppingDistance)
animator.SetBool("isWalking", false);
animator.SetFloat("Speed", 0);
animator.SetBool("isRunning", isRunning);
if (runButtonText != null)
runButtonText.text = isRunning ? "Walk" : "Run";
private void UpdateAgentSpeed()
navMeshAgent.speed = isRunning ? 6f : 3.5f;
Debug.Log($"Running toggled: {isRunning}, NavMeshAgent Speed: {navMeshAgent.speed}");
using System.Collections;
using Random = UnityEngine.Random;
public class PlayerCombatant : MonoBehaviour, ICombatant
private CombatStats stats;
private PlayerSkills playerSkills;
private EquipmentManager equipmentManager;
private RegenerationManager regenManager;
public CombatStats Stats => stats;
public bool IsAlive => Stats.Vitality > 0;
Transform ICombatant.transform => transform;
playerSkills = GetComponent<PlayerSkills>();
equipmentManager = GetComponent<EquipmentManager>();
regenManager = GetComponent<RegenerationManager>();
private void InitializeStats()
vitality: GetSkillLevel("Vitality"),
precision: GetSkillLevel("Precision"),
might: GetSkillLevel("Might"),
fortitude: GetSkillLevel("Fortitude"),
range: GetSkillLevel("Range"),
spellweaving: GetSkillLevel("Spellweaving"),
divinity: GetSkillLevel("Divinity")
Debug.Log($"Initialized player combat stats: " +
$"Vitality={stats.Vitality}, " +
$"Precision={stats.Precision}, " +
$"Might={stats.Might}, " +
$"Fortitude={stats.Fortitude}");
if (regenManager != null)
regenManager.StartRegeneration();
private int GetSkillLevel(string skillName)
var skill = playerSkills?.GetSkill(skillName);
return skill?.CurrentLevel ?? 1;
public AttackStyle GetCurrentCombatStyle()
var weapon = equipmentManager?.GetEquippedItem(EquipmentSlot.MainHand) as WeaponData;
return weapon?.defaultAttackStyle ?? AttackStyle.Stab;
public void Attack(ICombatant target)
if (!IsAlive || target == null || !target.IsAlive) return;
if (regenManager != null)
regenManager.PauseRegeneration(2.0f);
var weapon = equipmentManager?.GetEquippedItem(EquipmentSlot.MainHand) as CombatWeaponData;
PerformUnarmedAttack(target);
float accuracy = CombatCalculator.CalculateAccuracy(this, target, weapon.availableAttackStyles[0]);
if (Random.value < accuracy)
int damage = CalculateDamage(target, weapon);
target.TakeDamage(damage, weapon.attackStyle);
CombatExperience.AwardCombatExperience(this, damage, weapon);
Debug.Log($"Hit {target.GetDisplayName()} for {damage} damage!");
Debug.Log($"Missed attack against {target.GetDisplayName()}!");
private void PerformUnarmedAttack(ICombatant target)
float accuracy = (float)GetSkillLevel("Precision") /
(GetSkillLevel("Precision") + target.Stats.Fortitude);
if (Random.value < accuracy)
int damage = Mathf.Max(1, GetSkillLevel("Might") / 10);
target.TakeDamage(damage, AttackStyle.Crush);
Debug.Log($"Unarmed hit for {damage} damage!");
private int CalculateDamage(ICombatant target, CombatWeaponData weapon)
return weapon.attackStyle switch
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush =>
Mathf.FloorToInt(0.5f + (stats.Might * (weapon.bonuses.mightBonus + 64)) / 640f),
Mathf.FloorToInt(0.5f + (stats.Range * (weapon.bonuses.rangeStrengthBonus + 64)) / 640f),
AttackStyle.Spellweaving =>
Mathf.FloorToInt(10 * (1 + weapon.bonuses.spellweavingDamage / 100f)),
public void TakeDamage(int damage, AttackStyle style)
if (regenManager != null)
regenManager.PauseRegeneration(3.0f);
float defenceBonus = equipmentManager?.GetTotalFortitudeBonus() ?? 0;
int reducedDamage = Mathf.Max(1, (int)(damage * (1 - defenceBonus/100)));
stats.ModifyVitality(-reducedDamage);
Debug.Log($"Player took {reducedDamage} damage! Vitality: {stats.Vitality}/{stats.MaxVitality}");
StartCoroutine(HandleDeath());
private IEnumerator HandleDeath()
if (regenManager != null)
regenManager.StopRegeneration();
if (TryGetComponent<Animator>(out var animator))
animator.SetTrigger("Death");
yield return new WaitForSeconds(2f);
public float GetAttackSpeed()
var weapon = equipmentManager?.GetEquippedItem(EquipmentSlot.MainHand) as CombatWeaponData;
return weapon?.attackSpeed ?? 2.4f;
public string GetDisplayName() => "Player";
public class PlayerController : MonoBehaviour
private ResourceNode currentResourceTarget;
public static PlayerController Instance { get; private set; }
public void SetResourceTarget(ResourceNode target)
if (currentResourceTarget != null)
currentResourceTarget.OnRespawnStarted -= HandleResourceRespawn;
currentResourceTarget = target;
if (currentResourceTarget != null)
currentResourceTarget.OnRespawnStarted += HandleResourceRespawn;
private void HandleResourceRespawn(ResourceNode resource)
currentResourceTarget = null;
public void StopCurrentAction()
Woodcutting woodcutting = FindFirstObjectByType<Woodcutting>();
if (woodcutting != null && woodcutting.IsPerformingAction)
woodcutting.StopAction();
Mining mining = FindFirstObjectByType<Mining>();
if (mining != null && mining.IsPerformingAction)
using System.Collections.Generic;
public class PlayerSkills : MonoBehaviour
[HideInInspector] public Skill skill;
public double startingXP = 0;
[Header("UI References")]
[SerializeField] private UIManager uiManager;
[SerializeField] private SkillsUI skillsUI;
[Header("Skills Configuration")]
[SerializeField] private List<SkillData> skillDataList = new List<SkillData>();
[Header("Combat Skills")]
[SerializeField] private List<SkillData> combatSkills = new List<SkillData>
new SkillData { skillName = "Vitality", startingXP = ExperienceTable.GetXPForLevel(10) },
new SkillData { skillName = "Precision" },
new SkillData { skillName = "Might" },
new SkillData { skillName = "Fortitude" },
new SkillData { skillName = "Range" },
new SkillData { skillName = "Spellweaving" },
new SkillData { skillName = "Divinity" }
[Header("Gathering Skills")]
[SerializeField] private List<SkillData> gatheringSkills = new List<SkillData>
new SkillData { skillName = "Woodcutting" },
new SkillData { skillName = "Mining" },
private List<Skill> skills = new List<Skill>();
public IReadOnlyList<Skill> Skills => skills;
uiManager = FindFirstObjectByType<UIManager>();
Debug.Log($"UIManager assigned in Awake: {uiManager != null}");
skillsUI = FindFirstObjectByType<SkillsUI>();
Debug.Log($"SkillsUI assigned in Awake: {skillsUI != null}");
Debug.Log("PlayerSkills.Start invoked.");
Debug.Log($"Initialized and subscribed to {skills.Count} skills.");
private void InitializeSkills()
Debug.Log("Initializing skills...");
skillDataList.AddRange(combatSkills);
skillDataList.AddRange(gatheringSkills);
foreach (var skillData in skillDataList)
InitializeSkill(skillData);
Debug.Log($"Total skills initialized: {skills.Count}");
private void InitializeSkill(SkillData skillData)
if (!string.IsNullOrEmpty(skillData.skillName))
Skill newSkill = new Skill(skillData.skillName);
if (skillData.startingXP > 0)
newSkill.AddExperience(skillData.startingXP);
Debug.Log($"Created skill: {newSkill.SkillName} at level {newSkill.CurrentLevel}");
skillData.skill = newSkill;
Debug.LogError("Skill name is empty in SkillData. Skipping this entry.");
private void InitializeReferences()
uiManager = FindFirstObjectByType<UIManager>();
Debug.Log($"UIManager assigned: {uiManager != null}");
skillsUI = FindFirstObjectByType<SkillsUI>();
Debug.Log($"SkillsUI assigned: {skillsUI != null}");
private void ValidateReferences()
Debug.LogError("UIManager reference is missing!");
Debug.LogError("SkillsUI reference is missing!");
private void SetupUICallbacks()
Debug.LogError("UIManager not found! Cannot set up UI callbacks.");
Debug.LogError("SkillsUI not found! Cannot set up UI callbacks.");
Debug.Log($"Setting up UI callbacks for {skills.Count} skills.");
foreach (var skill in skills)
var currentSkill = skill;
currentSkill.ClearSubscribers();
currentSkill.OnXPGained += (skillName, level, xp, nextLevel) =>
Debug.Log($"[PlayerSkills] XP gained callback for {skillName}: Level={level}, XP={xp}");
uiManager.UpdateSkillUI(skillName, level, (int)xp, (int)nextLevel);
currentSkill.OnLevelUp += (skillName, level) =>
Debug.Log($"[PlayerSkills] Level up callback for {skillName}: Level={level}");
var chatbox = EnhancedChatbox.Instance;
chatbox.AddMessage($"Congratulations! Your {skillName} level is now {level}!",
EnhancedChatbox.MessageType.LevelUp);
Debug.Log($"[PlayerSkills] Subscribed to events for {currentSkill.SkillName}");
Debug.LogWarning("Encountered a null skill while setting up callbacks");
public void AddExperience(string skillName, double xp)
Debug.Log($"Adding {xp} XP to {skillName}");
var skill = GetSkill(skillName);
if (!skill.HasSubscribers())
Debug.LogWarning($"[PlayerSkills] Missing subscribers for {skillName}, re-initializing callbacks");
Debug.LogError($"Skill not found: {skillName}");
Debug.LogError($"Available skills: {string.Join(", ", skills.Select(s => s.SkillName))}");
public int CalculateCombatLevel()
Skill fortitude = GetSkill("Fortitude");
Skill vitality = GetSkill("Vitality");
Skill divinity = GetSkill("Divinity");
int meleeLevel = Mathf.Max(
GetSkill("Precision")?.CurrentLevel ?? 1,
GetSkill("Might")?.CurrentLevel ?? 1
int rangeLevel = GetSkill("Range")?.CurrentLevel ?? 1;
int spellweavingLevel = GetSkill("Spellweaving")?.CurrentLevel ?? 1;
int highestCombatStyle = Mathf.Max(meleeLevel, rangeLevel, spellweavingLevel);
(fortitude?.CurrentLevel ?? 1) +
(vitality?.CurrentLevel ?? 10) +
((divinity?.CurrentLevel ?? 1) / 2f) +
return Mathf.FloorToInt(combatLevel);
public void RefreshCallbacks()
Debug.Log("[PlayerSkills] Manually refreshing callbacks");
private void HandleXPGained(string name, int level, double xp, double nextLevel)
Debug.Log($"XP gained callback triggered for {name} - Level: {level}, XP: {xp}");
uiManager.UpdateSkillUI(name, level, (int)xp, (int)nextLevel);
Debug.LogError("UIManager is null. Cannot update UI.");
private void HandleLevelUp(string name, int level)
Debug.Log($"Level up callback triggered for {name} - New Level: {level}");
EnhancedChatbox.Instance?.AddMessage(
$"Congratulations! Your {name} level is now {level}!",
EnhancedChatbox.MessageType.LevelUp
Debug.Log("Unsubscribing from skill events.");
foreach (var skill in skills)
skill.OnXPGained -= HandleXPGained;
skill.OnLevelUp -= HandleLevelUp;
public Skill GetSkill(string skillName)
return skills.FirstOrDefault(s =>
string.Equals(s.SkillName, skillName, System.StringComparison.OrdinalIgnoreCase));
public int GetTotalLevel()
return skills.Sum(skill => skill.CurrentLevel);
private void OnValidate()
Debug.Log("Validating SkillDataList...");
foreach (var data in skillDataList)
Debug.Log($"SkillData: {data.skillName}");
var duplicates = skillDataList
.GroupBy(x => x.skillName)
.Where(g => g.Count() > 1)
Debug.LogError($"Duplicate skill names found: {string.Join(", ", duplicates)}");
using System.Collections;
public class RegenerationManager : MonoBehaviour
[Header("Regeneration Settings")]
public float baseRegenRate = 0.01f;
public float regenTickInterval = 0.6f;
private CombatStats combatStats;
private bool isRegenerating = true;
private Coroutine regenCoroutine;
combatStats = GetComponent<PlayerCombatant>()?.Stats;
Debug.LogError("RegenerationManager: No CombatStats found!");
public void StartRegeneration()
if (regenCoroutine == null)
regenCoroutine = StartCoroutine(RegenerationRoutine());
public void StopRegeneration()
if (regenCoroutine != null)
StopCoroutine(regenCoroutine);
private IEnumerator RegenerationRoutine()
WaitForSeconds wait = new WaitForSeconds(regenTickInterval);
if (combatStats != null && combatStats.Vitality < combatStats.MaxVitality)
float regenAmount = combatStats.MaxVitality * baseRegenRate * regenTickInterval;
combatStats.ModifyVitality(Mathf.CeilToInt(regenAmount));
Debug.Log($"Regenerated {regenAmount:F1} Vitality. Current: {combatStats.Vitality}/{combatStats.MaxVitality}");
public void PauseRegeneration(float duration)
StartCoroutine(ResumeRegenerationAfterDelay(duration));
private IEnumerator ResumeRegenerationAfterDelay(float delay)
yield return new WaitForSeconds(delay);
using System.Collections;
using System.Collections.Generic;
public abstract class ResourceNode : MonoBehaviour
[Header("Basic Properties")]
public int requiredLevel = 1;
public float baseGatherTime = 3f;
public Item primaryResource;
public ToolType requiredToolType;
[Header("Resource Settings")]
public int resourcesRemaining;
public int maxResources = 5;
public float respawnTime = 30f;
public float depletionChance = 0.2f;
public List<BonusItem> bonusItems = new List<BonusItem>();
protected ParticleSystem gatheringEffect;
public bool IsRespawning => isRespawning;
protected bool isRespawning = false;
protected Renderer resourceRenderer;
protected WorldInteractable worldInteractable;
protected Collider resourceCollider;
public event System.Action<ResourceNode> OnRespawnStarted;
public event System.Action<ResourceNode> OnRespawnCompleted;
protected virtual void Start()
Debug.Log($"[{nodeName}] Initialized with requiredToolType: {requiredToolType}");
protected virtual void Initialize()
resourceRenderer = GetComponent<Renderer>();
resourceCollider = GetComponent<Collider>();
resourcesRemaining = maxResources;
Debug.Log($"[{nodeName}] Resources: {resourcesRemaining}/{maxResources}");
public virtual bool CanGather(ISkill skill)
if (isRespawning || resourcesRemaining <= 0)
Debug.Log($"[{nodeName}] Cannot gather: isRespawning={isRespawning}, resourcesRemaining={resourcesRemaining}");
if (skill != null && skill.CurrentLevel < requiredLevel)
Debug.Log($"[{nodeName}] Skill level too low. Required: {requiredLevel}, Current: {skill.CurrentLevel}");
public virtual bool Gather(Tool tool)
Debug.Log($"[{nodeName}] Attempting to gather with tool: {(tool != null ? tool.itemName : "null")}");
Debug.Log($"[{nodeName}] Cannot gather - basic checks failed");
Debug.Log($"[{nodeName}] No tool provided for gathering");
if (tool.toolType != requiredToolType)
Debug.Log($"[{nodeName}] Incorrect tool type. Required: {requiredToolType}, Used: {tool.toolType}");
Debug.Log($"[{nodeName}] Gathered successfully. Resources remaining: {resourcesRemaining}");
if (gatheringEffect != null)
foreach (var bonus in bonusItems)
if (Random.value < bonus.chance)
if (resourcesRemaining <= 0 || (Random.value < depletionChance))
StartCoroutine(RespawnResource());
public bool HasResources()
return resourcesRemaining > 0 && !isRespawning;
protected virtual void SpawnBonus(BonusItem bonus)
if (Inventory.Instance != null)
bool added = Inventory.Instance.AddItem(bonus.item, 1);
Debug.Log($"[{nodeName}] Added bonus item {bonus.item.itemName} to inventory: {added}");
protected virtual IEnumerator RespawnResource()
Debug.Log($"[{nodeName}] Starting respawn sequence");
OnRespawnStarted?.Invoke(this);
yield return StartCoroutine(FadeOut());
Debug.Log($"[{nodeName}] Waiting {respawnTime} seconds to respawn");
yield return new WaitForSeconds(respawnTime);
yield return StartCoroutine(FadeIn());
resourcesRemaining = maxResources;
OnRespawnCompleted?.Invoke(this);
Debug.Log($"[{nodeName}] Respawn complete. Resources reset to {maxResources}");
protected abstract IEnumerator FadeOut();
protected abstract IEnumerator FadeIn();
protected void SetGatheringEffect(ParticleSystem effect)
gatheringEffect = effect;
protected virtual void SetupInteraction()
worldInteractable = gameObject.AddComponent<WorldInteractable>();
worldInteractable.objectName = nodeName;
if (this is ChoppableTree)
worldInteractable.AddCustomOption($"Chop {nodeName}",
else if (this is MineableRock)
worldInteractable.AddCustomOption($"Mine {nodeName}",
OnRespawnStarted += (node) => worldInteractable.enabled = false;
OnRespawnCompleted += (node) => worldInteractable.enabled = true;
public bool IsValidForGathering()
return !isRespawning && resourcesRemaining > 0;
private void AttemptGather()
var player = GameObject.FindGameObjectWithTag("Player");
if (this is ChoppableTree)
var woodcutting = player.GetComponent<Woodcutting>();
woodcutting.SetTarget(this);
else if (this is MineableRock)
var mining = player.GetComponent<Mining>();
public class RuneScapeCamera : MonoBehaviour
public float distance = 10f;
public float minDistance = 5f;
public float maxDistance = 20f;
public float zoomSpeed = 2f;
public float rotationSpeed = 100f;
public float orbitSpeed = 50f;
public float heightOffset = 2f;
public float minVerticalAngle = -30f;
public float maxVerticalAngle = 60f;
public float groundClearance = 1f;
private float currentRotation = 0f;
private float currentVerticalRotation = 45f;
private float smoothedVerticalRotation;
private const float groundCheckRadius = 0.3f;
if (target == null) return;
if (Input.GetKey(KeyCode.LeftArrow))
currentRotation += orbitSpeed * Time.deltaTime;
if (Input.GetKey(KeyCode.RightArrow))
currentRotation -= orbitSpeed * Time.deltaTime;
float previousVerticalRotation = currentVerticalRotation;
if (Input.GetKey(KeyCode.UpArrow))
currentVerticalRotation -= orbitSpeed * Time.deltaTime;
if (Input.GetKey(KeyCode.DownArrow))
currentVerticalRotation += orbitSpeed * Time.deltaTime;
if (Input.GetMouseButton(2))
float mouseX = Input.GetAxis("Mouse X") * rotationSpeed * Time.deltaTime;
float mouseY = Input.GetAxis("Mouse Y") * rotationSpeed * Time.deltaTime;
currentRotation -= mouseX;
currentVerticalRotation = Mathf.Clamp(currentVerticalRotation + mouseY, minVerticalAngle, maxVerticalAngle);
currentVerticalRotation = Mathf.Clamp(currentVerticalRotation, minVerticalAngle, maxVerticalAngle);
float scroll = Input.GetAxis("Mouse ScrollWheel") * zoomSpeed;
distance = Mathf.Clamp(distance - scroll, minDistance, maxDistance);
Vector3 targetPos = target.position + Vector3.up * heightOffset;
Vector3 rotation = new Vector3(currentVerticalRotation, currentRotation, 0);
Vector3 negDistance = new Vector3(0.0f, 0.0f, -distance);
Quaternion rot = Quaternion.Euler(rotation);
Vector3 desiredPosition = rot * negDistance + targetPos;
Vector3 directionToCamera = (desiredPosition - targetPos).normalized;
if (Physics.SphereCast(targetPos, groundCheckRadius, directionToCamera, out hit, distance))
float distanceToGround = hit.distance;
if (distanceToGround < groundClearance)
currentVerticalRotation = previousVerticalRotation;
rotation = new Vector3(currentVerticalRotation, currentRotation, 0);
rot = Quaternion.Euler(rotation);
desiredPosition = rot * negDistance + targetPos;
transform.rotation = rot;
transform.position = desiredPosition;
public void ResetCamera()
currentRotation = target.eulerAngles.y;
currentVerticalRotation = 45f;
private void OnDrawGizmosSelected()
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, groundCheckRadius);
public class SidebarButton : MonoBehaviour
public GameObject targetPanel;
private bool initialized = false;
private void Initialize()
button = GetComponent<Button>();
Debug.LogError($"No Button component found on {gameObject.name}!");
Debug.LogError($"No target panel assigned for {gameObject.name}!");
button.onClick.RemoveAllListeners();
button.onClick.AddListener(OnButtonClick);
targetPanel.SetActive(false);
Debug.Log($"SidebarButton initialized for {gameObject.name}");
public void OnButtonClick()
if (targetPanel == null || UIManager.Instance == null)
Debug.LogError($"Missing references on {gameObject.name}!");
targetPanel.SetActive(true);
var equipmentUI = targetPanel.GetComponent<EquipmentUI>();
equipmentUI.EnsureInitialized();
UIManager.Instance.TogglePanel(targetPanel);
using System.Collections.Generic;
public class SidebarUI : MonoBehaviour
public class SidebarButton
public GameObject associatedPanel;
public List<SidebarButton> sidebarButtons;
public GameObject buttonPrefab;
public Transform buttonContainer;
private GameObject activePanel;
private void InitializeSidebar()
foreach (var buttonData in sidebarButtons)
GameObject buttonObj = Instantiate(buttonPrefab, buttonContainer);
Button button = buttonObj.GetComponent<Button>();
Image icon = buttonObj.GetComponentInChildren<Image>();
icon.sprite = buttonData.icon;
button.onClick.AddListener(() => OnSidebarButtonClicked(buttonData));
if (buttonData.associatedPanel != null)
buttonData.associatedPanel.SetActive(false);
private void OnSidebarButtonClicked(SidebarButton buttonData)
activePanel.SetActive(false);
if (activePanel != buttonData.associatedPanel)
buttonData.associatedPanel.SetActive(true);
activePanel = buttonData.associatedPanel;
public class Skill : ISkill
private double currentXP;
private int cachedLevel = 1;
public string SkillName => skillName;
public int CurrentLevel => GetCurrentLevel();
public double CurrentXP => currentXP;
public double XPToNextLevel => ExperienceTable.GetXPToNextLevel(CurrentLevel, currentXP);
public event Action<string, int, double, double> OnXPGained;
public event Action<string, int> OnLevelUp;
public Skill(string name)
public void InitializeUI()
OnXPGained?.Invoke(skillName, CurrentLevel, currentXP, XPToNextLevel);
public void AddExperience(double xp)
Debug.LogWarning($"[{skillName}] Attempted to add invalid XP amount: {xp}");
double oldXP = currentXP;
int oldLevel = CurrentLevel;
currentXP = Math.Min(currentXP + xp, ExperienceTable.GetXPForLevel(ExperienceTable.MAX_LEVEL));
int newLevel = GetCurrentLevel();
Debug.Log($"[{skillName}] Added {xp} XP. Old XP: {oldXP}, New XP: {currentXP}");
Debug.Log($"[{skillName}] Level Up! {oldLevel} -> {newLevel}");
OnLevelUp.Invoke(skillName, newLevel);
Debug.LogWarning($"[{skillName}] OnLevelUp has no subscribers!");
Debug.Log($"[{skillName}] Triggering XP update. Level={CurrentLevel}, XP={currentXP}, NextLevel={XPToNextLevel}");
OnXPGained.Invoke(skillName, CurrentLevel, currentXP, XPToNextLevel);
Debug.LogWarning($"[{skillName}] OnXPGained has no subscribers! Requesting callback refresh.");
var playerSkills = UnityEngine.Object.FindFirstObjectByType<PlayerSkills>();
if (playerSkills != null)
playerSkills.RefreshCallbacks();
public bool HasSubscribers()
return OnXPGained != null;
public void ClearSubscribers()
Debug.Log($"[{skillName}] Cleared event subscribers");
private int GetCurrentLevel()
cachedLevel = ExperienceTable.GetLevel(currentXP);
public bool HasRequirement(int level) => CurrentLevel >= level;
public string GetSkillInfo()
return $"{skillName} - Level: {CurrentLevel} - XP: {ExperienceTable.FormatXP(currentXP)} " +
$"- Next Level: {ExperienceTable.FormatXP(XPToNextLevel)} XP remaining";
using UnityEngine.EventSystems;
public class SkillEntryUI : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
[Header("UI Components")]
public TextMeshProUGUI skillNameText;
public TextMeshProUGUI levelText;
public TextMeshProUGUI xpText;
public Slider xpProgressBar;
public GameObject tooltipPanel;
public TextMeshProUGUI tooltipText;
public void UpdateDisplay(string skillName, int level, double currentXP, double xpToNextLevel)
if (skillNameText != null)
skillNameText.text = skillName;
levelText.text = $"Level {level}";
if (level >= ExperienceTable.MAX_LEVEL)
xpText.text = ExperienceTable.FormatXP(currentXP);
xpText.text = $"{currentXP:N0} / {(currentXP + xpToNextLevel):N0} XP";
if (xpProgressBar != null)
xpProgressBar.value = ExperienceTable.GetProgressToNextLevel(currentXP);
UpdateTooltip(skillName, level, currentXP, xpToNextLevel);
private void UpdateTooltip(string skillName, int level, double currentXP, double xpToNextLevel)
if (tooltipText == null) return;
string tooltipString = $"{skillName}\n";
tooltipString += $"Level: {level}";
if (level < ExperienceTable.MAX_LEVEL)
double totalXPForLevel = currentXP + xpToNextLevel;
double percentage = totalXPForLevel > 0 ? (currentXP / totalXPForLevel) * 100 : 0;
tooltipString += $"\nXP: {ExperienceTable.FormatXP(currentXP)}";
tooltipString += $"\nNext Level: {ExperienceTable.FormatXP(xpToNextLevel)} XP remaining";
tooltipString += $"\nProgress: {percentage:F1}%";
tooltipString += $"\nMax Level!\nTotal XP: {ExperienceTable.FormatXP(currentXP)}";
tooltipText.text = tooltipString;
public void OnPointerEnter(PointerEventData eventData)
if (tooltipPanel != null)
tooltipPanel.SetActive(true);
public void OnPointerExit(PointerEventData eventData)
if (tooltipPanel != null)
tooltipPanel.SetActive(false);
public class SkillsAudioManager : MonoBehaviour
private AudioSource audioSource;
audioSource = GetComponent<AudioSource>();
audioSource = gameObject.AddComponent<AudioSource>();
public void PlaySkillSound(AudioClip clip)
if (audioSource != null && clip != null)
audioSource.PlayOneShot(clip);
[CreateAssetMenu(fileName = "New Skill", menuName = "Skills/Skill Data")]
public class SkillData : ScriptableObject
public string description;
[Header("Skill Settings")]
public int startingLevel = 1;
public int maxLevel = 99;
public float experienceMultiplier = 1f;
[Header("Tool Requirements")]
public ItemType[] allowedTools;
public int minimumToolLevel = 1;
using System.Collections;
using System.Collections.Generic;
public class SkillsUI : MonoBehaviour
public class SkillIconData
public string description;
public GameObject skillEntryPrefab;
public Transform skillsContainer;
public TextMeshProUGUI totalLevelText;
[Header("Visual Settings")]
public Color normalColor = Color.white;
public Color maxLevelColor = Color.yellow;
public Color virtualLevelColor = new Color(0.7f, 0.7f, 1f);
public List<SkillIconData> skillIcons = new List<SkillIconData>();
private Dictionary<string, SkillEntryUI> skillEntries = new Dictionary<string, SkillEntryUI>();
private PlayerSkills cachedPlayerSkills;
Debug.Log("SkillsUI Starting...");
StartCoroutine(DelayedInitialization());
private void InitializeReferences()
if (cachedPlayerSkills == null)
cachedPlayerSkills = FindFirstObjectByType<PlayerSkills>();
if (cachedPlayerSkills == null)
Debug.LogError("PlayerSkills reference not found!");
Debug.Log($"Found PlayerSkills reference");
if (skillEntryPrefab == null || skillsContainer == null)
Debug.LogError($"Missing critical references - Prefab: {skillEntryPrefab != null}, Container: {skillsContainer != null}");
private IEnumerator DelayedInitialization()
yield return new WaitForSeconds(0.1f);
private void InitializeSkills()
if (cachedPlayerSkills != null)
var skills = cachedPlayerSkills.Skills.ToList();
Debug.Log($"Initializing {skills.Count} skills");
foreach (var skill in skills)
CreateSkillEntry(skill.SkillName);
Debug.LogError("Cannot initialize skills - PlayerSkills reference is null");
private void ClearContainer()
if (skillsContainer != null)
foreach (Transform child in skillsContainer)
Destroy(child.gameObject);
Debug.Log("Cleared existing skill entries");
private void CreateSkillEntry(string skillName)
if (string.IsNullOrEmpty(skillName) || skillEntryPrefab == null || skillsContainer == null)
Debug.LogError($"Cannot create skill entry - missing references for {skillName}");
GameObject entryObj = Instantiate(skillEntryPrefab, skillsContainer);
Debug.LogError($"Failed to instantiate skill entry prefab for {skillName}");
SkillEntryUI entryUI = entryObj.GetComponent<SkillEntryUI>();
Debug.LogError($"SkillEntryUI component missing on prefab for {skillName}");
var iconData = skillIcons.FirstOrDefault(x =>
string.Equals(x.skillName, skillName, StringComparison.OrdinalIgnoreCase));
if (iconData != null && iconData.icon != null && entryUI.skillIcon != null)
entryUI.skillIcon.sprite = iconData.icon;
entryUI.skillIcon.enabled = true;
Debug.Log($"Set icon for {skillName}");
if (entryUI.skillIcon != null)
entryUI.skillIcon.enabled = false;
Debug.LogWarning($"No icon data found for {skillName}");
if (cachedPlayerSkills != null)
var skill = cachedPlayerSkills.GetSkill(skillName);
skillEntries[skillName] = entryUI;
entryUI.UpdateDisplay(skillName, skill.CurrentLevel, skill.CurrentXP, skill.XPToNextLevel);
Debug.Log($"Successfully created entry for {skillName}");
Debug.LogError($"Failed to get skill data for {skillName}");
catch (System.Exception e)
Debug.LogError($"Error creating skill entry for {skillName}: {e.Message}\n{e.StackTrace}");
public void UpdateSkillDisplay(string skillName, int level, double currentXP, double xpToNextLevel)
Debug.Log($"Updating display for {skillName} - Level: {level}, Current XP: {currentXP}, Next Level: {xpToNextLevel}");
if (!skillEntries.ContainsKey(skillName))
Debug.LogWarning($"No existing entry for {skillName}, creating new one");
CreateSkillEntry(skillName);
if (skillEntries.TryGetValue(skillName, out SkillEntryUI entry))
entry.UpdateDisplay(skillName, level, currentXP, xpToNextLevel);
Debug.Log($"Successfully updated UI for {skillName}");
Debug.LogError($"Entry UI is null for {skillName}, recreating entry");
skillEntries.Remove(skillName);
CreateSkillEntry(skillName);
catch (System.Exception e)
Debug.LogError($"Error updating skill display for {skillName}: {e.Message}\n{e.StackTrace}");
private void UpdateTotalLevel()
if (cachedPlayerSkills != null && totalLevelText != null)
int total = cachedPlayerSkills.GetTotalLevel();
int maxedSkills = cachedPlayerSkills.Skills.Count(s => s.CurrentLevel >= ExperienceTable.MAX_LEVEL);
string levelText = $"Total Level: {total}";
levelText += $" ({maxedSkills} maxed)";
totalLevelText.text = levelText;
Debug.Log($"Updated total level display: {levelText}");
Debug.Log("Refreshing all skill displays");
if (cachedPlayerSkills != null)
foreach (var skill in cachedPlayerSkills.Skills)
Debug.LogError("Cannot refresh UI - PlayerSkills reference is null");
[CreateAssetMenu(fileName = "New Tool", menuName = "RVAMP/Items/Tool")]
[Header("Tool Properties")]
public float toolEfficiency = 1f;
public int requiredLevel = 1;
public float GetEfficiencyMultiplier(int skillLevel)
float baseMultiplier = 1f;
float tierBonus = tier * 0.1f;
float skillBonus = skillLevel * 0.01f;
return (baseMultiplier + tierBonus + skillBonus) * toolEfficiency;
using UnityEngine.EventSystems;
public class UIDebugHelper : MonoBehaviour
public Canvas targetCanvas;
public GameObject contextMenuPanel;
public RectTransform menuContainer;
public EventSystem eventSystem;
[Header("Debug Options")]
public bool showDebugVisuals = true;
public Color debugColor = Color.yellow;
if (!showDebugVisuals) return;
GUILayout.BeginArea(new Rect(10, 10, 300, 500));
GUILayout.Label("=== UI Debug Info ===");
if (targetCanvas != null)
GUILayout.Label($"Canvas Mode: {targetCanvas.renderMode}");
GUILayout.Label($"Sort Order: {targetCanvas.sortingOrder}");
GUILayout.Label($"Pixel Perfect: {targetCanvas.pixelPerfect}");
GUILayout.Label($"Scale Factor: {targetCanvas.scaleFactor}");
GUILayout.Label("No Canvas assigned!");
if (contextMenuPanel != null)
var rect = contextMenuPanel.GetComponent<RectTransform>();
GUILayout.Label($"Menu Active: {contextMenuPanel.activeSelf}");
GUILayout.Label($"Menu Position: {rect.position}");
GUILayout.Label($"Menu Size: {rect.sizeDelta}");
GUILayout.Label($"Menu Scale: {rect.localScale}");
var canvasGroup = contextMenuPanel.GetComponent<CanvasGroup>();
GUILayout.Label($"Alpha: {canvasGroup.alpha}");
GUILayout.Label($"Interactable: {canvasGroup.interactable}");
GUILayout.Label($"Blocks Raycasts: {canvasGroup.blocksRaycasts}");
GUILayout.Label("No Context Menu Panel assigned!");
GUILayout.Label($"EventSystem enabled: {eventSystem.enabled}");
GUILayout.Label($"Current Selected: {eventSystem.currentSelectedGameObject?.name ?? "none"}");
GUILayout.Label($"Mouse Position: {Input.mousePosition}");
GUILayout.Label($"Over UI: {eventSystem.IsPointerOverGameObject()}");
GUILayout.Label("No EventSystem found!");
private void OnDrawGizmos()
if (!showDebugVisuals || contextMenuPanel == null) return;
var rect = contextMenuPanel.GetComponent<RectTransform>();
var corners = new Vector3[4];
rect.GetWorldCorners(corners);
Gizmos.color = debugColor;
for (int i = 0; i < 4; i++)
Gizmos.DrawLine(corners[i], corners[(i + 1) % 4]);
[UnityEngine.ContextMenu("Fix Common Issues")]
public void FixCommonIssues()
if (targetCanvas != null)
targetCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
targetCanvas.sortingOrder = 100;
if (contextMenuPanel != null)
var rect = contextMenuPanel.GetComponent<RectTransform>();
rect.localScale = Vector3.one;
var canvasGroup = contextMenuPanel.GetComponent<CanvasGroup>();
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
eventSystem = FindFirstObjectByType<EventSystem>();
var eventSystemObj = new GameObject("EventSystem");
eventSystem = eventSystemObj.AddComponent<EventSystem>();
eventSystemObj.AddComponent<StandaloneInputModule>();
using System.Collections.Generic;
public class UIManager : MonoBehaviour
public static UIManager Instance { get; private set; }
[Header("Panel References")]
public GameObject inventoryPanel;
public GameObject equipmentPanel;
public GameObject skillsPanel;
public GameObject settingsPanel;
[Header("Skills UI Reference")]
public SkillsUI skillsUI;
private GameObject currentActivePanel;
skillsUI = skillsPanel?.GetComponent<SkillsUI>();
public void ToggleSkillsPanel()
bool isActive = !skillsPanel.activeSelf;
skillsPanel.SetActive(isActive);
if (isActive && skillsUI != null)
public void TogglePanel(GameObject panel)
if (currentActivePanel == panel)
currentActivePanel = null;
if (currentActivePanel != null)
currentActivePanel.SetActive(false);
currentActivePanel = panel;
public void UpdateSkillUI(string skillName, int level, int currentXP, int xpToNextLevel)
Debug.Log($"UIManager.UpdateSkillUI called for {skillName}");
Debug.LogError("SkillsUI reference is null in UIManager!");
skillsUI.UpdateSkillDisplay(skillName, level, currentXP, xpToNextLevel);
public void OpenSkillGuide(string skillName)
Debug.Log($"Opening skill guide for {skillName}");
public void HideAllPanels()
if (inventoryPanel) inventoryPanel.SetActive(false);
if (equipmentPanel) equipmentPanel.SetActive(false);
if (skillsPanel) skillsPanel.SetActive(false);
if (settingsPanel) settingsPanel.SetActive(false);
currentActivePanel = null;
using System.Collections.Generic;
public class VisibleEquipmentManager : MonoBehaviour
public class EquipmentSocket
public EquipmentSlot slot;
public Transform mountPoint;
[Header("Equipment Positioning")]
public Vector3 positionOffset = new Vector3(0, 0, 0);
public Vector3 rotationOffset = new Vector3(0, -90, 0);
public Vector3 scale = Vector3.one;
[Header("Equipment Setup")]
public List<EquipmentSocket> equipmentSockets;
private Dictionary<EquipmentSlot, GameObject> equippedModels = new Dictionary<EquipmentSlot, GameObject>();
var equipmentManager = EquipmentManager.Instance;
if (equipmentManager != null)
equipmentManager.OnEquipmentChanged += HandleEquipmentChanged;
private void HandleEquipmentChanged(EquipmentSlot slot, Item item)
Debug.Log($"Handling equipment change for slot {slot}");
if (equippedModels.TryGetValue(slot, out GameObject existingModel))
equippedModels.Remove(slot);
if (item != null && item is CombatEquipment combatEquip)
var socket = equipmentSockets.Find(s => s.slot == slot);
if (socket != null && socket.mountPoint != null && combatEquip.equipmentModel != null)
GameObject model = Instantiate(combatEquip.equipmentModel, socket.mountPoint);
model.transform.localPosition = socket.positionOffset;
model.transform.localRotation = Quaternion.Euler(socket.rotationOffset);
model.transform.localScale = socket.scale;
equippedModels[slot] = model;
Debug.Log($"Successfully created visual model for {item.itemName}");
catch (System.Exception e)
Debug.LogError($"Error creating equipment model: {e.Message}");
Debug.LogWarning($"Missing socket configuration for {slot} or equipment model for {item.itemName}");
if (EquipmentManager.Instance != null)
EquipmentManager.Instance.OnEquipmentChanged -= HandleEquipmentChanged;
public class WeaponAttackStyle
public AttackStyle attackStyle;
public CombatTrainingStyle trainingStyle;
public bool splitXPWithFortitude;
public float accuracyModifier = 1f;
public float damageModifier = 1f;
public float experienceMultiplier = 4f;
public CombatStyle GetCombatStyle()
return attackStyle switch
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush => CombatStyle.Melee,
AttackStyle.Range => CombatStyle.Ranged,
AttackStyle.Spellweaving => CombatStyle.Magic,
[CreateAssetMenu(fileName = "New Weapon", menuName = "Combat/Weapon")]
public class WeaponData : CombatEquipment
[Header("Weapon Properties")]
public WeaponType weaponType;
public CombatConfig combatConfig;
[Header("Attack Properties")]
public float baseAttackSpeed = 2.4f;
public bool hasSpecialAttack;
public int specialAttackCost = 50;
[Header("Combat Training")]
public CombatTrainingStyle primaryTrainingStyle;
public CombatTrainingStyle[] availableTrainingStyles;
public bool allowFortitudeTraining = true;
public float accuracyModifier = 1f;
public float damageModifier = 1f;
itemType = ItemType.Equipment;
equipSlot = EquipmentSlot.MainHand;
private void SetupCombatConfig()
if (combatConfig == null)
combatConfig = CreateInstance<CombatConfig>();
combatConfig.baseAttackInterval = baseAttackSpeed;
combatConfig.primaryStyle.attackStyle = defaultAttackStyle;
combatConfig.primaryStyle.trainingStyle = primaryTrainingStyle;
combatConfig.primaryStyle.splitXPWithFortitude = allowFortitudeTraining;
public int CalculateMaxHit(PlayerSkills skills)
if (skills == null) return 0;
switch (defaultAttackStyle)
var mightSkill = skills.GetSkill("Might");
if (mightSkill == null) return 0;
return Mathf.FloorToInt(0.5f + (mightSkill.CurrentLevel * (bonuses.mightBonus + 64)) / 640f);
var rangeSkill = skills.GetSkill("Range");
if (rangeSkill == null) return 0;
return Mathf.FloorToInt(0.5f + (rangeSkill.CurrentLevel * (bonuses.rangeStrengthBonus + 64)) / 640f);
case AttackStyle.Spellweaving:
int baseSpellDamage = 10;
return Mathf.FloorToInt(baseSpellDamage * (1 + bonuses.spellweavingDamage / 100f));
public float CalculateHitChance(PlayerSkills skills, CombatStats targetStats)
if (skills == null || targetStats == null) return 0f;
var attackSkill = GetAttackSkill(skills);
if (attackSkill == null) return 0f;
float effectiveAccuracy = attackSkill.CurrentLevel + bonuses.GetAttackBonus(defaultAttackStyle);
float effectiveFortitude = targetStats.Fortitude + targetStats.GetDefenseBonus(defaultAttackStyle);
return effectiveAccuracy / (effectiveAccuracy + effectiveFortitude);
private Skill GetAttackSkill(PlayerSkills skills)
return defaultAttackStyle switch
AttackStyle.Stab or AttackStyle.Slash or AttackStyle.Crush => skills.GetSkill("Precision"),
AttackStyle.Range => skills.GetSkill("Range"),
AttackStyle.Spellweaving => skills.GetSkill("Spellweaving"),
public int RollDamage(int maxHit)
float randomFactor = Random.value;
return Mathf.Max(10, Mathf.FloorToInt(10 + (maxHit - 10) * randomFactor));
using System.Collections.Generic;
public class Woodcutting : BaseSkillAction
[Header("Woodcutting Specific")]
public ParticleSystem woodchipsEffect;
private ChoppableTree currentTree;
protected override void Start()
requiredSkillName = "Woodcutting";
requiredToolType = ToolType.Axe;
Debug.Log("[Woodcutting] Initialized woodcutting skill action.");
public override void SetTarget(ResourceNode target)
if (target is ChoppableTree tree)
Debug.Log($"[Woodcutting] Targeting tree: {tree.name}");
Debug.LogError("[Woodcutting] Invalid target type. Expected ChoppableTree.");
protected override string GetAnimationTrigger() => "isChopping";
protected virtual void PlayWoodchipsEffect()
if (woodchipsEffect != null)
Debug.Log("Playing woodchips effect.");
protected override bool ProcessAction()
if (currentTarget != null && currentTarget.IsRespawning)
Debug.Log($"[{requiredSkillName}] Cannot gather - resource is respawning");
if (currentTree == null || !currentTree.HasResources())
Debug.Log("[Woodcutting] No tree target or tree depleted!");
Tool currentAxe = inventory.GetToolOfType(requiredToolType) as Tool;
Debug.Log("[Woodcutting] No axe found in inventory!");
NotifyPlayer("You need an axe to chop wood.");
if (currentTree.Gather(currentAxe))
Debug.Log("[Woodcutting] Successfully chopped wood from tree.");
inventory.AddItem(currentTree.primaryResource, 1);
playerSkills.AddExperience(requiredSkillName, currentTree.baseXP);
Debug.Log("[Woodcutting] Failed to chop wood.");
public override void StopAction()
SetAnimationState(false);
using UnityEngine.EventSystems;
using System.Collections.Generic;
public class WorldInteractable : MonoBehaviour
public string objectName = "Object";
public string examineText;
private List<ContextMenuOption> orderedOptions = new List<ContextMenuOption>();
private Dictionary<string, (Action action, Func<bool> condition)> customOptions =
new Dictionary<string, (Action, Func<bool>)>();
InitializeDefaultOptions();
private void InitializeDefaultOptions()
if (!gameObject.GetComponent<InventoryItemUI>())
AddCustomOption("Walk here", () => {
var clickToMove = FindFirstObjectByType<ClickToMove>();
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, LayerMask.GetMask("Ground", "Terrain")))
if (UnityEngine.AI.NavMesh.SamplePosition(hit.point, out UnityEngine.AI.NavMeshHit navHit, 2f, UnityEngine.AI.NavMesh.AllAreas))
clickToMove.MoveToTarget(navHit.position);
if (gameObject.layer != LayerMask.NameToLayer("Ground"))
AddCustomOption("Examine", () => {
string displayText = !string.IsNullOrEmpty(examineText)
: $"You examine the {objectName}.";
EnhancedChatbox.Instance?.AddMessage(displayText);
public void AddCustomOption(string optionName, Action action, Func<bool> condition = null, int priority = 0)
customOptions[optionName] = (action, condition ?? (() => true));
var option = new ContextMenuOption(optionName, action);
while (insertIndex < orderedOptions.Count &&
GetOptionPriority(orderedOptions[insertIndex].Text) <= priority)
orderedOptions.Insert(insertIndex, option);
private int GetOptionPriority(string optionName)
switch (optionName.ToLower())
case var s when s.StartsWith("attack"): return 0;
case var s when s.StartsWith("pick up"): return 100;
case var s when s.StartsWith("chop"): return 200;
case var s when s.StartsWith("mine"): return 200;
case var s when s.StartsWith("equip"): return 300;
case var s when s.StartsWith("drink"): return 300;
case var s when s.StartsWith("eat"): return 300;
case var s when s.StartsWith("use"): return 400;
case "walk here": return 900;
case "examine": return 950;
case "cancel": return 1000;
if (Input.GetMouseButtonDown(0))
if (!EventSystem.current.IsPointerOverGameObject())
else if (Input.GetMouseButtonDown(1))
if (!EventSystem.current.IsPointerOverGameObject() ||
(ContextMenu.Instance != null && ContextMenu.Instance.IsClickingMenuItem()))
private void ExecutePrimaryAction()
foreach (var option in orderedOptions)
if (customOptions.TryGetValue(option.Text, out var optionData))
if (optionData.condition())
optionData.action?.Invoke();
private List<ContextMenuOption> GetAvailableOptions()
List<ContextMenuOption> availableOptions = new List<ContextMenuOption>();
foreach (var option in orderedOptions)
if (customOptions.TryGetValue(option.Text, out var optionData))
if (optionData.condition())
availableOptions.Add(option);
availableOptions.Add(new ContextMenuOption("Cancel", () => {
if (ContextMenu.Instance != null)
ContextMenu.Instance.HideMenu();
private void ShowContextMenu()
if (ContextMenu.Instance == null)
Debug.LogError("ContextMenu.Instance is null! Ensure ContextMenu prefab is in the scene.");
List<ContextMenuOption> availableOptions = GetAvailableOptions();
if (availableOptions.Count > 0)
ContextMenu.Instance.ShowMenu(Input.mousePosition, availableOptions);
using System.Collections;
public abstract class BaseSkillAction : MonoBehaviour
[Header("Basic Settings")]
public float actionDistance = 2.0f;
public float baseActionInterval = 3f;
public float animationDuration = 0.8f;
public ToolType requiredToolType;
public int minimumLevel = 1;
public string requiredSkillName;
public AudioClip[] actionSounds;
public bool IsPerformingAction => isPerformingAction;
private float lastSoundTime = 0f;
private float soundCooldown = 1f;
private SkillsAudioManager skillsAudioManager;
protected NavMeshAgent agent;
protected Animator animator;
protected Inventory inventory;
protected PlayerSkills playerSkills;
protected ResourceNode currentTarget;
protected bool isPerformingAction = false;
protected bool isWalkingToTarget = false;
private bool shouldStopAction = false;
private Coroutine actionCoroutine;
protected virtual void Start()
agent = GetComponent<NavMeshAgent>();
animator = GetComponent<Animator>();
inventory = Inventory.Instance;
playerSkills = GetComponent<PlayerSkills>();
skillsAudioManager = FindFirstObjectByType<SkillsAudioManager>();
protected virtual void ValidateComponents()
if (agent == null) Debug.LogError($"NavMeshAgent missing on {gameObject.name}");
if (animator == null) Debug.LogError($"Animator missing on {gameObject.name}");
if (inventory == null) Debug.LogError("Inventory not found");
if (playerSkills == null) Debug.LogError("PlayerSkills not found");
protected virtual void Update()
if (isWalkingToTarget && currentTarget != null)
public virtual void SetTarget(ResourceNode target)
if (isPerformingAction && currentTarget == target)
Debug.Log($"[BaseSkillAction] Already gathering from {target?.name}, skipping reset.");
Debug.Log($"[BaseSkillAction] Setting new target: {target?.name}");
Debug.Log($"[BaseSkillAction] Stopping current action before setting new target");
UnsubscribeFromResourceEvents();
if (currentTarget != null)
Debug.Log($"Setting target and walking to {target.name}");
agent.stoppingDistance = actionDistance;
agent.SetDestination(target.transform.position);
isWalkingToTarget = true;
SubscribeToResourceEvents();
protected virtual void CheckArrivalAtTarget()
if (currentTarget == null || agent.pathPending) return;
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.transform.position);
if (distanceToTarget <= actionDistance && !isPerformingAction)
Debug.Log("Arrived at target, starting action");
isWalkingToTarget = false;
protected virtual void FaceTarget()
if (currentTarget == null) return;
Vector3 directionToTarget = (currentTarget.transform.position - transform.position).normalized;
Quaternion targetRotation = Quaternion.LookRotation(new Vector3(directionToTarget.x, 0, directionToTarget.z));
transform.rotation = targetRotation;
protected virtual void StartAction()
if (!CanPerformAction() || isPerformingAction)
Debug.Log("Starting gathering action");
isPerformingAction = true;
if (actionCoroutine != null)
StopCoroutine(actionCoroutine);
actionCoroutine = StartCoroutine(ActionRoutine());
protected virtual bool CanPerformAction()
if (currentTarget != null && (!currentTarget.IsValidForGathering() || currentTarget.IsRespawning))
Debug.Log($"[{requiredSkillName}] Cannot gather - resource is respawning or depleted");
NotifyPlayer($"You need a {requiredToolType} to perform this action.");
Skill skill = playerSkills.GetSkill(requiredSkillName);
Debug.LogError($"Skill {requiredSkillName} not found!");
if (minimumLevel > 1 && skill.CurrentLevel < minimumLevel)
NotifyPlayer($"You need level {minimumLevel} {requiredSkillName} to perform this action.");
protected virtual bool HasRequiredTool()
if (requiredToolType == ToolType.None) return true;
Tool tool = inventory.GetToolOfType(requiredToolType);
Debug.Log($"Found required tool: {tool.itemName}");
Debug.Log($"Required tool of type {requiredToolType} not found in inventory.");
protected virtual IEnumerator ActionRoutine()
Debug.Log($"[{requiredSkillName}] Starting action routine");
float nextActionTime = 0f;
shouldStopAction = false;
while (!shouldStopAction && isPerformingAction && CanPerformAction())
if (Time.time >= nextActionTime)
if (currentTarget == null || currentTarget.IsRespawning)
Debug.Log($"[{requiredSkillName}] Target is null or respawning, stopping action");
yield return new WaitForSeconds(animationDuration);
SetAnimationState(false);
nextActionTime = Time.time + baseActionInterval;
float remainingInterval = baseActionInterval - animationDuration;
if (remainingInterval > 0)
yield return new WaitForSeconds(remainingInterval);
Debug.Log($"[{requiredSkillName}] Exiting action routine");
protected virtual void PlayActionSound()
if (skillsAudioManager != null && actionSounds != null && actionSounds.Length > 0)
if (Time.time - lastSoundTime >= soundCooldown)
AudioClip randomSound = actionSounds[Random.Range(0, actionSounds.Length)];
skillsAudioManager.PlaySkillSound(randomSound);
lastSoundTime = Time.time;
protected void UnsubscribeFromResourceEvents()
if (currentTarget != null)
currentTarget.OnRespawnStarted -= HandleResourceRespawnStarted;
currentTarget.OnRespawnCompleted -= HandleResourceRespawnCompleted;
protected virtual void HandleResourceRespawnStarted(ResourceNode resource)
if (resource == currentTarget)
Debug.Log($"[{requiredSkillName}] Resource entered respawn state, stopping action");
protected virtual void HandleResourceRespawnCompleted(ResourceNode resource)
Debug.Log($"[{requiredSkillName}] Resource has respawned");
protected virtual void SubscribeToResourceEvents()
if (currentTarget != null)
currentTarget.OnRespawnStarted += HandleResourceRespawnStarted;
currentTarget.OnRespawnCompleted += HandleResourceRespawnCompleted;
public virtual void StopAction()
if (!isPerformingAction) return;
Debug.Log($"[{requiredSkillName}] Stopping action");
isPerformingAction = false;
SetAnimationState(false);
if (actionCoroutine != null)
StopCoroutine(actionCoroutine);
UnsubscribeFromResourceEvents();
protected abstract bool ProcessAction();
protected virtual void NotifyPlayer(string message)
EnhancedChatbox.Instance?.AddMessage(message);
protected virtual void SetAnimationState(bool active)
string trigger = GetAnimationTrigger();
if (!string.IsNullOrEmpty(trigger))
animator.SetBool(trigger, active);
protected abstract string GetAnimationTrigger();
using System.Collections;
[RequireComponent(typeof(NavMeshAgent))]
public class BasicNPC : MonoBehaviour, ICombatant
[Header("NPC Base Stats")]
public string npcName = "Training Dummy";
public int baseVitality = 100;
private CombatStats stats;
[Header("Combat Configuration")]
public float attackRange = 2f;
public float attackSpeed = 2.4f;
public AttackStyle defaultAttackStyle = AttackStyle.Slash;
public float baseXPValue = 40f;
[Header("Combat Bonuses")]
public CombatBonuses bonuses = new CombatBonuses();
[Header("Behavior Settings")]
public float aggroRange = 5f;
public bool isAggressive = false;
private bool isAlive = true;
private bool isInCombat = false;
private NavMeshAgent agent;
private ICombatant currentTarget;
private float nextAttackTime;
private Animator animator;
public CombatStats Stats => stats;
public bool IsAlive => isAlive;
public new Transform transform => base.transform;
public string GetDisplayName() => npcName;
StartCoroutine(BehaviorRoutine());
private void InitializeComponents()
agent = GetComponent<NavMeshAgent>();
animator = GetComponent<Animator>();
Debug.LogError($"NavMeshAgent missing on {npcName}!");
private void InitializeStats()
int scaledVitality = baseVitality + (level * 4);
int scaledPrecision = level;
int scaledFortitude = level;
int scaledSpellweaving = level;
int scaledDivinity = level;
vitality: scaledVitality,
precision: scaledPrecision,
fortitude: scaledFortitude,
spellweaving: scaledSpellweaving,
stats.combatBonuses = bonuses;
Debug.Log($"Initialized {npcName} (Level {level}) - " +
$"Vitality: {stats.Vitality}, " +
$"Combat Style: {defaultAttackStyle}");
private void SetupInteraction()
var interactable = gameObject.AddComponent<WorldInteractable>();
interactable.objectName = npcName;
interactable.AddCustomOption("Attack", () => {
var player = GameObject.FindGameObjectWithTag("Player");
var playerCombat = player.GetComponent<CombatHandler>();
if (playerCombat != null)
playerCombat.StartCombat(this);
private IEnumerator BehaviorRoutine()
if (isAggressive && !isInCombat)
yield return new WaitForSeconds(0.5f);
private void CheckForTargets()
var player = GameObject.FindGameObjectWithTag("Player");
float distance = Vector3.Distance(transform.position, player.transform.position);
if (distance <= aggroRange)
var playerCombat = player.GetComponent<ICombatant>();
if (playerCombat != null && playerCombat.IsAlive)
StartCombat(playerCombat);
private void UpdateCombat()
if (currentTarget == null || !currentTarget.IsAlive)
float distance = Vector3.Distance(transform.position, currentTarget.transform.position);
if (distance > attackRange)
agent.SetDestination(currentTarget.transform.position);
if (Time.time >= nextAttackTime)
public void Attack(ICombatant target)
if (!IsAlive || target == null || !target.IsAlive) return;
var attackStyle = new WeaponAttackStyle
styleName = $"{npcName} Attack",
attackStyle = defaultAttackStyle,
trainingStyle = CombatTrainingStyle.Precision,
float accuracy = CombatCalculator.CalculateAccuracy(this, target, attackStyle);
if (Random.value < accuracy)
int damage = CalculateDamage();
target.TakeDamage(damage, defaultAttackStyle);
Debug.Log($"{npcName} hit {target.GetDisplayName()} for {damage} damage!");
if (target is MonoBehaviour targetObj)
var combatHandler = targetObj.GetComponent<CombatHandler>();
combatHandler?.PlayHitEffect(targetObj.transform.position);
combatHandler?.PlayCombatSound(true);
Debug.Log($"{npcName} missed their attack against {target.GetDisplayName()}!");
if (target is MonoBehaviour targetObj)
var combatHandler = targetObj.GetComponent<CombatHandler>();
combatHandler?.PlayCombatSound(false);
nextAttackTime = Time.time + attackSpeed;
animator.SetTrigger("Attack");
private int CalculateDamage()
int maxHit = stats.CalculateMaxHit(defaultAttackStyle);
return Random.Range(1, maxHit + 1);
public void TakeDamage(int damage, AttackStyle style)
stats.ModifyVitality(-damage);
Debug.Log($"{npcName} took {damage} damage! Vitality: {stats.Vitality}/{stats.MaxVitality}");
public AttackStyle GetCurrentCombatStyle()
return defaultAttackStyle;
public float GetAttackSpeed()
private void StartCombat(ICombatant target)
animator.SetBool("InCombat", true);
animator.SetBool("InCombat", false);
animator.SetTrigger("Death");
var player = GameObject.FindGameObjectWithTag("Player");
var playerSkills = player.GetComponent<PlayerSkills>();
if (playerSkills != null)
float scaledXP = baseXPValue * (1 + level * 0.1f);
playerSkills.AddExperience("Precision", scaledXP);
EnhancedChatbox.Instance?.AddMessage($"Defeated {npcName}! Gained {scaledXP:F1} Combat experience!");
var interactable = GetComponent<WorldInteractable>();
if (interactable != null)
StartCoroutine(DeathSequence());
private IEnumerator DeathSequence()
yield return new WaitForSeconds(2f);
private void OnDrawGizmosSelected()
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackRange);
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, aggroRange);
using System.Collections;
public class ChoppableTree : ResourceNode
[Header("Woodcutting Specific")]
public TreeType treeType;
public int birdNestChance = 5;
public ParticleSystem woodchipsParticles;
public GameObject treeStump;
public GameObject fallingTreeEffect;
private Material originalMaterial;
protected override void Initialize()
if (resourceRenderer != null)
originalMaterial = resourceRenderer.material;
requiredToolType = ToolType.Axe;
public override bool Gather(Tool tool)
if (!base.Gather(tool)) return false;
if (woodchipsParticles != null)
woodchipsParticles.Play();
private void CheckForBirdNest(Tool tool)
float bonusChance = birdNestChance;
bonusChance *= tool.GetEfficiencyMultiplier(1);
if (Random.value < bonusChance / 100f)
Debug.Log("Found a bird's nest!");
protected override IEnumerator FadeOut()
if (fallingTreeEffect != null)
Instantiate(fallingTreeEffect, transform.position, transform.rotation);
GameObject stump = Instantiate(treeStump, transform.position, transform.rotation);
Destroy(stump, respawnTime);
while (elapsed < duration)
elapsed += Time.deltaTime;
if (resourceRenderer != null && resourceRenderer.material != null)
Color newColor = resourceRenderer.material.color;
newColor.a = 1 - (elapsed / duration);
resourceRenderer.material.color = newColor;
resourceRenderer.enabled = false;
resourceCollider.enabled = false;
protected override IEnumerator FadeIn()
resourceRenderer.enabled = true;
resourceCollider.enabled = true;
while (elapsed < duration)
elapsed += Time.deltaTime;
if (resourceRenderer != null && resourceRenderer.material != null)
Color newColor = resourceRenderer.material.color;
newColor.a = elapsed / duration;
resourceRenderer.material.color = newColor;
if (resourceRenderer != null && originalMaterial != null)
resourceRenderer.material = originalMaterial;