Implement health modification on enemy kill; add HealOnKillModifier and update death behavior

This commit is contained in:
2025-07-12 12:57:21 +02:00
parent cb793bcc93
commit c3b1ac9213
22 changed files with 330 additions and 11 deletions

View File

@@ -0,0 +1,74 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c437a5565f39495281bf1dd14dd6ba40, type: 3}
m_Name: Vampire's draught
m_EditorClassIdentifier:
serializationData:
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes:
- Name: cures
Entry: 7
Data: 0|System.Collections.Generic.List`1[[Interfaces.IStatModifier, Assembly-CSharp]],
mscorlib
- Name:
Entry: 12
Data: 1
- Name:
Entry: 7
Data: 1|Modifiers.HealOnKillModifier, Assembly-CSharp
- Name: value
Entry: 4
Data: 10
- Name:
Entry: 8
Data:
- Name:
Entry: 13
Data:
- Name:
Entry: 8
Data:
- Name: curses
Entry: 7
Data: 2|System.Collections.Generic.List`1[[Interfaces.IStatModifier, Assembly-CSharp]],
mscorlib
- Name:
Entry: 12
Data: 1
- Name:
Entry: 7
Data: 3|Modifiers.PercentStatModifier, Assembly-CSharp
- Name: stat
Entry: 3
Data: 1
- Name: percent
Entry: 4
Data: -0.1
- Name:
Entry: 8
Data:
- Name:
Entry: 13
Data:
- Name:
Entry: 8
Data:
itemName: "Vampire\u2019s Draught"
description: 'Cure: +10 Health on Kill, Curse: MaxHealth -10%'
icon: {fileID: 0}
price: 0

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5366536d04e61959f911fa119df9eca4
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -410,6 +410,7 @@ MonoBehaviour:
character: {fileID: 7110610235440076017} character: {fileID: 7110610235440076017}
deathBehavior: deathBehavior:
_implementer: {fileID: 4476814410094481518} _implementer: {fileID: 4476814410094481518}
health: {fileID: 5572087310217837258}
--- !u!114 &4476814410094481518 --- !u!114 &4476814410094481518
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@@ -107,7 +107,7 @@ BoxCollider2D:
m_CallbackLayers: m_CallbackLayers:
serializedVersion: 2 serializedVersion: 2
m_Bits: 4294967295 m_Bits: 4294967295
m_IsTrigger: 0 m_IsTrigger: 1
m_UsedByEffector: 0 m_UsedByEffector: 0
m_CompositeOperation: 0 m_CompositeOperation: 0
m_CompositeOrder: 0 m_CompositeOrder: 0

View File

@@ -6313,6 +6313,7 @@ GameObject:
- component: {fileID: 1261447611} - component: {fileID: 1261447611}
- component: {fileID: 1261447610} - component: {fileID: 1261447610}
- component: {fileID: 1261447609} - component: {fileID: 1261447609}
- component: {fileID: 1261447613}
m_Layer: 6 m_Layer: 6
m_Name: Player m_Name: Player
m_TagString: Player m_TagString: Player
@@ -6418,7 +6419,7 @@ MonoBehaviour:
level: 1 level: 1
experience: 0 experience: 0
baseExperienceToLevelUp: 100 baseExperienceToLevelUp: 100
damage: 1 damage: 49
rangedDamage: 1 rangedDamage: 1
meleeDamage: 1 meleeDamage: 1
attackRange: 16 attackRange: 16
@@ -6479,6 +6480,7 @@ MonoBehaviour:
character: {fileID: 1261447603} character: {fileID: 1261447603}
deathBehavior: deathBehavior:
_implementer: {fileID: 1261447606} _implementer: {fileID: 1261447606}
health: {fileID: 1261447608}
--- !u!114 &1261447608 --- !u!114 &1261447608
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -6550,6 +6552,21 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
character: {fileID: 1261447603} character: {fileID: 1261447603}
--- !u!114 &1261447613
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1261447599}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f973da80e1dd49c8a9efdc270992b705, type: 3}
m_Name:
m_EditorClassIdentifier:
mainCamera: {fileID: 519420031}
weapons: []
weaponsManager: {fileID: 1261447612}
--- !u!1 &1300312975 --- !u!1 &1300312975
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -6951,7 +6968,7 @@ PrefabInstance:
objectReference: {fileID: 1261447609} objectReference: {fileID: 1261447609}
- target: {fileID: 1501730282976965803, guid: 4d1c30f952fa59fb9b0cc1a987f40ad8, type: 3} - target: {fileID: 1501730282976965803, guid: 4d1c30f952fa59fb9b0cc1a987f40ad8, type: 3}
propertyPath: possibleItems.Array.size propertyPath: possibleItems.Array.size
value: 1 value: 2
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1501730282976965803, guid: 4d1c30f952fa59fb9b0cc1a987f40ad8, type: 3} - target: {fileID: 1501730282976965803, guid: 4d1c30f952fa59fb9b0cc1a987f40ad8, type: 3}
propertyPath: possibleWeapons.Array.size propertyPath: possibleWeapons.Array.size
@@ -6961,6 +6978,10 @@ PrefabInstance:
propertyPath: 'possibleItems.Array.data[0]' propertyPath: 'possibleItems.Array.data[0]'
value: value:
objectReference: {fileID: 11400000, guid: 08c8a414828d19951b3a413f631a6d70, type: 2} objectReference: {fileID: 11400000, guid: 08c8a414828d19951b3a413f631a6d70, type: 2}
- target: {fileID: 1501730282976965803, guid: 4d1c30f952fa59fb9b0cc1a987f40ad8, type: 3}
propertyPath: 'possibleItems.Array.data[1]'
value:
objectReference: {fileID: 11400000, guid: 5366536d04e61959f911fa119df9eca4, type: 2}
- target: {fileID: 1501730282976965803, guid: 4d1c30f952fa59fb9b0cc1a987f40ad8, type: 3} - target: {fileID: 1501730282976965803, guid: 4d1c30f952fa59fb9b0cc1a987f40ad8, type: 3}
propertyPath: 'possibleWeapons.Array.data[0]' propertyPath: 'possibleWeapons.Array.data[0]'
value: value:

View File

@@ -1,7 +1,10 @@
using JetBrains.Annotations;
using UnityEngine;
namespace Interfaces namespace Interfaces
{ {
public interface IDeathBehavior public interface IDeathBehavior
{ {
void Die(); void Die([CanBeNull] GameObject killer = null);
} }
} }

View File

@@ -0,0 +1,9 @@
using UnityEngine;
namespace Interfaces
{
public interface IOnKillEffect
{
void OnKill(GameObject killer, GameObject victim);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3237e2bd7db84674bd34467854e53928
timeCreated: 1752314256

View File

@@ -0,0 +1,25 @@
using System;
using Data;
using Interfaces;
using Systems;
using UnityEngine;
namespace Modifiers
{
[Serializable]
public class HealOnKillModifier : IStatModifier, IOnKillEffect
{
public float value;
public string Description => $"+{value} Health on Kill";
public void Apply(CharacterAttributes attributes) { }
public void Remove(CharacterAttributes attributes) { }
public void OnKill(GameObject killer, GameObject victim)
{
killer.TryGetComponent(out Character character);
character?.attributes.ModifyHealth(value);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b329a84895234a3290156c5afe257212
timeCreated: 1752314314

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Data; using Data;
using Interfaces; using Interfaces;
@@ -10,20 +11,50 @@ namespace Systems
public class CharacterModifierManager : MonoBehaviour public class CharacterModifierManager : MonoBehaviour
{ {
[OdinSerialize] private List<IStatModifier> activeModifiers = new(); [OdinSerialize] private List<IStatModifier> activeModifiers = new();
[OdinSerialize] private List<IOnKillEffect> onKillEffects = new();
[SerializeField, Self] private Character character; [SerializeField, Self] private Character character;
private void OnEnable()
{
EnemyDeathBehavior.OnAnyEnemyKilled += HandleEnemyKilled;
}
private void OnDisable()
{
EnemyDeathBehavior.OnAnyEnemyKilled -= HandleEnemyKilled;
}
public void EquipItem(IStatModifier modifier) public void EquipItem(IStatModifier modifier)
{ {
activeModifiers.Add(modifier); activeModifiers.Add(modifier);
modifier.Apply(character.attributes); modifier.Apply(character.attributes);
if (modifier is IOnKillEffect onKillEffect)
{
onKillEffects.Add(onKillEffect);
}
} }
public void UnequipItem(IStatModifier modifier) public void UnequipItem(IStatModifier modifier)
{ {
if (activeModifiers.Remove(modifier)) if (!activeModifiers.Remove(modifier)) return;
{
modifier.Remove(character.attributes); modifier.Remove(character.attributes);
if (modifier is IOnKillEffect onKillEffect)
{
onKillEffects.Remove(onKillEffect);
}
}
private void HandleEnemyKilled(GameObject killer, GameObject victim)
{
if (killer != gameObject) return;
foreach (var effect in onKillEffects)
{
effect.OnKill(killer, victim);
} }
} }
} }

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Data; using Data;
using Inventory;
using KBCore.Refs; using KBCore.Refs;
using Sirenix.Serialization; using Sirenix.Serialization;
using UnityEngine; using UnityEngine;
@@ -14,6 +16,9 @@ namespace Systems
[SerializeField] private IReadOnlyList<Weapon> EquippedWeapons => equippedWeapons.AsReadOnly(); [SerializeField] private IReadOnlyList<Weapon> EquippedWeapons => equippedWeapons.AsReadOnly();
[SerializeField, Self] private Character character; [SerializeField, Self] private Character character;
public event Action<Weapon> WeaponEquipped;
public event Action<Weapon> WeaponUnequipped;
public Weapon EquipWeapon(GameObject weaponPrefab) public Weapon EquipWeapon(GameObject weaponPrefab)
{ {
var weaponObject = Instantiate(weaponPrefab, transform); var weaponObject = Instantiate(weaponPrefab, transform);
@@ -22,6 +27,7 @@ namespace Systems
weapon.character = character; weapon.character = character;
equippedWeapons.Add(weapon); equippedWeapons.Add(weapon);
weapon.enabled = true; weapon.enabled = true;
WeaponEquipped?.Invoke(weapon);
return weapon; return weapon;
} }
@@ -33,6 +39,7 @@ namespace Systems
{ {
if (!equippedWeapons.Remove(weapon)) return; if (!equippedWeapons.Remove(weapon)) return;
weapon.enabled = false; weapon.enabled = false;
WeaponUnequipped?.Invoke(weapon);
if (!destroy) return; if (!destroy) return;
Destroy(weapon.gameObject); Destroy(weapon.gameObject);
@@ -43,6 +50,7 @@ namespace Systems
foreach (var weapon in equippedWeapons) foreach (var weapon in equippedWeapons)
{ {
weapon.enabled = false; weapon.enabled = false;
WeaponUnequipped?.Invoke(weapon);
} }
} }
@@ -51,6 +59,7 @@ namespace Systems
foreach (var weapon in equippedWeapons) foreach (var weapon in equippedWeapons)
{ {
weapon.enabled = true; weapon.enabled = true;
WeaponEquipped?.Invoke(weapon);
} }
} }
@@ -58,6 +67,7 @@ namespace Systems
{ {
foreach (var weapon in equippedWeapons) foreach (var weapon in equippedWeapons)
{ {
WeaponUnequipped?.Invoke(weapon);
Destroy(weapon.gameObject); Destroy(weapon.gameObject);
} }
equippedWeapons.Clear(); equippedWeapons.Clear();

View File

@@ -9,6 +9,7 @@ namespace Systems
{ {
[Self, SerializeField] private Character character; [Self, SerializeField] private Character character;
[Self, SerializeField] private InterfaceRef<IDeathBehavior> deathBehavior; [Self, SerializeField] private InterfaceRef<IDeathBehavior> deathBehavior;
[Self, SerializeField] private Health health;
private void OnEnable() private void OnEnable()
{ {
@@ -30,7 +31,8 @@ namespace Systems
private void Die() private void Die()
{ {
deathBehavior.Value.Die(); var lastAttacker = health.LastAttacker;
deathBehavior.Value.Die(lastAttacker);
} }
} }
} }

View File

@@ -1,3 +1,4 @@
using System;
using Interfaces; using Interfaces;
using Sirenix.Serialization; using Sirenix.Serialization;
using UnityEngine; using UnityEngine;
@@ -6,13 +7,18 @@ namespace Systems
{ {
public class EnemyDeathBehavior : MonoBehaviour, IDeathBehavior public class EnemyDeathBehavior : MonoBehaviour, IDeathBehavior
{ {
public static event Action<GameObject, GameObject> OnAnyEnemyKilled;
[OdinSerialize, SerializeField] private int expReward = 5; [OdinSerialize, SerializeField] private int expReward = 5;
[OdinSerialize, SerializeField] private int coinReward = 1; [OdinSerialize, SerializeField] private int coinReward = 1;
public void Die() public void Die(GameObject killer = null)
{ {
GameManager.Instance.Player.attributes.ModifyExperience(expReward); GameManager.Instance.Player.attributes.ModifyExperience(expReward);
GameManager.Instance.AddCoins(coinReward); GameManager.Instance.AddCoins(coinReward);
OnAnyEnemyKilled?.Invoke(killer ?? GameManager.Instance.Player.gameObject, gameObject);
Destroy(gameObject); Destroy(gameObject);
// later let's add particle effects, sound effects, etc. // later let's add particle effects, sound effects, etc.

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
using UnityEngine;
using Weapons;
namespace Systems
{
public class EnemyWeaponTargetSetter : MonoBehaviour
{
[SerializeField] private List<AutoWeapon> weapons = new();
[SerializeField] private Transform target;
private void Reset()
{
if (weapons.Count == 0)
{
weapons = new List<AutoWeapon>(GetComponentsInChildren<AutoWeapon>());
}
}
private void Update()
{
if (!target || weapons.Count == 0) return;
foreach (var weapon in weapons)
{
weapon.Target = target.position;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5523bce9a0d94d6fafb3fc09e3bf9cc6
timeCreated: 1752316561

View File

@@ -7,16 +7,21 @@ namespace Systems
{ {
public class Health : MonoBehaviour public class Health : MonoBehaviour
{ {
private GameObject lastAttacker;
[Self, SerializeField] private Character character; [Self, SerializeField] private Character character;
[SerializeField] private float initialHealth = 100f; [SerializeField] private float initialHealth = 100f;
public GameObject LastAttacker => lastAttacker;
private void Start() private void Start()
{ {
character.attributes.SetHealth(initialHealth); character.attributes.SetHealth(initialHealth);
} }
public void TakeDamage(float damage) public void TakeDamage(float damage, GameObject attacker = null)
{ {
lastAttacker = attacker;
var effectiveDamage = Math.Max(damage - character.attributes.Armor, 1); var effectiveDamage = Math.Max(damage - character.attributes.Armor, 1);
character.attributes.ModifyHealth(-effectiveDamage); character.attributes.ModifyHealth(-effectiveDamage);
} }

View File

@@ -13,6 +13,9 @@ namespace Systems
[SerializeField, Self] private CharacterModifierManager characterModifierManager; [SerializeField, Self] private CharacterModifierManager characterModifierManager;
[SerializeField, Self] private CharacterWeaponsManager characterWeaponsManager; [SerializeField, Self] private CharacterWeaponsManager characterWeaponsManager;
public event Action<StatModifierItem> ItemEquipped;
public event Action<StatModifierItem> ItemUnequipped;
private void Start() private void Start()
{ {
if (inventory == null) if (inventory == null)
@@ -42,6 +45,7 @@ namespace Systems
inventory.AddItem(item); inventory.AddItem(item);
foreach (var cure in item.cures) characterModifierManager.EquipItem(cure); foreach (var cure in item.cures) characterModifierManager.EquipItem(cure);
foreach (var curse in item.curses) characterModifierManager.EquipItem(curse); foreach (var curse in item.curses) characterModifierManager.EquipItem(curse);
ItemEquipped?.Invoke(item);
} }
public void UnequipItem(StatModifierItem item) public void UnequipItem(StatModifierItem item)
@@ -51,6 +55,7 @@ namespace Systems
foreach (var cure in item.cures) characterModifierManager.UnequipItem(cure); foreach (var cure in item.cures) characterModifierManager.UnequipItem(cure);
foreach (var curse in item.curses) characterModifierManager.UnequipItem(curse); foreach (var curse in item.curses) characterModifierManager.UnequipItem(curse);
ItemUnequipped?.Invoke(item);
} }
public void EquipWeapon(WeaponItem weaponItem) public void EquipWeapon(WeaponItem weaponItem)

View File

@@ -6,7 +6,7 @@ namespace Systems
{ {
public class PlayerDeathBehavior : MonoBehaviour, IDeathBehavior public class PlayerDeathBehavior : MonoBehaviour, IDeathBehavior
{ {
public void Die() public void Die(GameObject killer = null)
{ {
SceneManager.LoadScene(SceneManager.GetActiveScene().name); SceneManager.LoadScene(SceneManager.GetActiveScene().name);
} }

View File

@@ -0,0 +1,71 @@
using System.Collections.Generic;
using Inventory;
using KBCore.Refs;
using UnityEngine;
using UnityEngine.InputSystem;
using Weapons;
namespace Systems
{
public class PlayerWeaponTargetSetter : MonoBehaviour
{
[SerializeField, Scene] private Camera mainCamera;
[SerializeField] private List<AutoWeapon> weapons = new();
[Self, SerializeField] private CharacterWeaponsManager weaponsManager;
private void OnEnable()
{
weaponsManager.WeaponEquipped += OnWeaponEquipped;
weaponsManager.WeaponUnequipped += OnWeaponUnequipped;
}
private void OnDisable()
{
weaponsManager.WeaponEquipped -= OnWeaponEquipped;
weaponsManager.WeaponUnequipped -= OnWeaponUnequipped;
}
private void Reset()
{
if (!mainCamera) mainCamera = Camera.main;
if (weapons == null || weapons.Count == 0)
{
weapons = new List<AutoWeapon>(GetComponentsInChildren<AutoWeapon>());
}
}
private void Update()
{
if (!mainCamera || weapons.Count == 0) return;
var mouseScreen = Mouse.current.position.ReadValue();
var mouseWorld = mainCamera.ScreenToWorldPoint(mouseScreen);
mouseWorld.z = 0f;
foreach (var weapon in weapons)
{
weapon.Target = mouseWorld;
}
}
private void OnWeaponEquipped(Weapon weapon)
{
if (!weapon || weapon is not AutoWeapon autoWeapon) return;
if (autoWeapon && !weapons.Contains(autoWeapon))
{
weapons.Add(autoWeapon);
}
}
private void OnWeaponUnequipped(Weapon weapon)
{
if (!weapon || weapon is not AutoWeapon autoWeapon) return;
if (autoWeapon && weapons.Contains(autoWeapon))
{
weapons.Remove(autoWeapon);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f973da80e1dd49c8a9efdc270992b705
timeCreated: 1752316388

View File

@@ -8,8 +8,14 @@ namespace Weapons
[SerializeField] private GameObject projectilePrefab; [SerializeField] private GameObject projectilePrefab;
[SerializeField] private Transform firePoint; [SerializeField] private Transform firePoint;
public Vector2 Target { get; set; }
public override void Fire() public override void Fire()
{ {
var direction = (Target - (Vector2)firePoint.position).normalized;
firePoint.up = direction;
Debug.DrawLine(firePoint.position, Target, Color.red, 2f);
var projectile = Instantiate(projectilePrefab, firePoint.position, firePoint.rotation); var projectile = Instantiate(projectilePrefab, firePoint.position, firePoint.rotation);
projectile.TryGetComponent<IDamageInflectorSetup>(out var inflector); projectile.TryGetComponent<IDamageInflectorSetup>(out var inflector);