using System; using UnityEngine; namespace BumpCombat.Enemies { public enum EnemyKind { Slime = 0, Skeleton = 1, Lancer = 2, GreatswordSkeleton = 3, Warlock = 4, Bat = 5, Necrofire = 6, SkeletonArcher = 7, ArmoredSkeleton = 8, Werewolf = 9, Werebear = 10, NecroGolem = 11, Necromancer = 12, } public enum EnemyRole { Normal, Crowd, } public enum EnemyAttackShape { Body, Box, Cone, TargetCircle, } public enum EnemyAttackDirectionMode { LockedDirection, HorizontalRoot, } public enum EnemyAttackGeometryScaleMode { WorldUnits, RootScale, } [Serializable] public struct EnemyAttackContactPhase { [SerializeField, Range(0f, 1f)] private float startNormalized; [SerializeField, Range(0f, 1f)] private float endNormalized; public float StartNormalized => Mathf.Clamp01(startNormalized); public float EndNormalized => Mathf.Clamp01(Mathf.Max( startNormalized, endNormalized)); public static EnemyAttackContactPhase Create(float start, float end) { return new EnemyAttackContactPhase { startNormalized = Mathf.Clamp01(start), endNormalized = Mathf.Clamp01(end), }; } } [Serializable] public struct EnemyAttackPattern { [SerializeField] private string animationState; [SerializeField] private EnemyAttackShape attackShape; [SerializeField] private EnemyAttackDirectionMode directionMode; [SerializeField] private EnemyAttackGeometryScaleMode geometryScaleMode; [SerializeField, Min(0f)] private float warningDuration; [SerializeField, Min(0f)] private float activeDuration; [SerializeField, Min(0f)] private float recoveryDuration; [SerializeField, Min(0f)] private float attackRange; [SerializeField, Min(0f)] private float attackLength; [SerializeField, Min(0f)] private float attackWidth; [SerializeField, Min(0f)] private float attackRadius; [SerializeField] private Vector2 attackOriginOffset; [SerializeField, Min(0f)] private float animationLeadTime; [SerializeField, Min(0f)] private float damageMultiplier; [SerializeField] private EnemyAttackContactPhase[] contactPhases; public string AnimationState => animationState; public EnemyAttackShape AttackShape => attackShape; public EnemyAttackDirectionMode DirectionMode => directionMode; public EnemyAttackGeometryScaleMode GeometryScaleMode => geometryScaleMode; public float WarningDuration => Mathf.Max(0f, warningDuration); public float ActiveDuration => Mathf.Max(0f, activeDuration); public float RecoveryDuration => Mathf.Max(0f, recoveryDuration); public float AttackRange => Mathf.Max(0f, attackRange); public float AttackLength => Mathf.Max(0f, attackLength); public float AttackWidth => Mathf.Max(0f, attackWidth); public float AttackRadius => Mathf.Max(0f, attackRadius); public Vector2 AttackOriginOffset => attackOriginOffset; public float AnimationLeadTime => Mathf.Max(0f, animationLeadTime); public float DamageMultiplier => damageMultiplier <= 0f ? 1f : damageMultiplier; public int ContactPhaseCount => contactPhases?.Length ?? 0; public EnemyAttackContactPhase GetContactPhase(int index) { if (contactPhases == null || contactPhases.Length == 0) { return EnemyAttackContactPhase.Create(0f, 1f); } return contactPhases[Mathf.Clamp(index, 0, contactPhases.Length - 1)]; } public bool IsContactWindowActive( float activeElapsed, float effectiveActiveDuration) { if (effectiveActiveDuration <= 0.0001f || activeElapsed < 0f || activeElapsed >= effectiveActiveDuration) { return false; } if (contactPhases == null || contactPhases.Length == 0) { return true; } float normalized = effectiveActiveDuration <= 0.0001f ? 1f : Mathf.Clamp01(activeElapsed / effectiveActiveDuration); for (int i = 0; i < contactPhases.Length; i++) { EnemyAttackContactPhase phase = contactPhases[i]; if (normalized >= phase.StartNormalized && normalized < phase.EndNormalized) { return true; } } return false; } public static EnemyAttackPattern Create( string state, EnemyAttackShape shape, float warning, float active, float recovery, float range, float length, float width, float radius, float damage = 1f, float animationLead = 0f) { return new EnemyAttackPattern { animationState = state, attackShape = shape, directionMode = EnemyAttackDirectionMode.LockedDirection, geometryScaleMode = EnemyAttackGeometryScaleMode.WorldUnits, warningDuration = warning, activeDuration = active, recoveryDuration = recovery, attackRange = range, attackLength = length, attackWidth = width, attackRadius = radius, attackOriginOffset = Vector2.zero, animationLeadTime = animationLead, damageMultiplier = damage, contactPhases = null, }; } } [Serializable] public struct RunEventEnemyTuning { [SerializeField] private EnemyKind prefabKind; [SerializeField, Min(1f)] private float healthMultiplier; [SerializeField, Min(0)] private int experienceValue; [SerializeField, Min(1f)] private float scaleMultiplier; [SerializeField] private Color tint; [SerializeField, Min(0f)] private float moveSpeedMultiplier; [SerializeField, Min(0f)] private float attackDamageMultiplier; [SerializeField, Min(0f)] private float warningDurationMultiplier; [SerializeField, Min(0f)] private float activeDurationMultiplier; [SerializeField, Min(0f)] private float recoveryDurationMultiplier; [SerializeField, Min(0f)] private float attackRangeMultiplier; [SerializeField, Min(0f)] private float attackLengthMultiplier; [SerializeField, Min(0f)] private float attackWidthMultiplier; [SerializeField, Min(0f)] private float attackRadiusMultiplier; [SerializeField, Min(1)] private int attacksPerSequence; [SerializeField, Min(0f)] private float repeatWarningDurationMultiplier; [SerializeField, Min(0f)] private float betweenAttacksRecoveryMultiplier; [SerializeField, Min(1)] private int backHitsToInterrupt; [SerializeField, Range(0f, 1f)] private float knockbackMultiplier; [SerializeField] private bool canBeLaunched; public EnemyKind PrefabKind => prefabKind; public float HealthMultiplier => Mathf.Max(1f, healthMultiplier); public int ExperienceValue => Mathf.Max(0, experienceValue); public float ScaleMultiplier => Mathf.Max(1f, scaleMultiplier); public Color Tint => tint; public float MoveSpeedMultiplier => Mathf.Max(0f, moveSpeedMultiplier); public float AttackDamageMultiplier => Mathf.Max(0f, attackDamageMultiplier); public float WarningDurationMultiplier => Mathf.Max(0f, warningDurationMultiplier); public float ActiveDurationMultiplier => Mathf.Max(0f, activeDurationMultiplier); public float RecoveryDurationMultiplier => Mathf.Max(0f, recoveryDurationMultiplier); public float AttackRangeMultiplier => Mathf.Max(0f, attackRangeMultiplier); public float AttackLengthMultiplier => Mathf.Max(0f, attackLengthMultiplier); public float AttackWidthMultiplier => Mathf.Max(0f, attackWidthMultiplier); public float AttackRadiusMultiplier => Mathf.Max(0f, attackRadiusMultiplier); public int AttacksPerSequence => Mathf.Max(1, attacksPerSequence); public float RepeatWarningDurationMultiplier => Mathf.Max(0f, repeatWarningDurationMultiplier); public float BetweenAttacksRecoveryMultiplier => Mathf.Max(0f, betweenAttacksRecoveryMultiplier); public int BackHitsToInterrupt => Mathf.Max(1, backHitsToInterrupt); public float KnockbackMultiplier => Mathf.Clamp01(knockbackMultiplier); public bool CanBeLaunched => canBeLaunched; public static RunEventEnemyTuning Create( EnemyKind kind, float health, int experience, float scale, Color color, float moveSpeed, float attackDamage, float warningDuration, float activeDuration, float recoveryDuration, float attackRange, float attackLength, float attackWidth, float attackRadius, int attackCount, float repeatWarning, float betweenAttacksRecovery, int staggerHits, float knockback, bool launchable) { return new RunEventEnemyTuning { prefabKind = kind, healthMultiplier = health, experienceValue = experience, scaleMultiplier = scale, tint = color, moveSpeedMultiplier = moveSpeed, attackDamageMultiplier = attackDamage, warningDurationMultiplier = warningDuration, activeDurationMultiplier = activeDuration, recoveryDurationMultiplier = recoveryDuration, attackRangeMultiplier = attackRange, attackLengthMultiplier = attackLength, attackWidthMultiplier = attackWidth, attackRadiusMultiplier = attackRadius, attacksPerSequence = attackCount, repeatWarningDurationMultiplier = repeatWarning, betweenAttacksRecoveryMultiplier = betweenAttacksRecovery, backHitsToInterrupt = staggerHits, knockbackMultiplier = knockback, canBeLaunched = launchable, }; } } [CreateAssetMenu(menuName = "BumpCombat/Enemy Definition")] public sealed class EnemyDefinition : ScriptableObject { [SerializeField] private EnemyKind kind; [SerializeField] private EnemyRole role; [SerializeField] private EnemyAttackShape attackShape; [SerializeField, Min(1f)] private float maxHealth; [SerializeField, Min(0f)] private float moveSpeed; [SerializeField, Min(0f)] private float attackDamage; [SerializeField, Min(0f)] private float warningDuration; [SerializeField, Min(0f)] private float activeDuration; [SerializeField, Min(0f)] private float recoveryDuration; [SerializeField, Min(0f)] private float attackRange; [SerializeField, Min(0f)] private float preferredRange; [SerializeField, Min(0f)] private float attackLength; [SerializeField, Min(0f)] private float attackWidth; [SerializeField, Min(0f)] private float attackRadius; [SerializeField, Min(0f)] private float attackAnimationLeadTime; [SerializeField, Min(0f)] private float projectileSpeed; [SerializeField, Min(0f)] private float projectileRadius; [SerializeField, Min(0)] private int experienceValue; [SerializeField] private EnemyAttackPattern[] attackPatterns; [SerializeField, Min(0f)] private float separationRadius = 0.55f; [SerializeField, Min(0f)] private float separationStrength = 0.6f; public EnemyKind Kind => kind; public EnemyRole Role => role; public EnemyAttackShape AttackShape => attackShape; public float MaxHealth => maxHealth; public float MoveSpeed => moveSpeed; public float AttackDamage => attackDamage; public float WarningDuration => warningDuration; public float ActiveDuration => activeDuration; public float RecoveryDuration => recoveryDuration; public float AttackRange => attackRange; public float PreferredRange => preferredRange; public float AttackLength => attackLength; public float AttackWidth => attackWidth; public float AttackRadius => attackRadius; public float AttackAnimationLeadTime => Mathf.Max(0f, attackAnimationLeadTime); public float ProjectileSpeed => Mathf.Max(0f, projectileSpeed); public float ProjectileRadius => Mathf.Max(0f, projectileRadius); public int ExperienceValue => experienceValue; public float SeparationRadius => Mathf.Max(0f, separationRadius); public float SeparationStrength => Mathf.Max(0f, separationStrength); public int AttackPatternCount => attackPatterns?.Length ?? 0; public bool IsRanged => kind == EnemyKind.Warlock || kind == EnemyKind.Necrofire || kind == EnemyKind.SkeletonArcher || kind == EnemyKind.Necromancer; public bool IsCrowd => role == EnemyRole.Crowd; public bool HasContactDamage => kind == EnemyKind.Slime || kind == EnemyKind.Bat; public EnemyAttackPattern GetAttackPattern(int index) { if (attackPatterns == null || attackPatterns.Length == 0) { return EnemyAttackPattern.Create( "Attack", attackShape, warningDuration, activeDuration, recoveryDuration, attackRange, attackLength, attackWidth, attackRadius, 1f, attackAnimationLeadTime); } return attackPatterns[Mathf.Clamp(index, 0, attackPatterns.Length - 1)]; } public EnemyAttackPattern[] AttackPatterns => attackPatterns; #if UNITY_EDITOR public void Configure( EnemyKind enemyKind, EnemyAttackShape shape, float health, float speed, float damage, float warning, float active, float recovery, float range, float preferred, float length, float width, float radius, int experience, float animationLeadTime = 0f) { kind = enemyKind; attackShape = shape; maxHealth = health; moveSpeed = speed; attackDamage = damage; warningDuration = warning; activeDuration = active; recoveryDuration = recovery; attackRange = range; preferredRange = preferred; attackLength = length; attackWidth = width; attackRadius = radius; attackAnimationLeadTime = animationLeadTime; experienceValue = experience; } public void ConfigureRole(EnemyRole enemyRole) { role = enemyRole; } public void ConfigureAttackPatterns(EnemyAttackPattern[] patterns) { attackPatterns = patterns; } #endif } }