Files
2026-06-07 16:58:24 +01:00

293 lines
8.7 KiB
C#

using System;
using System.Collections;
using System.IO;
using UnityEngine;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
// Sends Events:
// - ClientSimPlayerDeathStatusChangedEvent
[AddComponentMenu("")]
public class ClientSimCombatSystemHelper : ClientSimBehaviour, IVRC_Destructible
{
private static readonly string _visualDamagePrefabPath =
Path.Combine("Assets", "VRChat Examples", "Prefabs", "VRCPlayerVisualDamage.prefab");
private VRCPlayerApi _player;
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimProxyObjectProvider _proxyProvider;
private ClientSimPlayerController _playerController;
// Make values public so users can see and modify these values at runtime.
public bool respawnOnDeath;
public bool resetHealthOnRespawn = true;
public float respawnTime = 5f;
public float maxPlayerHealth = 100;
public float currentHealth = 100;
public Transform respawnPoint;
private GameObject _visualDamagePrefab;
private GameObject _visualDamageObj;
private VRC_VisualDamage _visualDamage;
private bool _dead = false;
private static ClientSimCombatSystemHelper GetCombatHelper(VRCPlayerApi player)
{
return player.GetClientSimPlayer().GetCombatHelper();
}
public static void CombatSetup(VRCPlayerApi player)
{
player.GetClientSimPlayer().InitializeCombat();
}
public static void CombatSetMaxHitpoints(VRCPlayerApi player, float maxHealth)
{
ClientSimCombatSystemHelper combatHelper = GetCombatHelper(player);
if (combatHelper == null)
{
return;
}
combatHelper.maxPlayerHealth = maxHealth;
}
public static float CombatGetCurrentHitpoints(VRCPlayerApi player)
{
ClientSimCombatSystemHelper combatHelper = GetCombatHelper(player);
if (combatHelper == null)
{
// If a player doesn't have combat setup, their hitpoints are -1.
return -1;
}
return combatHelper.currentHealth;
}
public static void CombatSetRespawn(VRCPlayerApi player, bool respawnOnDeath, float respawnTime, Transform spawnPoint)
{
ClientSimCombatSystemHelper combatHelper = GetCombatHelper(player);
if (combatHelper == null)
{
return;
}
combatHelper.respawnOnDeath = respawnOnDeath;
combatHelper.respawnTime = respawnTime;
combatHelper.respawnPoint = spawnPoint;
}
public static void CombatSetDamageGraphic(VRCPlayerApi player, GameObject visualDamage)
{
ClientSimCombatSystemHelper combatHelper = GetCombatHelper(player);
if (combatHelper == null)
{
return;
}
combatHelper._visualDamagePrefab = visualDamage;
}
public static IVRC_Destructible CombatGetDestructible(VRCPlayerApi player)
{
return GetCombatHelper(player);
}
public static void CombatSetCurrentHitpoints(VRCPlayerApi player, float health)
{
ClientSimCombatSystemHelper combatHelper = GetCombatHelper(player);
if (combatHelper == null)
{
return;
}
if (player.isLocal)
{
float delta = health - combatHelper.currentHealth;
if (delta <= 0)
{
combatHelper.ApplyDamage(-delta);
}
else
{
combatHelper.ApplyHealing(delta);
}
}
combatHelper.currentHealth = health;
}
public void Initialize(
VRCPlayerApi player,
IClientSimEventDispatcher eventDispatcher,
IClientSimProxyObjectProvider proxyProvider,
ClientSimPlayerController playerController)
{
_player = player;
// Values below will be null for remote players.
_eventDispatcher = eventDispatcher;
_playerController = playerController;
_proxyProvider = proxyProvider;
// TODO add ragdoll to player avatar
}
private void Start()
{
currentHealth = GetMaxHealth();
CreateVisualDamage();
}
private void CreateVisualDamage()
{
if (!_player.isLocal)
{
return;
}
if (_visualDamageObj != null)
{
Destroy(_visualDamageObj);
}
GameObject damage = _visualDamagePrefab;
#if UNITY_EDITOR
// If damage prefab is null, try loading it from the sample prefabs
if (damage == null)
{
damage = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(_visualDamagePrefabPath);
}
#endif
if (damage != null)
{
_visualDamageObj = Instantiate(damage, _proxyProvider.CameraProxy().transform);
_visualDamageObj.transform.localScale = new Vector3(40, 40, 40);
_visualDamageObj.transform.localPosition = new Vector3(0, 0, 0.5f);
_visualDamage = _visualDamageObj.GetComponent<VRC_VisualDamage>();
if (_visualDamage != null)
{
// VRChatBug: Visual Damage script is blacklisted in SDK3 and will be destroyed after spawning.
DestroyImmediate(_visualDamage);
}
}
}
public float GetMaxHealth()
{
return maxPlayerHealth;
}
public float GetCurrentHealth()
{
return currentHealth;
}
private void ApplyVisualDamage()
{
if (_visualDamage != null)
{
try
{
_visualDamage.SetDamagePercent(1 - (currentHealth / maxPlayerHealth));
}
catch (Exception e)
{
this.LogWarning($"Error applying damage: {e}");
}
}
}
public void ApplyDamage(float damage)
{
if (!_player.isLocal)
{
return;
}
if (currentHealth <= 0)
{
return;
}
this.Log($"ApplyDamage: {damage} currentHealth: {currentHealth}");
currentHealth = Mathf.Clamp(currentHealth - damage, 0, maxPlayerHealth);
ApplyVisualDamage();
if (currentHealth <= 0 && !_dead)
{
_dead = true;
this.Log("Player Died");
_player.EnablePickups(false);
_eventDispatcher?.SendEvent(new ClientSimPlayerDeathStatusChangedEvent
{
player = _player,
isDead = true,
});
StartCoroutine(PlayerDied());
}
}
private IEnumerator PlayerDied()
{
yield return new WaitForSeconds(respawnTime);
RevivePlayer();
}
private void RevivePlayer()
{
if (!_player.isLocal)
{
return;
}
_dead = false;
this.Log("Player Revived");
_player.EnablePickups(true);
if (respawnPoint != null && respawnOnDeath)
{
_playerController.Teleport(respawnPoint, false);
}
if (resetHealthOnRespawn)
{
ApplyHealing(maxPlayerHealth);
}
_eventDispatcher?.SendEvent(new ClientSimPlayerDeathStatusChangedEvent
{
player = _player,
isDead = false,
});
}
public void ApplyHealing(float healing)
{
if (!_player.isLocal)
{
return;
}
currentHealth = Mathf.Clamp(currentHealth + healing, 0, maxPlayerHealth);
ApplyVisualDamage();
this.Log($"ApplyHealing: {healing} currentHealth: {currentHealth}");
}
public object[] GetState()
{
throw new System.NotImplementedException();
}
public void SetState(object[] state)
{
throw new System.NotImplementedException();
}
}
}