feat: refine enemy engagements and reduce tower damage
This commit is contained in:
@@ -42,6 +42,12 @@ namespace Northbound.Core
|
||||
void TakeDamage(float amount, Team sourceTeam);
|
||||
}
|
||||
|
||||
public interface IEnemyStructureTarget
|
||||
{
|
||||
bool CanBeTargetedByEnemies { get; }
|
||||
IDamageable DamageReceiver { get; }
|
||||
}
|
||||
|
||||
public interface IInteractable
|
||||
{
|
||||
string GetInteractionPrompt();
|
||||
|
||||
@@ -21,6 +21,8 @@ namespace Northbound.Editor
|
||||
public const string ScenePath = "Assets/Northbound/Scenes/NorthboundGameplayPrototype.unity";
|
||||
private const string MaterialDirectory = "Assets/Northbound/Generated/Materials";
|
||||
private const string DefinitionDirectory = "Assets/Northbound/Generated/Definitions";
|
||||
private const string PlayerDefinitionPath = DefinitionDirectory + "/PrototypePlayerDefinition.asset";
|
||||
private const string EnemyPursuitDefinitionPath = DefinitionDirectory + "/DefaultEnemyPursuit.asset";
|
||||
private const string CoreBuildingPath = DefinitionDirectory + "/SouthernCoreBuilding.asset";
|
||||
private const string OutpostBuildingPath = DefinitionDirectory + "/CrystalOutpostBuilding.asset";
|
||||
private const string PortalBuildingPath = DefinitionDirectory + "/PortalBuilding.asset";
|
||||
@@ -53,6 +55,8 @@ namespace Northbound.Editor
|
||||
var basicTowerBuilding = GetBuildingDefinition(BasicTowerBuildingPath, 2000, "Basic Tower", 150f, tower, PrimitiveType.Cylinder, new Vector3(0.7f, 1.25f, 0.7f));
|
||||
var basicTowerDefinition = GetBasicTowerDefinition(basicTowerBuilding);
|
||||
var basicTowerConstruction = GetBasicTowerConstruction(basicTowerBuilding);
|
||||
var playerDefinition = GetPlayerDefinition();
|
||||
var enemyPursuitDefinition = GetEnemyPursuitDefinition();
|
||||
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
CreateEnvironment(ground, lane);
|
||||
@@ -61,9 +65,9 @@ namespace Northbound.Editor
|
||||
var systems = new GameObject("Game Systems");
|
||||
systems.AddComponent<SharedGameState>();
|
||||
systems.AddComponent<PrototypeHud>();
|
||||
systems.AddComponent<WaveDirector>().Configure(grunt, raider, elite);
|
||||
systems.AddComponent<WaveDirector>().Configure(grunt, raider, elite, enemyPursuitDefinition);
|
||||
|
||||
var playerController = CreatePlayer(player);
|
||||
var playerController = CreatePlayer(player, playerDefinition);
|
||||
CreateCamera(playerController);
|
||||
CreateCore(core, coreBuilding);
|
||||
|
||||
@@ -74,8 +78,8 @@ namespace Northbound.Editor
|
||||
CreatePortal(index, portalPositions[index], initialState, portalBuilding, enemyPortal, ruinedPortal, alliedPortal);
|
||||
if (initialState == PortalState.Enemy)
|
||||
{
|
||||
CreateGuardian(new Vector3(-3f, 1f, portalPositions[index] - 3f), grunt);
|
||||
CreateGuardian(new Vector3(3f, 1f, portalPositions[index] - 3f), grunt);
|
||||
CreateGuardian(new Vector3(-3f, 1f, portalPositions[index] - 3f), grunt, enemyPursuitDefinition);
|
||||
CreateGuardian(new Vector3(3f, 1f, portalPositions[index] - 3f), grunt, enemyPursuitDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,15 +117,16 @@ namespace Northbound.Editor
|
||||
light.intensity = 1.25f;
|
||||
}
|
||||
|
||||
private static PlayerController CreatePlayer(Material material)
|
||||
private static PlayerController CreatePlayer(Material material, PlayerDefinition definition)
|
||||
{
|
||||
var gameObject = CreatePrimitive("Player", PrimitiveType.Capsule, new Vector3(0f, 1f, -76f), Vector3.one, material);
|
||||
Object.DestroyImmediate(gameObject.GetComponent<CapsuleCollider>());
|
||||
gameObject.AddComponent<CharacterController>();
|
||||
gameObject.AddComponent<Health>().Configure(Team.Player, 100f);
|
||||
gameObject.AddComponent<Health>().Configure(Team.Player, definition.MaximumHealth);
|
||||
gameObject.AddComponent<PlayerActionController>();
|
||||
var controller = gameObject.AddComponent<PlayerController>();
|
||||
gameObject.AddComponent<PlayerCombat>();
|
||||
controller.Configure(definition);
|
||||
gameObject.AddComponent<PlayerCombat>().Configure(definition);
|
||||
return controller;
|
||||
}
|
||||
|
||||
@@ -176,10 +181,10 @@ namespace Northbound.Editor
|
||||
slot.AddComponent<DefenseSlot>().Configure(outpost, constructionDefinition, towerDefinition);
|
||||
}
|
||||
|
||||
private static void CreateGuardian(Vector3 position, Material material)
|
||||
private static void CreateGuardian(Vector3 position, Material material, EnemyPursuitDefinition pursuitDefinition)
|
||||
{
|
||||
var gameObject = CreatePrimitive("Portal Guard", PrimitiveType.Capsule, position, Vector3.one, material);
|
||||
gameObject.AddComponent<EnemyAgent>().Configure(EnemyRole.PortalGuard, 0, material);
|
||||
gameObject.AddComponent<EnemyAgent>().Configure(EnemyRole.PortalGuard, 0, material, pursuitDefinition);
|
||||
}
|
||||
|
||||
private static GameObject CreatePrimitive(string name, PrimitiveType type, Vector3 position, Vector3 scale, Material material)
|
||||
@@ -255,7 +260,7 @@ namespace Northbound.Editor
|
||||
|
||||
definition = ScriptableObject.CreateInstance<TowerDefinition>();
|
||||
definition.name = "BasicTowerDefinition";
|
||||
definition.ConfigurePrototype(building, 8f, 9f, 1.2f, new Color(1f, 0.75f, 0.15f, 0.95f));
|
||||
definition.ConfigurePrototype(building, 8f, 6f, 1.2f, new Color(1f, 0.75f, 0.15f, 0.95f));
|
||||
AssetDatabase.CreateAsset(definition, BasicTowerDefinitionPath);
|
||||
return definition;
|
||||
}
|
||||
@@ -275,6 +280,50 @@ namespace Northbound.Editor
|
||||
return definition;
|
||||
}
|
||||
|
||||
private static PlayerDefinition GetPlayerDefinition()
|
||||
{
|
||||
var definition = AssetDatabase.LoadAssetAtPath<PlayerDefinition>(PlayerDefinitionPath);
|
||||
if (definition != null)
|
||||
{
|
||||
return definition;
|
||||
}
|
||||
|
||||
definition = ScriptableObject.CreateInstance<PlayerDefinition>();
|
||||
definition.name = "PrototypePlayerDefinition";
|
||||
definition.ConfigurePrototype(
|
||||
"Prototype Player",
|
||||
100f,
|
||||
5f,
|
||||
15f,
|
||||
3.2f,
|
||||
13f,
|
||||
3f,
|
||||
0.4f,
|
||||
new PlayerActionDefinition("Normal Attack", 0.3f, false),
|
||||
34f,
|
||||
4.2f,
|
||||
25f,
|
||||
5.5f,
|
||||
new PlayerActionDefinition("Special Attack", 0.65f, false));
|
||||
AssetDatabase.CreateAsset(definition, PlayerDefinitionPath);
|
||||
return definition;
|
||||
}
|
||||
|
||||
private static EnemyPursuitDefinition GetEnemyPursuitDefinition()
|
||||
{
|
||||
var definition = AssetDatabase.LoadAssetAtPath<EnemyPursuitDefinition>(EnemyPursuitDefinitionPath);
|
||||
if (definition != null)
|
||||
{
|
||||
return definition;
|
||||
}
|
||||
|
||||
definition = ScriptableObject.CreateInstance<EnemyPursuitDefinition>();
|
||||
definition.name = "DefaultEnemyPursuit";
|
||||
definition.ConfigurePrototype(7f, 4.5f, 10f, 9f, 3.5f);
|
||||
AssetDatabase.CreateAsset(definition, EnemyPursuitDefinitionPath);
|
||||
return definition;
|
||||
}
|
||||
|
||||
private static void EnsureDirectory(string assetPath)
|
||||
{
|
||||
var absolutePath = Path.GetFullPath(assetPath);
|
||||
|
||||
@@ -6,12 +6,23 @@ using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Defense
|
||||
{
|
||||
public sealed class BasicTower : MonoBehaviour
|
||||
public sealed class BasicTower : MonoBehaviour, IEnemyStructureTarget
|
||||
{
|
||||
[SerializeField] private TowerDefinition definition;
|
||||
|
||||
private float readyAt;
|
||||
|
||||
public bool CanBeTargetedByEnemies
|
||||
{
|
||||
get
|
||||
{
|
||||
var health = GetComponent<Health>();
|
||||
return definition?.Building != null && health != null && health.IsAlive;
|
||||
}
|
||||
}
|
||||
|
||||
public IDamageable DamageReceiver => GetComponent<Health>();
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (definition == null || Time.time < readyAt)
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Northbound.Gameplay.Defense
|
||||
{
|
||||
[SerializeField] private BuildingDefinition building;
|
||||
[SerializeField] private float range = 8f;
|
||||
[SerializeField] private float damage = 9f;
|
||||
[SerializeField] private float damage = 6f;
|
||||
[SerializeField] private float attackInterval = 1.2f;
|
||||
[SerializeField] private Color attackColor = new(1f, 0.75f, 0.15f, 0.95f);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Northbound.Gameplay.Enemy
|
||||
[SerializeField] private float attackDamage = 12f;
|
||||
[SerializeField] private float attackRange = 2f;
|
||||
[SerializeField] private float attackInterval = 1.25f;
|
||||
[SerializeField] private float playerAggroRange = 7f;
|
||||
[SerializeField] private EnemyPursuitDefinition pursuitDefinition;
|
||||
[SerializeField] private bool affectedByResonance = true;
|
||||
|
||||
private float currentHealth;
|
||||
@@ -32,6 +32,12 @@ namespace Northbound.Gameplay.Enemy
|
||||
private Vector3 guardPosition;
|
||||
private CharacterController movementController;
|
||||
private float avoidanceDirection;
|
||||
private bool isPursuingPlayer;
|
||||
private bool isPlayerAggroSuppressed;
|
||||
private float unansweredPursuitTime;
|
||||
private Vector3 playerPursuitOrigin;
|
||||
private MonoBehaviour strategicTarget;
|
||||
private bool isStructureEngagementLocked;
|
||||
|
||||
public EnemyRole Role => role;
|
||||
public Team Team => Team.Enemy;
|
||||
@@ -57,6 +63,7 @@ namespace Northbound.Gameplay.Enemy
|
||||
var target = SelectTarget();
|
||||
if (target == null)
|
||||
{
|
||||
ReturnToGuardPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,17 +76,32 @@ namespace Northbound.Gameplay.Enemy
|
||||
return;
|
||||
}
|
||||
|
||||
if (Time.time >= readyAt && target is IDamageable damageable)
|
||||
var damageable = ResolveDamageReceiver(target);
|
||||
if (Time.time >= readyAt && damageable != null)
|
||||
{
|
||||
readyAt = Time.time + attackInterval;
|
||||
if (IsValidStructureTarget(target))
|
||||
{
|
||||
// Selecting a march destination does not commit us; the first attack does.
|
||||
strategicTarget = target;
|
||||
isStructureEngagementLocked = true;
|
||||
EndPlayerPursuit(true);
|
||||
}
|
||||
if (isPursuingPlayer)
|
||||
{
|
||||
// Only an enemy attack opportunity replenishes pursuit, not incoming damage.
|
||||
unansweredPursuitTime = 0f;
|
||||
playerPursuitOrigin = transform.position;
|
||||
}
|
||||
AttackTracer.Spawn(transform.position, targetPosition, new Color(1f, 0.18f, 0.08f, 0.95f), 0.22f, 0.2f);
|
||||
damageable.TakeDamage(attackDamage, Team.Enemy);
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(EnemyRole enemyRole, int resonance, Material material)
|
||||
public void Configure(EnemyRole enemyRole, int resonance, Material material, EnemyPursuitDefinition pursuit)
|
||||
{
|
||||
role = enemyRole;
|
||||
pursuitDefinition = pursuit;
|
||||
affectedByResonance = enemyRole != EnemyRole.PortalGuard;
|
||||
guardPosition = transform.position;
|
||||
|
||||
@@ -176,43 +198,206 @@ namespace Northbound.Gameplay.Enemy
|
||||
|
||||
private MonoBehaviour SelectTarget()
|
||||
{
|
||||
var player = FindAnyObjectByType<PlayerController>();
|
||||
if (player != null && Vector3.SqrMagnitude(player.transform.position - transform.position) <= playerAggroRange * playerAggroRange)
|
||||
if (isStructureEngagementLocked)
|
||||
{
|
||||
return player.GetComponent<Combat.Health>();
|
||||
if (IsValidStructureTarget(strategicTarget))
|
||||
{
|
||||
// Neither nearby players nor newly built structures interrupt this engagement.
|
||||
return strategicTarget;
|
||||
}
|
||||
|
||||
isStructureEngagementLocked = false;
|
||||
strategicTarget = null;
|
||||
// A destroyed/lost structure opens a new engagement, including player detection.
|
||||
EndPlayerPursuit(false);
|
||||
}
|
||||
|
||||
if (role != EnemyRole.PortalGuard)
|
||||
{
|
||||
var northernTarget = SelectNorthernStructureTarget();
|
||||
if (!IsValidStructureTarget(strategicTarget)
|
||||
|| northernTarget != null && northernTarget.transform.position.z > strategicTarget.transform.position.z + 0.01f)
|
||||
{
|
||||
strategicTarget = northernTarget;
|
||||
}
|
||||
}
|
||||
|
||||
var player = FindAnyObjectByType<PlayerController>();
|
||||
var playerHealth = player == null ? null : player.GetComponent<Combat.Health>();
|
||||
var aggroOrigin = role == EnemyRole.PortalGuard ? guardPosition : transform.position;
|
||||
if (TrySelectPlayer(player, playerHealth, aggroOrigin, out var selectedPlayer))
|
||||
{
|
||||
return selectedPlayer;
|
||||
}
|
||||
|
||||
if (role == EnemyRole.PortalGuard)
|
||||
{
|
||||
if (player == null || Vector3.SqrMagnitude(player.transform.position - guardPosition) > playerAggroRange * playerAggroRange)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return player.GetComponent<Combat.Health>();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (role == EnemyRole.Raider)
|
||||
return strategicTarget;
|
||||
}
|
||||
|
||||
private MonoBehaviour SelectNorthernStructureTarget()
|
||||
{
|
||||
MonoBehaviour selected = null;
|
||||
var selectedZ = float.NegativeInfinity;
|
||||
var selectedDistance = float.MaxValue;
|
||||
var backtrackDistance = pursuitDefinition == null
|
||||
? 0f
|
||||
: pursuitDefinition.MaximumTargetBacktrackDistance;
|
||||
var northernLimit = transform.position.z + backtrackDistance;
|
||||
|
||||
foreach (var behaviour in FindObjectsByType<MonoBehaviour>())
|
||||
{
|
||||
CrystalOutpost nearest = null;
|
||||
var nearestDistance = float.MaxValue;
|
||||
foreach (var outpost in FindObjectsByType<CrystalOutpost>())
|
||||
if (!behaviour.isActiveAndEnabled
|
||||
|| behaviour is not IEnemyStructureTarget structure
|
||||
|| !structure.CanBeTargetedByEnemies)
|
||||
{
|
||||
var distance = Vector3.SqrMagnitude(outpost.transform.position - transform.position);
|
||||
if (outpost.IsCaptured && distance < nearestDistance)
|
||||
continue;
|
||||
}
|
||||
|
||||
var receiver = structure.DamageReceiver;
|
||||
if (receiver == null || !receiver.IsAlive || receiver.Team != Team.Player)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidateZ = behaviour.transform.position.z;
|
||||
if (candidateZ > northernLimit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var distance = PlanarSqrDistance(behaviour.transform.position, transform.position);
|
||||
var sameFrontline = Mathf.Abs(candidateZ - selectedZ) <= 0.01f;
|
||||
var raiderPrefersOutpost = role == EnemyRole.Raider
|
||||
&& sameFrontline
|
||||
&& behaviour is CrystalOutpost
|
||||
&& selected is not CrystalOutpost;
|
||||
var raiderKeepsOutpost = role == EnemyRole.Raider
|
||||
&& sameFrontline
|
||||
&& selected is CrystalOutpost
|
||||
&& behaviour is not CrystalOutpost;
|
||||
if (candidateZ > selectedZ + 0.01f
|
||||
|| raiderPrefersOutpost
|
||||
|| sameFrontline && !raiderKeepsOutpost && distance < selectedDistance)
|
||||
{
|
||||
selected = behaviour;
|
||||
selectedZ = candidateZ;
|
||||
selectedDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static bool IsValidStructureTarget(MonoBehaviour target)
|
||||
{
|
||||
if (target == null || !target.isActiveAndEnabled
|
||||
|| target is not IEnemyStructureTarget structure || !structure.CanBeTargetedByEnemies)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var receiver = structure.DamageReceiver;
|
||||
return receiver != null && receiver.IsAlive && receiver.Team == Team.Player;
|
||||
}
|
||||
|
||||
private static IDamageable ResolveDamageReceiver(MonoBehaviour target)
|
||||
{
|
||||
return target is IEnemyStructureTarget structure
|
||||
? structure.DamageReceiver
|
||||
: target as IDamageable;
|
||||
}
|
||||
|
||||
private bool TrySelectPlayer(
|
||||
PlayerController player,
|
||||
Combat.Health playerHealth,
|
||||
Vector3 aggroOrigin,
|
||||
out MonoBehaviour selectedPlayer)
|
||||
{
|
||||
selectedPlayer = null;
|
||||
if (pursuitDefinition == null || player == null || playerHealth == null || !playerHealth.IsAlive)
|
||||
{
|
||||
EndPlayerPursuit(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
var distanceFromAggroOrigin = PlanarSqrDistance(player.transform.position, aggroOrigin);
|
||||
if (isPursuingPlayer)
|
||||
{
|
||||
// Use the same collider-surface range check as the attack itself.
|
||||
// Cooldown time in reach preserves engagement but does not refill it.
|
||||
var canReachPlayer = PlanarSqrDistance(GetClosestTargetPoint(playerHealth), transform.position)
|
||||
<= attackRange * attackRange;
|
||||
if (!canReachPlayer)
|
||||
{
|
||||
unansweredPursuitTime += Time.deltaTime;
|
||||
var exceededDuration = unansweredPursuitTime >= pursuitDefinition.DisengageDelay;
|
||||
var exceededDistance = PlanarSqrDistance(transform.position, playerPursuitOrigin)
|
||||
>= pursuitDefinition.MaximumUnansweredPursuitDistance * pursuitDefinition.MaximumUnansweredPursuitDistance;
|
||||
if (exceededDuration || exceededDistance)
|
||||
{
|
||||
nearest = outpost;
|
||||
nearestDistance = distance;
|
||||
EndPlayerPursuit(true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearest != null)
|
||||
{
|
||||
return nearest;
|
||||
}
|
||||
selectedPlayer = playerHealth;
|
||||
return true;
|
||||
}
|
||||
|
||||
return FindAnyObjectByType<CoreObjective>();
|
||||
if (isPlayerAggroSuppressed)
|
||||
{
|
||||
if (distanceFromAggroOrigin <= pursuitDefinition.RearmDistance * pursuitDefinition.RearmDistance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
isPlayerAggroSuppressed = false;
|
||||
}
|
||||
|
||||
if (PlanarSqrDistance(player.transform.position, aggroOrigin)
|
||||
> pursuitDefinition.AggroRange * pursuitDefinition.AggroRange)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
isPursuingPlayer = true;
|
||||
playerPursuitOrigin = transform.position;
|
||||
unansweredPursuitTime = 0f;
|
||||
selectedPlayer = playerHealth;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EndPlayerPursuit(bool suppressAggro)
|
||||
{
|
||||
isPursuingPlayer = false;
|
||||
isPlayerAggroSuppressed = suppressAggro;
|
||||
unansweredPursuitTime = 0f;
|
||||
}
|
||||
|
||||
private void ReturnToGuardPosition()
|
||||
{
|
||||
if (role != EnemyRole.PortalGuard)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var planar = guardPosition - transform.position;
|
||||
planar.y = 0f;
|
||||
if (planar.sqrMagnitude > 0.1f)
|
||||
{
|
||||
MoveTowardsTarget(planar.normalized);
|
||||
}
|
||||
}
|
||||
|
||||
private static float PlanarSqrDistance(Vector3 a, Vector3 b)
|
||||
{
|
||||
a.y = 0f;
|
||||
b.y = 0f;
|
||||
return Vector3.SqrMagnitude(a - b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Northbound.Gameplay.Enemy
|
||||
{
|
||||
[CreateAssetMenu(fileName = "EnemyPursuitDefinition", menuName = "Northbound/Enemy Pursuit Definition")]
|
||||
public sealed class EnemyPursuitDefinition : ScriptableObject
|
||||
{
|
||||
[SerializeField] private float aggroRange = 7f;
|
||||
[FormerlySerializedAs("maximumPursuitDuration")]
|
||||
[Tooltip("Seconds spent outside attack reach since the enemy's last attack attempt before giving up.")]
|
||||
[SerializeField] private float disengageDelay = 4.5f;
|
||||
[FormerlySerializedAs("maximumPursuitDistance")]
|
||||
[Tooltip("Maximum displacement without an enemy attack opportunity. Resets when the enemy attacks.")]
|
||||
[SerializeField] private float maximumUnansweredPursuitDistance = 10f;
|
||||
[SerializeField] private float rearmDistance = 9f;
|
||||
[SerializeField] private float maximumTargetBacktrackDistance = 3.5f;
|
||||
|
||||
public float AggroRange => Mathf.Max(0f, aggroRange);
|
||||
public float DisengageDelay => Mathf.Max(0f, disengageDelay);
|
||||
public float MaximumUnansweredPursuitDistance => Mathf.Max(0f, maximumUnansweredPursuitDistance);
|
||||
public float RearmDistance => Mathf.Max(AggroRange, rearmDistance);
|
||||
public float MaximumTargetBacktrackDistance => Mathf.Max(0f, maximumTargetBacktrackDistance);
|
||||
|
||||
public void ConfigurePrototype(
|
||||
float detectionRange,
|
||||
float disengageTime,
|
||||
float unansweredPursuitDistance,
|
||||
float resetDistance,
|
||||
float targetBacktrackDistance)
|
||||
{
|
||||
aggroRange = Mathf.Max(0f, detectionRange);
|
||||
disengageDelay = Mathf.Max(0f, disengageTime);
|
||||
maximumUnansweredPursuitDistance = Mathf.Max(0f, unansweredPursuitDistance);
|
||||
rearmDistance = Mathf.Max(aggroRange, resetDistance);
|
||||
maximumTargetBacktrackDistance = Mathf.Max(0f, targetBacktrackDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9aa8e0cc71be49c9ac69d950aa3fe0d4
|
||||
@@ -6,15 +6,7 @@ namespace Northbound.Gameplay.Player
|
||||
{
|
||||
public sealed class PlayerCombat : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float normalDamage = 13f;
|
||||
[SerializeField] private float normalRange = 3f;
|
||||
[SerializeField] private float normalCooldown = 0.4f;
|
||||
[SerializeField] private PlayerActionDefinition normalAction = new("Normal Attack", 0.3f, false);
|
||||
[SerializeField] private float specialDamage = 34f;
|
||||
[SerializeField] private float specialRadius = 4.2f;
|
||||
[SerializeField] private float specialManaCost = 25f;
|
||||
[SerializeField] private float specialCooldown = 5.5f;
|
||||
[SerializeField] private PlayerActionDefinition specialAction = new("Special Attack", 0.65f, false);
|
||||
[SerializeField] private PlayerDefinition definition;
|
||||
|
||||
private float normalReadyAt;
|
||||
private float specialReadyAt;
|
||||
@@ -24,6 +16,13 @@ namespace Northbound.Gameplay.Player
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
Debug.LogError($"[{nameof(PlayerCombat)}] Player definition is missing on {name}.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
actionController = GetComponent<PlayerActionController>();
|
||||
if (actionController == null)
|
||||
{
|
||||
@@ -31,6 +30,11 @@ namespace Northbound.Gameplay.Player
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(PlayerDefinition playerDefinition)
|
||||
{
|
||||
definition = playerDefinition;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetMouseButtonDown(0) && Time.time >= normalReadyAt && actionController is { CanStartAction: true })
|
||||
@@ -46,30 +50,30 @@ namespace Northbound.Gameplay.Player
|
||||
|
||||
private void NormalAttack()
|
||||
{
|
||||
if (!actionController.TryBeginAction(normalAction))
|
||||
if (!actionController.TryBeginAction(definition.NormalAction))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
normalReadyAt = Time.time + normalCooldown;
|
||||
var center = transform.position + transform.forward * (normalRange * 0.55f);
|
||||
var radius = normalRange * 0.55f;
|
||||
normalReadyAt = Time.time + definition.NormalCooldown;
|
||||
var center = transform.position + transform.forward * (definition.NormalRange * 0.55f);
|
||||
var radius = definition.NormalRange * 0.55f;
|
||||
AttackFlash.Spawn(center, radius, new Color(0.15f, 0.85f, 1f, 0.72f), 0.16f);
|
||||
DamageTargets(Physics.OverlapSphere(center, radius), normalDamage);
|
||||
DamageTargets(Physics.OverlapSphere(center, radius), definition.NormalDamage);
|
||||
}
|
||||
|
||||
private void SpecialAttack()
|
||||
{
|
||||
var gameState = SharedGameState.Instance;
|
||||
if (actionController == null || !actionController.CanStartAction || gameState == null || !gameState.TrySpendMana(specialManaCost))
|
||||
if (actionController == null || !actionController.CanStartAction || gameState == null || !gameState.TrySpendMana(definition.SpecialManaCost))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
actionController.TryBeginAction(specialAction);
|
||||
specialReadyAt = Time.time + specialCooldown;
|
||||
AttackFlash.Spawn(transform.position, specialRadius, new Color(0.8f, 0.25f, 1f, 0.68f), 0.32f);
|
||||
DamageTargets(Physics.OverlapSphere(transform.position, specialRadius), specialDamage);
|
||||
actionController.TryBeginAction(definition.SpecialAction);
|
||||
specialReadyAt = Time.time + definition.SpecialCooldown;
|
||||
AttackFlash.Spawn(transform.position, definition.SpecialRadius, new Color(0.8f, 0.25f, 1f, 0.68f), 0.32f);
|
||||
DamageTargets(Physics.OverlapSphere(transform.position, definition.SpecialRadius), definition.SpecialDamage);
|
||||
}
|
||||
|
||||
private static void DamageTargets(Collider[] colliders, float damage)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay.Combat;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Player
|
||||
{
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
[RequireComponent(typeof(Health))]
|
||||
[RequireComponent(typeof(PlayerActionController))]
|
||||
[DefaultExecutionOrder(-100)]
|
||||
public sealed class PlayerController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float moveSpeed = 5f;
|
||||
[SerializeField] private float horizontalLimit = 15f;
|
||||
[SerializeField] private float interactionRadius = 3.2f;
|
||||
[SerializeField] private PlayerDefinition definition;
|
||||
|
||||
private CharacterController characterController;
|
||||
private PlayerActionController actionController;
|
||||
@@ -21,12 +21,30 @@ namespace Northbound.Gameplay.Player
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
Debug.LogError($"[{nameof(PlayerController)}] Player definition is missing on {name}.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
characterController = GetComponent<CharacterController>();
|
||||
actionController = GetComponent<PlayerActionController>();
|
||||
if (actionController == null)
|
||||
{
|
||||
actionController = gameObject.AddComponent<PlayerActionController>();
|
||||
}
|
||||
|
||||
GetComponent<Health>().Configure(Team.Player, definition.MaximumHealth);
|
||||
}
|
||||
|
||||
public void Configure(PlayerDefinition playerDefinition)
|
||||
{
|
||||
definition = playerDefinition;
|
||||
if (definition != null && TryGetComponent<Health>(out var health))
|
||||
{
|
||||
health.Configure(Team.Player, definition.MaximumHealth);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -58,9 +76,9 @@ namespace Northbound.Gameplay.Player
|
||||
LastMoveDirection = input.normalized;
|
||||
}
|
||||
|
||||
characterController.Move(input * (moveSpeed * Time.deltaTime));
|
||||
characterController.Move(input * (definition.MoveSpeed * Time.deltaTime));
|
||||
var position = transform.position;
|
||||
position.x = Mathf.Clamp(position.x, -horizontalLimit, horizontalLimit);
|
||||
position.x = Mathf.Clamp(position.x, -definition.HorizontalLimit, definition.HorizontalLimit);
|
||||
position.y = 1f;
|
||||
transform.position = position;
|
||||
}
|
||||
@@ -93,7 +111,7 @@ namespace Northbound.Gameplay.Player
|
||||
var nearestDistance = float.MaxValue;
|
||||
IInteractable nearest = null;
|
||||
|
||||
foreach (var collider in Physics.OverlapSphere(transform.position, interactionRadius))
|
||||
foreach (var collider in Physics.OverlapSphere(transform.position, definition.InteractionRadius))
|
||||
{
|
||||
foreach (var behaviour in collider.GetComponentsInParent<MonoBehaviour>())
|
||||
{
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using Northbound.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Player
|
||||
{
|
||||
[CreateAssetMenu(fileName = "PlayerDefinition", menuName = "Northbound/Player Definition")]
|
||||
public sealed class PlayerDefinition : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
[SerializeField] private string displayName = "Player";
|
||||
|
||||
[Header("Survivability and Movement")]
|
||||
[SerializeField] private float maximumHealth = 100f;
|
||||
[SerializeField] private float moveSpeed = 5f;
|
||||
[SerializeField] private float horizontalLimit = 15f;
|
||||
[SerializeField] private float interactionRadius = 3.2f;
|
||||
|
||||
[Header("Normal Attack")]
|
||||
[SerializeField] private float normalDamage = 13f;
|
||||
[SerializeField] private float normalRange = 3f;
|
||||
[SerializeField] private float normalCooldown = 0.4f;
|
||||
[SerializeField] private PlayerActionDefinition normalAction = new("Normal Attack", 0.3f, false);
|
||||
|
||||
[Header("Special Attack")]
|
||||
[SerializeField] private float specialDamage = 34f;
|
||||
[SerializeField] private float specialRadius = 4.2f;
|
||||
[SerializeField] private float specialManaCost = 25f;
|
||||
[SerializeField] private float specialCooldown = 5.5f;
|
||||
[SerializeField] private PlayerActionDefinition specialAction = new("Special Attack", 0.65f, false);
|
||||
|
||||
public string DisplayName => displayName;
|
||||
public float MaximumHealth => Mathf.Max(1f, maximumHealth);
|
||||
public float MoveSpeed => Mathf.Max(0f, moveSpeed);
|
||||
public float HorizontalLimit => Mathf.Max(0f, horizontalLimit);
|
||||
public float InteractionRadius => Mathf.Max(0f, interactionRadius);
|
||||
public float NormalDamage => Mathf.Max(0f, normalDamage);
|
||||
public float NormalRange => Mathf.Max(0f, normalRange);
|
||||
public float NormalCooldown => Mathf.Max(0f, normalCooldown);
|
||||
public PlayerActionDefinition NormalAction => normalAction;
|
||||
public float SpecialDamage => Mathf.Max(0f, specialDamage);
|
||||
public float SpecialRadius => Mathf.Max(0f, specialRadius);
|
||||
public float SpecialManaCost => Mathf.Max(0f, specialManaCost);
|
||||
public float SpecialCooldown => Mathf.Max(0f, specialCooldown);
|
||||
public PlayerActionDefinition SpecialAction => specialAction;
|
||||
|
||||
public void ConfigurePrototype(
|
||||
string playerName,
|
||||
float health,
|
||||
float speed,
|
||||
float mapHorizontalLimit,
|
||||
float interactRadius,
|
||||
float basicDamage,
|
||||
float basicRange,
|
||||
float basicCooldown,
|
||||
PlayerActionDefinition basicAction,
|
||||
float abilityDamage,
|
||||
float abilityRadius,
|
||||
float abilityManaCost,
|
||||
float abilityCooldown,
|
||||
PlayerActionDefinition abilityAction)
|
||||
{
|
||||
displayName = playerName;
|
||||
maximumHealth = Mathf.Max(1f, health);
|
||||
moveSpeed = Mathf.Max(0f, speed);
|
||||
horizontalLimit = Mathf.Max(0f, mapHorizontalLimit);
|
||||
interactionRadius = Mathf.Max(0f, interactRadius);
|
||||
normalDamage = Mathf.Max(0f, basicDamage);
|
||||
normalRange = Mathf.Max(0f, basicRange);
|
||||
normalCooldown = Mathf.Max(0f, basicCooldown);
|
||||
normalAction = basicAction;
|
||||
specialDamage = Mathf.Max(0f, abilityDamage);
|
||||
specialRadius = Mathf.Max(0f, abilityRadius);
|
||||
specialManaCost = Mathf.Max(0f, abilityManaCost);
|
||||
specialCooldown = Mathf.Max(0f, abilityCooldown);
|
||||
specialAction = abilityAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c84b6b014f9549e2a2d94b59016ca3bb
|
||||
@@ -12,6 +12,7 @@ namespace Northbound.Gameplay.Waves
|
||||
[SerializeField] private Material gruntMaterial;
|
||||
[SerializeField] private Material raiderMaterial;
|
||||
[SerializeField] private Material eliteMaterial;
|
||||
[SerializeField] private EnemyPursuitDefinition pursuitDefinition;
|
||||
|
||||
private float nextWaveAt;
|
||||
private int wave;
|
||||
@@ -41,11 +42,12 @@ namespace Northbound.Gameplay.Waves
|
||||
PublishStatus(state.ResonanceSnapshot);
|
||||
}
|
||||
|
||||
public void Configure(Material grunt, Material raider, Material elite)
|
||||
public void Configure(Material grunt, Material raider, Material elite, EnemyPursuitDefinition pursuit)
|
||||
{
|
||||
gruntMaterial = grunt;
|
||||
raiderMaterial = raider;
|
||||
eliteMaterial = elite;
|
||||
pursuitDefinition = pursuit;
|
||||
}
|
||||
|
||||
private void BeginWave()
|
||||
@@ -97,7 +99,7 @@ namespace Northbound.Gameplay.Waves
|
||||
EnemyRole.Elite => eliteMaterial,
|
||||
_ => gruntMaterial
|
||||
};
|
||||
enemy.AddComponent<EnemyAgent>().Configure(role, resonance, material);
|
||||
enemy.AddComponent<EnemyAgent>().Configure(role, resonance, material, pursuitDefinition);
|
||||
}
|
||||
|
||||
private void PublishStatus(int resonance)
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.World
|
||||
{
|
||||
public sealed class CoreObjective : MonoBehaviour, IDamageable
|
||||
public sealed class CoreObjective : MonoBehaviour, IDamageable, IEnemyStructureTarget
|
||||
{
|
||||
[SerializeField] private BuildingDefinition buildingDefinition;
|
||||
[SerializeField] private float defenseRange = 10f;
|
||||
@@ -19,6 +19,8 @@ namespace Northbound.Gameplay.World
|
||||
public bool IsAlive => CurrentHealth > 0f;
|
||||
public float CurrentHealth { get; private set; }
|
||||
public float MaximumHealth => buildingDefinition == null ? 1f : buildingDefinition.MaximumHealth;
|
||||
public bool CanBeTargetedByEnemies => IsAlive;
|
||||
public IDamageable DamageReceiver => this;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.World
|
||||
{
|
||||
public sealed class CrystalOutpost : MonoBehaviour, IDamageable, IInteractable
|
||||
public sealed class CrystalOutpost : MonoBehaviour, IDamageable, IInteractable, IEnemyStructureTarget
|
||||
{
|
||||
[SerializeField] private BuildingDefinition buildingDefinition;
|
||||
[SerializeField] private float manaPerSecond = 1.25f;
|
||||
@@ -27,6 +27,8 @@ namespace Northbound.Gameplay.World
|
||||
public bool CanInteract => !IsCaptured;
|
||||
public float CurrentHealth { get; private set; }
|
||||
public float MaximumHealth => buildingDefinition == null ? 1f : buildingDefinition.MaximumHealth;
|
||||
public bool CanBeTargetedByEnemies => IsCaptured && IsAlive;
|
||||
public IDamageable DamageReceiver => this;
|
||||
|
||||
private float defenseReadyAt;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Northbound.Gameplay.World
|
||||
Allied
|
||||
}
|
||||
|
||||
public sealed class PortalSite : MonoBehaviour, IDamageable, IInteractable
|
||||
public sealed class PortalSite : MonoBehaviour, IDamageable, IInteractable, IEnemyStructureTarget
|
||||
{
|
||||
[SerializeField] private int networkIndex;
|
||||
[SerializeField] private PortalState state = PortalState.Enemy;
|
||||
@@ -36,6 +36,8 @@ namespace Northbound.Gameplay.World
|
||||
public bool CanInteract => state != PortalState.Enemy;
|
||||
public float CurrentHealth { get; private set; }
|
||||
public float MaximumHealth => buildingDefinition == null ? 1f : buildingDefinition.MaximumHealth;
|
||||
public bool CanBeTargetedByEnemies => state == PortalState.Allied && IsAlive;
|
||||
public IDamageable DamageReceiver => this;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -70,7 +72,9 @@ namespace Northbound.Gameplay.World
|
||||
|
||||
public void TakeDamage(float amount, Team sourceTeam)
|
||||
{
|
||||
if (state != PortalState.Enemy || sourceTeam != Team.Player)
|
||||
var playerAttackingEnemyPortal = state == PortalState.Enemy && sourceTeam == Team.Player;
|
||||
var enemyAttackingAlliedPortal = state == PortalState.Allied && sourceTeam == Team.Enemy;
|
||||
if (!playerAttackingEnemyPortal && !enemyAttackingAlliedPortal)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -85,7 +89,7 @@ namespace Northbound.Gameplay.World
|
||||
RefreshVisual();
|
||||
|
||||
var gameState = SharedGameState.Instance;
|
||||
if (gameState != null && gameState.CountEnemyPortals() == 0)
|
||||
if (playerAttackingEnemyPortal && gameState != null && gameState.CountEnemyPortals() == 0)
|
||||
{
|
||||
gameState.MarkVictory();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,6 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Defense.TowerDefinition
|
||||
building: {fileID: 11400000, guid: e49a0dcf744247b8a77b63f797a21c4c, type: 2}
|
||||
range: 8
|
||||
damage: 9
|
||||
damage: 6
|
||||
attackInterval: 1.2
|
||||
attackColor: {r: 1, g: 0.75, b: 0.15, a: 0.95}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
%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: 9aa8e0cc71be49c9ac69d950aa3fe0d4, type: 3}
|
||||
m_Name: DefaultEnemyPursuit
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Enemy.EnemyPursuitDefinition
|
||||
aggroRange: 7
|
||||
disengageDelay: 4.5
|
||||
maximumUnansweredPursuitDistance: 10
|
||||
rearmDistance: 9
|
||||
maximumTargetBacktrackDistance: 3.5
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4ec708d3ab44238953e3bec1af6bb8e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,36 @@
|
||||
%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: c84b6b014f9549e2a2d94b59016ca3bb, type: 3}
|
||||
m_Name: PrototypePlayerDefinition
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Player.PlayerDefinition
|
||||
displayName: Prototype Player
|
||||
maximumHealth: 100
|
||||
moveSpeed: 4.2
|
||||
horizontalLimit: 15
|
||||
interactionRadius: 3.2
|
||||
normalDamage: 13
|
||||
normalRange: 3
|
||||
normalCooldown: 0.4
|
||||
normalAction:
|
||||
actionName: Normal Attack
|
||||
duration: 0.3
|
||||
allowsMovement: 0
|
||||
canBeCancelled: 0
|
||||
specialDamage: 34
|
||||
specialRadius: 4.2
|
||||
specialManaCost: 25
|
||||
specialCooldown: 5.5
|
||||
specialAction:
|
||||
actionName: Special Attack
|
||||
duration: 0.65
|
||||
allowsMovement: 0
|
||||
canBeCancelled: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3d795b67fc44625a25501cdf16da914
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -157,7 +157,7 @@ MonoBehaviour:
|
||||
attackDamage: 15
|
||||
attackRange: 2
|
||||
attackInterval: 1.25
|
||||
playerAggroRange: 7
|
||||
pursuitDefinition: {fileID: 11400000, guid: d4ec708d3ab44238953e3bec1af6bb8e, type: 2}
|
||||
affectedByResonance: 0
|
||||
--- !u!23 &10466387
|
||||
MeshRenderer:
|
||||
@@ -292,7 +292,7 @@ MonoBehaviour:
|
||||
attackDamage: 15
|
||||
attackRange: 2
|
||||
attackInterval: 1.25
|
||||
playerAggroRange: 7
|
||||
pursuitDefinition: {fileID: 11400000, guid: d4ec708d3ab44238953e3bec1af6bb8e, type: 2}
|
||||
affectedByResonance: 0
|
||||
--- !u!23 &57707079
|
||||
MeshRenderer:
|
||||
@@ -879,9 +879,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 7e89beb034a288c45b59485f3ff66293, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Player.PlayerController
|
||||
moveSpeed: 5
|
||||
horizontalLimit: 15
|
||||
interactionRadius: 3.2
|
||||
definition: {fileID: 11400000, guid: a3d795b67fc44625a25501cdf16da914, type: 2}
|
||||
--- !u!114 &597054783
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -894,23 +892,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 01dc4a815b7e62c4580a7b5b60a0a05e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Player.PlayerCombat
|
||||
normalDamage: 13
|
||||
normalRange: 3
|
||||
normalCooldown: 0.4
|
||||
normalAction:
|
||||
actionName: Normal Attack
|
||||
duration: 0.3
|
||||
allowsMovement: 0
|
||||
canBeCancelled: 0
|
||||
specialDamage: 34
|
||||
specialRadius: 4.2
|
||||
specialManaCost: 25
|
||||
specialCooldown: 5.5
|
||||
specialAction:
|
||||
actionName: Special Attack
|
||||
duration: 0.65
|
||||
allowsMovement: 0
|
||||
canBeCancelled: 0
|
||||
definition: {fileID: 11400000, guid: a3d795b67fc44625a25501cdf16da914, type: 2}
|
||||
--- !u!114 &597054784
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1061,7 +1043,7 @@ MonoBehaviour:
|
||||
attackDamage: 15
|
||||
attackRange: 2
|
||||
attackInterval: 1.25
|
||||
playerAggroRange: 7
|
||||
pursuitDefinition: {fileID: 11400000, guid: d4ec708d3ab44238953e3bec1af6bb8e, type: 2}
|
||||
affectedByResonance: 0
|
||||
--- !u!23 &673355559
|
||||
MeshRenderer:
|
||||
@@ -1322,6 +1304,7 @@ MonoBehaviour:
|
||||
gruntMaterial: {fileID: 2100000, guid: 0c49d825c6397b14593263a8a25a95b0, type: 2}
|
||||
raiderMaterial: {fileID: 2100000, guid: 5cb05999871dcdb44b50f6506260527a, type: 2}
|
||||
eliteMaterial: {fileID: 2100000, guid: 00e28eb844e1d184a944503ae8ffd906, type: 2}
|
||||
pursuitDefinition: {fileID: 11400000, guid: d4ec708d3ab44238953e3bec1af6bb8e, type: 2}
|
||||
--- !u!114 &963263111
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -2536,7 +2519,7 @@ MonoBehaviour:
|
||||
attackDamage: 15
|
||||
attackRange: 2
|
||||
attackInterval: 1.25
|
||||
playerAggroRange: 7
|
||||
pursuitDefinition: {fileID: 11400000, guid: d4ec708d3ab44238953e3bec1af6bb8e, type: 2}
|
||||
affectedByResonance: 0
|
||||
--- !u!23 &1999748834
|
||||
MeshRenderer:
|
||||
@@ -2671,7 +2654,7 @@ MonoBehaviour:
|
||||
attackDamage: 15
|
||||
attackRange: 2
|
||||
attackInterval: 1.25
|
||||
playerAggroRange: 7
|
||||
pursuitDefinition: {fileID: 11400000, guid: d4ec708d3ab44238953e3bec1af6bb8e, type: 2}
|
||||
affectedByResonance: 0
|
||||
--- !u!23 &2057559070
|
||||
MeshRenderer:
|
||||
@@ -3198,7 +3181,7 @@ MonoBehaviour:
|
||||
attackDamage: 15
|
||||
attackRange: 2
|
||||
attackInterval: 1.25
|
||||
playerAggroRange: 7
|
||||
pursuitDefinition: {fileID: 11400000, guid: d4ec708d3ab44238953e3bec1af6bb8e, type: 2}
|
||||
affectedByResonance: 0
|
||||
--- !u!23 &2117110600
|
||||
MeshRenderer:
|
||||
|
||||
Reference in New Issue
Block a user