feat: deepen prototype combat and building systems
This commit is contained in:
@@ -1,5 +1,27 @@
|
||||
namespace Northbound.Core
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct PlayerActionDefinition
|
||||
{
|
||||
[UnityEngine.SerializeField] private string actionName;
|
||||
[UnityEngine.SerializeField] private float duration;
|
||||
[UnityEngine.SerializeField] private bool allowsMovement;
|
||||
[UnityEngine.SerializeField] private bool canBeCancelled;
|
||||
|
||||
public string ActionName => actionName;
|
||||
public float Duration => UnityEngine.Mathf.Max(0f, duration);
|
||||
public bool AllowsMovement => allowsMovement;
|
||||
public bool CanBeCancelled => canBeCancelled;
|
||||
|
||||
public PlayerActionDefinition(string name, float actionDuration, bool canMoveDuringAction, bool isCancellable = false)
|
||||
{
|
||||
actionName = name;
|
||||
duration = UnityEngine.Mathf.Max(0f, actionDuration);
|
||||
allowsMovement = canMoveDuringAction;
|
||||
canBeCancelled = isCancellable;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Team
|
||||
{
|
||||
Neutral,
|
||||
@@ -7,7 +29,13 @@ namespace Northbound.Core
|
||||
Enemy
|
||||
}
|
||||
|
||||
public interface IDamageable
|
||||
public interface IHealthReadable
|
||||
{
|
||||
float CurrentHealth { get; }
|
||||
float MaximumHealth { get; }
|
||||
}
|
||||
|
||||
public interface IDamageable : IHealthReadable
|
||||
{
|
||||
Team Team { get; }
|
||||
bool IsAlive { get; }
|
||||
@@ -17,8 +45,16 @@ namespace Northbound.Core
|
||||
public interface IInteractable
|
||||
{
|
||||
string GetInteractionPrompt();
|
||||
PlayerActionDefinition GetActionDefinition();
|
||||
bool CanInteract { get; }
|
||||
void Interact(UnityEngine.GameObject actor);
|
||||
bool CanStartInteraction(UnityEngine.GameObject actor);
|
||||
bool Interact(UnityEngine.GameObject actor);
|
||||
}
|
||||
|
||||
public interface IProgressiveInteraction
|
||||
{
|
||||
bool BeginInteraction(UnityEngine.GameObject actor);
|
||||
void CancelInteraction(UnityEngine.GameObject actor, float normalizedActionProgress);
|
||||
}
|
||||
|
||||
public static class TeamRules
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.IO;
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay;
|
||||
using Northbound.Gameplay.Buildings;
|
||||
using Northbound.Gameplay.Combat;
|
||||
using Northbound.Gameplay.Construction;
|
||||
using Northbound.Gameplay.Defense;
|
||||
using Northbound.Gameplay.Enemy;
|
||||
using Northbound.Gameplay.Player;
|
||||
@@ -18,11 +20,19 @@ 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 CoreBuildingPath = DefinitionDirectory + "/SouthernCoreBuilding.asset";
|
||||
private const string OutpostBuildingPath = DefinitionDirectory + "/CrystalOutpostBuilding.asset";
|
||||
private const string PortalBuildingPath = DefinitionDirectory + "/PortalBuilding.asset";
|
||||
private const string BasicTowerBuildingPath = DefinitionDirectory + "/BasicTowerBuilding.asset";
|
||||
private const string BasicTowerDefinitionPath = DefinitionDirectory + "/BasicTowerDefinition.asset";
|
||||
private const string BasicTowerConstructionPath = DefinitionDirectory + "/BasicTowerConstruction.asset";
|
||||
|
||||
[MenuItem("Northbound/Build Gameplay Prototype")]
|
||||
public static void BuildScene()
|
||||
{
|
||||
EnsureDirectory(MaterialDirectory);
|
||||
EnsureDirectory(DefinitionDirectory);
|
||||
|
||||
var ground = GetMaterial("Ground", new Color(0.11f, 0.15f, 0.14f));
|
||||
var lane = GetMaterial("Lane", new Color(0.18f, 0.24f, 0.22f));
|
||||
@@ -37,6 +47,12 @@ namespace Northbound.Editor
|
||||
var grunt = GetMaterial("EnemyGrunt", new Color(0.85f, 0.32f, 0.25f));
|
||||
var raider = GetMaterial("EnemyRaider", new Color(1f, 0.55f, 0.12f));
|
||||
var elite = GetMaterial("EnemyElite", new Color(0.75f, 0.12f, 0.75f));
|
||||
var coreBuilding = GetBuildingDefinition(CoreBuildingPath, 1000, "Southern Core", 1000f, core, PrimitiveType.Cube, new Vector3(8f, 4f, 5f));
|
||||
var outpostBuilding = GetBuildingDefinition(OutpostBuildingPath, 1100, "Crystal Outpost", 300f, crystalActive, PrimitiveType.Sphere, new Vector3(2f, 3f, 2f));
|
||||
var portalBuilding = GetBuildingDefinition(PortalBuildingPath, 1200, "Portal", 240f, alliedPortal, PrimitiveType.Cylinder, new Vector3(3f, 0.45f, 3f));
|
||||
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 scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
CreateEnvironment(ground, lane);
|
||||
@@ -49,13 +65,13 @@ namespace Northbound.Editor
|
||||
|
||||
var playerController = CreatePlayer(player);
|
||||
CreateCamera(playerController);
|
||||
CreateCore(core);
|
||||
CreateCore(core, coreBuilding);
|
||||
|
||||
var portalPositions = new[] { -68f, -25f, 20f, 68f };
|
||||
for (var index = 0; index < portalPositions.Length; index++)
|
||||
{
|
||||
var initialState = index == 0 ? PortalState.Allied : PortalState.Enemy;
|
||||
CreatePortal(index, portalPositions[index], initialState, enemyPortal, ruinedPortal, alliedPortal);
|
||||
CreatePortal(index, portalPositions[index], initialState, portalBuilding, enemyPortal, ruinedPortal, alliedPortal);
|
||||
if (initialState == PortalState.Enemy)
|
||||
{
|
||||
CreateGuardian(new Vector3(-3f, 1f, portalPositions[index] - 3f), grunt);
|
||||
@@ -63,9 +79,9 @@ namespace Northbound.Editor
|
||||
}
|
||||
}
|
||||
|
||||
CreateCrystal(new Vector3(-8f, 1f, -45f), crystalDormant, crystalActive, tower);
|
||||
CreateCrystal(new Vector3(8f, 1f, -3f), crystalDormant, crystalActive, tower);
|
||||
CreateCrystal(new Vector3(-7f, 1f, 42f), crystalDormant, crystalActive, tower);
|
||||
CreateCrystal(new Vector3(-8f, 1f, -45f), outpostBuilding, crystalDormant, crystalActive, basicTowerConstruction, basicTowerDefinition);
|
||||
CreateCrystal(new Vector3(8f, 1f, -3f), outpostBuilding, crystalDormant, crystalActive, basicTowerConstruction, basicTowerDefinition);
|
||||
CreateCrystal(new Vector3(-7f, 1f, 42f), outpostBuilding, crystalDormant, crystalActive, basicTowerConstruction, basicTowerDefinition);
|
||||
|
||||
EditorSceneManager.SaveScene(scene, ScenePath);
|
||||
EditorBuildSettings.scenes = new[] { new EditorBuildSettingsScene(ScenePath, true) };
|
||||
@@ -102,7 +118,8 @@ namespace Northbound.Editor
|
||||
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, 180f);
|
||||
gameObject.AddComponent<Health>().Configure(Team.Player, 100f);
|
||||
gameObject.AddComponent<PlayerActionController>();
|
||||
var controller = gameObject.AddComponent<PlayerController>();
|
||||
gameObject.AddComponent<PlayerCombat>();
|
||||
return controller;
|
||||
@@ -112,7 +129,7 @@ namespace Northbound.Editor
|
||||
{
|
||||
var cameraObject = new GameObject("Main Camera");
|
||||
cameraObject.tag = "MainCamera";
|
||||
cameraObject.transform.position = new Vector3(0f, 30f, -70f);
|
||||
cameraObject.transform.position = new Vector3(0f, 30f, -76f);
|
||||
cameraObject.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
var camera = cameraObject.AddComponent<Camera>();
|
||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
@@ -123,33 +140,40 @@ namespace Northbound.Editor
|
||||
cameraObject.AddComponent<VerticalCamera>().Configure(player);
|
||||
}
|
||||
|
||||
private static void CreateCore(Material material)
|
||||
private static void CreateCore(Material material, BuildingDefinition definition)
|
||||
{
|
||||
var gameObject = CreatePrimitive("Southern Core", PrimitiveType.Cube, new Vector3(0f, 2f, -84f), new Vector3(8f, 4f, 5f), material);
|
||||
gameObject.AddComponent<CoreObjective>();
|
||||
gameObject.AddComponent<CoreObjective>().Configure(definition);
|
||||
}
|
||||
|
||||
private static void CreatePortal(
|
||||
int index,
|
||||
float z,
|
||||
PortalState initialState,
|
||||
BuildingDefinition definition,
|
||||
Material enemy,
|
||||
Material ruined,
|
||||
Material allied)
|
||||
{
|
||||
var gameObject = CreatePrimitive($"Portal {index}", PrimitiveType.Cylinder, new Vector3(0f, 0.45f, z), new Vector3(3f, 0.45f, 3f), ruined);
|
||||
var renderer = gameObject.GetComponent<Renderer>();
|
||||
gameObject.AddComponent<PortalSite>().Configure(index, initialState, renderer, enemy, ruined, allied);
|
||||
gameObject.AddComponent<PortalSite>().Configure(index, initialState, definition, renderer, enemy, ruined, allied);
|
||||
}
|
||||
|
||||
private static void CreateCrystal(Vector3 position, Material dormant, Material active, Material tower)
|
||||
private static void CreateCrystal(
|
||||
Vector3 position,
|
||||
BuildingDefinition outpostDefinition,
|
||||
Material dormant,
|
||||
Material active,
|
||||
ConstructionDefinition constructionDefinition,
|
||||
TowerDefinition towerDefinition)
|
||||
{
|
||||
var gameObject = CreatePrimitive("Crystal Outpost", PrimitiveType.Sphere, position, new Vector3(2f, 3f, 2f), dormant);
|
||||
var outpost = gameObject.AddComponent<CrystalOutpost>();
|
||||
outpost.Configure(gameObject.GetComponent<Renderer>(), dormant, active);
|
||||
outpost.Configure(outpostDefinition, gameObject.GetComponent<Renderer>(), dormant, active);
|
||||
|
||||
var slot = CreatePrimitive("Defense Slot", PrimitiveType.Cube, position + new Vector3(3.3f, -0.65f, 0f), new Vector3(1.8f, 0.3f, 1.8f), dormant);
|
||||
slot.AddComponent<DefenseSlot>().Configure(outpost, tower);
|
||||
slot.AddComponent<DefenseSlot>().Configure(outpost, constructionDefinition, towerDefinition);
|
||||
}
|
||||
|
||||
private static void CreateGuardian(Vector3 position, Material material)
|
||||
@@ -184,6 +208,64 @@ namespace Northbound.Editor
|
||||
return material;
|
||||
}
|
||||
|
||||
private static BuildingDefinition GetBuildingDefinition(
|
||||
string path,
|
||||
int buildingId,
|
||||
string displayName,
|
||||
float maximumHealth,
|
||||
Material prototypeMaterial,
|
||||
PrimitiveType prototypePrimitive,
|
||||
Vector3 prototypeScale)
|
||||
{
|
||||
var definition = AssetDatabase.LoadAssetAtPath<BuildingDefinition>(path);
|
||||
if (definition != null)
|
||||
{
|
||||
return definition;
|
||||
}
|
||||
|
||||
definition = ScriptableObject.CreateInstance<BuildingDefinition>();
|
||||
definition.name = Path.GetFileNameWithoutExtension(path);
|
||||
definition.ConfigurePrototype(
|
||||
buildingId,
|
||||
displayName,
|
||||
maximumHealth,
|
||||
prototypeMaterial,
|
||||
prototypePrimitive,
|
||||
prototypeScale);
|
||||
AssetDatabase.CreateAsset(definition, path);
|
||||
return definition;
|
||||
}
|
||||
|
||||
private static TowerDefinition GetBasicTowerDefinition(BuildingDefinition building)
|
||||
{
|
||||
var definition = AssetDatabase.LoadAssetAtPath<TowerDefinition>(BasicTowerDefinitionPath);
|
||||
if (definition != null)
|
||||
{
|
||||
return definition;
|
||||
}
|
||||
|
||||
definition = ScriptableObject.CreateInstance<TowerDefinition>();
|
||||
definition.name = "BasicTowerDefinition";
|
||||
definition.ConfigurePrototype(building, 8f, 9f, 1.2f, new Color(1f, 0.75f, 0.15f, 0.95f));
|
||||
AssetDatabase.CreateAsset(definition, BasicTowerDefinitionPath);
|
||||
return definition;
|
||||
}
|
||||
|
||||
private static ConstructionDefinition GetBasicTowerConstruction(BuildingDefinition building)
|
||||
{
|
||||
var definition = AssetDatabase.LoadAssetAtPath<ConstructionDefinition>(BasicTowerConstructionPath);
|
||||
if (definition != null)
|
||||
{
|
||||
return definition;
|
||||
}
|
||||
|
||||
definition = ScriptableObject.CreateInstance<ConstructionDefinition>();
|
||||
definition.name = "BasicTowerConstruction";
|
||||
definition.ConfigurePrototype(building, 35f, 3f, false, true);
|
||||
AssetDatabase.CreateAsset(definition, BasicTowerConstructionPath);
|
||||
return definition;
|
||||
}
|
||||
|
||||
private static void EnsureDirectory(string assetPath)
|
||||
{
|
||||
var absolutePath = Path.GetFullPath(assetPath);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b02cc11687084bacaae00cc7963c9d14
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Buildings
|
||||
{
|
||||
[CreateAssetMenu(fileName = "BuildingDefinition", menuName = "Northbound/Building Definition")]
|
||||
public sealed class BuildingDefinition : ScriptableObject
|
||||
{
|
||||
[SerializeField] private int buildingId;
|
||||
[SerializeField] private string displayName = "Building";
|
||||
[SerializeField, TextArea] private string description;
|
||||
[SerializeField] private float maximumHealth = 100f;
|
||||
[SerializeField] private GameObject completedPrefab;
|
||||
[SerializeField] private GameObject constructionPreviewPrefab;
|
||||
[SerializeField] private Material prototypeMaterial;
|
||||
[SerializeField] private PrimitiveType prototypePrimitive = PrimitiveType.Cube;
|
||||
[SerializeField] private Vector3 prototypeScale = Vector3.one;
|
||||
|
||||
public int BuildingId => buildingId;
|
||||
public string DisplayName => displayName;
|
||||
public string Description => description;
|
||||
public float MaximumHealth => Mathf.Max(1f, maximumHealth);
|
||||
public GameObject CompletedPrefab => completedPrefab;
|
||||
public GameObject ConstructionPreviewPrefab => constructionPreviewPrefab;
|
||||
public Material PrototypeMaterial => prototypeMaterial;
|
||||
public PrimitiveType PrototypePrimitive => prototypePrimitive;
|
||||
public Vector3 PrototypeScale => prototypeScale;
|
||||
|
||||
public void ConfigurePrototype(
|
||||
int id,
|
||||
string definitionName,
|
||||
float health,
|
||||
Material material,
|
||||
PrimitiveType primitive,
|
||||
Vector3 scale)
|
||||
{
|
||||
buildingId = id;
|
||||
displayName = definitionName;
|
||||
maximumHealth = Mathf.Max(1f, health);
|
||||
prototypeMaterial = material;
|
||||
prototypePrimitive = primitive;
|
||||
prototypeScale = scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85b66c82f35d4c4c9626b843a50e12f1
|
||||
@@ -15,6 +15,8 @@ namespace Northbound.Gameplay.Combat
|
||||
public bool IsAlive => Current > 0f;
|
||||
public float Current { get; private set; }
|
||||
public float Maximum => maximum;
|
||||
public float CurrentHealth => Current;
|
||||
public float MaximumHealth => maximum;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76cb6d4e020445f28c20adf669b4f56f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,44 @@
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay.Buildings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Construction
|
||||
{
|
||||
[CreateAssetMenu(fileName = "ConstructionDefinition", menuName = "Northbound/Construction Definition")]
|
||||
public sealed class ConstructionDefinition : ScriptableObject
|
||||
{
|
||||
[SerializeField] private BuildingDefinition building;
|
||||
[SerializeField] private float manaCost = 35f;
|
||||
[SerializeField] private float buildDuration = 3f;
|
||||
[SerializeField] private bool allowsMovementDuringBuild;
|
||||
[SerializeField] private bool canCancelBuild = true;
|
||||
|
||||
public BuildingDefinition Building => building;
|
||||
public float ManaCost => Mathf.Max(0f, manaCost);
|
||||
public float BuildDuration => Mathf.Max(0.05f, buildDuration);
|
||||
|
||||
public PlayerActionDefinition CreateBuildAction(float currentProgress)
|
||||
{
|
||||
var remainingDuration = BuildDuration * (1f - Mathf.Clamp01(currentProgress));
|
||||
return new PlayerActionDefinition(
|
||||
$"Build {building.DisplayName}",
|
||||
Mathf.Max(0.05f, remainingDuration),
|
||||
allowsMovementDuringBuild,
|
||||
canCancelBuild);
|
||||
}
|
||||
|
||||
public void ConfigurePrototype(
|
||||
BuildingDefinition buildingDefinition,
|
||||
float cost,
|
||||
float duration,
|
||||
bool allowsMovement,
|
||||
bool canCancel)
|
||||
{
|
||||
building = buildingDefinition;
|
||||
manaCost = Mathf.Max(0f, cost);
|
||||
buildDuration = Mathf.Max(0.05f, duration);
|
||||
allowsMovementDuringBuild = allowsMovement;
|
||||
canCancelBuild = canCancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ca73b95ed924661b0f122f634acb09f
|
||||
@@ -1,26 +1,26 @@
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay.Combat;
|
||||
using Northbound.Gameplay.Enemy;
|
||||
using Northbound.Gameplay.Presentation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Defense
|
||||
{
|
||||
public sealed class BasicTower : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float range = 8f;
|
||||
[SerializeField] private float damage = 9f;
|
||||
[SerializeField] private float attackInterval = 1.2f;
|
||||
[SerializeField] private TowerDefinition definition;
|
||||
|
||||
private float readyAt;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Time.time < readyAt)
|
||||
if (definition == null || Time.time < readyAt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyAgent nearest = null;
|
||||
var nearestDistance = range * range;
|
||||
var nearestDistance = definition.Range * definition.Range;
|
||||
foreach (var enemy in FindObjectsByType<EnemyAgent>())
|
||||
{
|
||||
var distance = Vector3.SqrMagnitude(enemy.transform.position - transform.position);
|
||||
@@ -36,8 +36,26 @@ namespace Northbound.Gameplay.Defense
|
||||
return;
|
||||
}
|
||||
|
||||
readyAt = Time.time + attackInterval;
|
||||
nearest.TakeDamage(damage, Team.Player);
|
||||
readyAt = Time.time + definition.AttackInterval;
|
||||
AttackTracer.Spawn(transform.position, nearest.transform.position, definition.AttackColor, 0.16f, 0.18f);
|
||||
nearest.TakeDamage(definition.Damage, Team.Player);
|
||||
}
|
||||
|
||||
public void Configure(TowerDefinition towerDefinition)
|
||||
{
|
||||
definition = towerDefinition;
|
||||
if (definition?.Building == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var health = GetComponent<Health>();
|
||||
if (health == null)
|
||||
{
|
||||
health = gameObject.AddComponent<Health>();
|
||||
}
|
||||
|
||||
health.Configure(Team.Player, definition.Building.MaximumHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,197 @@
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay.Construction;
|
||||
using Northbound.Gameplay.World;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Defense
|
||||
{
|
||||
public sealed class DefenseSlot : MonoBehaviour, IInteractable
|
||||
public sealed class DefenseSlot : MonoBehaviour, IInteractable, IProgressiveInteraction
|
||||
{
|
||||
[SerializeField] private CrystalOutpost outpost;
|
||||
[SerializeField] private float buildCost = 35f;
|
||||
[SerializeField] private Material towerMaterial;
|
||||
[SerializeField] private ConstructionDefinition constructionDefinition;
|
||||
[SerializeField] private TowerDefinition towerDefinition;
|
||||
|
||||
public bool CanInteract => outpost != null && outpost.IsCaptured && GetComponentInChildren<BasicTower>() == null;
|
||||
private float buildProgress;
|
||||
private bool constructionFunded;
|
||||
private bool isActivelyBuilding;
|
||||
private float activeBuildStartedAt;
|
||||
private float activeBuildStartProgress;
|
||||
private float activeBuildDuration;
|
||||
private GameObject constructionPreview;
|
||||
|
||||
public void Configure(CrystalOutpost owner, Material material)
|
||||
public bool CanInteract => constructionDefinition != null
|
||||
&& constructionDefinition.Building != null
|
||||
&& towerDefinition != null
|
||||
&& towerDefinition.Building == constructionDefinition.Building
|
||||
&& outpost != null
|
||||
&& outpost.IsCaptured
|
||||
&& GetComponentInChildren<BasicTower>() == null;
|
||||
|
||||
public void Configure(
|
||||
CrystalOutpost owner,
|
||||
ConstructionDefinition construction,
|
||||
TowerDefinition tower)
|
||||
{
|
||||
outpost = owner;
|
||||
towerMaterial = material;
|
||||
constructionDefinition = construction;
|
||||
towerDefinition = tower;
|
||||
}
|
||||
|
||||
public string GetInteractionPrompt()
|
||||
private void Update()
|
||||
{
|
||||
return $"Build delaying tower ({buildCost:0} mana)";
|
||||
}
|
||||
|
||||
public void Interact(GameObject actor)
|
||||
{
|
||||
var gameState = SharedGameState.Instance;
|
||||
if (!CanInteract || gameState == null || !gameState.TrySpendMana(buildCost))
|
||||
if (!isActivelyBuilding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var towerObject = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
||||
towerObject.name = "Basic Tower";
|
||||
towerObject.transform.SetParent(transform, false);
|
||||
towerObject.transform.localPosition = Vector3.up * 1.25f;
|
||||
towerObject.transform.localScale = new Vector3(0.7f, 1.25f, 0.7f);
|
||||
towerObject.GetComponent<Renderer>().sharedMaterial = towerMaterial;
|
||||
towerObject.AddComponent<BasicTower>();
|
||||
var actionProgress = Mathf.Clamp01((Time.time - activeBuildStartedAt) / activeBuildDuration);
|
||||
buildProgress = Mathf.Lerp(activeBuildStartProgress, 1f, actionProgress);
|
||||
RefreshConstructionPreview();
|
||||
}
|
||||
|
||||
public string GetInteractionPrompt()
|
||||
{
|
||||
return constructionFunded
|
||||
? $"Resume {constructionDefinition.Building.DisplayName} ({buildProgress * 100f:0}%)"
|
||||
: $"Build {constructionDefinition.Building.DisplayName} ({constructionDefinition.ManaCost:0} mana)";
|
||||
}
|
||||
|
||||
public PlayerActionDefinition GetActionDefinition()
|
||||
{
|
||||
return constructionDefinition.CreateBuildAction(buildProgress);
|
||||
}
|
||||
|
||||
public bool CanStartInteraction(GameObject actor)
|
||||
{
|
||||
var gameState = SharedGameState.Instance;
|
||||
return CanInteract
|
||||
&& gameState != null
|
||||
&& (constructionFunded || gameState.Mana + 0.001f >= constructionDefinition.ManaCost);
|
||||
}
|
||||
|
||||
public bool BeginInteraction(GameObject actor)
|
||||
{
|
||||
var gameState = SharedGameState.Instance;
|
||||
if (!CanInteract || gameState == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!constructionFunded)
|
||||
{
|
||||
if (!gameState.TrySpendMana(constructionDefinition.ManaCost))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constructionFunded = true;
|
||||
}
|
||||
|
||||
activeBuildStartProgress = buildProgress;
|
||||
activeBuildDuration = Mathf.Max(0.05f, constructionDefinition.BuildDuration * (1f - buildProgress));
|
||||
activeBuildStartedAt = Time.time;
|
||||
isActivelyBuilding = true;
|
||||
EnsureConstructionPreview();
|
||||
RefreshConstructionPreview();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CancelInteraction(GameObject actor, float normalizedActionProgress)
|
||||
{
|
||||
buildProgress = Mathf.Max(
|
||||
buildProgress,
|
||||
Mathf.Lerp(activeBuildStartProgress, 1f, Mathf.Clamp01(normalizedActionProgress)));
|
||||
isActivelyBuilding = false;
|
||||
RefreshConstructionPreview();
|
||||
}
|
||||
|
||||
public bool Interact(GameObject actor)
|
||||
{
|
||||
if (!CanInteract || !constructionFunded)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buildProgress = 1f;
|
||||
isActivelyBuilding = false;
|
||||
if (constructionPreview != null)
|
||||
{
|
||||
Destroy(constructionPreview);
|
||||
constructionPreview = null;
|
||||
}
|
||||
|
||||
var towerObject = CreateCompletedObject();
|
||||
var tower = towerObject.GetComponent<BasicTower>();
|
||||
if (tower == null)
|
||||
{
|
||||
tower = towerObject.AddComponent<BasicTower>();
|
||||
}
|
||||
|
||||
tower.Configure(towerDefinition);
|
||||
|
||||
constructionFunded = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EnsureConstructionPreview()
|
||||
{
|
||||
if (constructionPreview != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var building = constructionDefinition.Building;
|
||||
constructionPreview = building.ConstructionPreviewPrefab != null
|
||||
? Instantiate(building.ConstructionPreviewPrefab, transform)
|
||||
: GameObject.CreatePrimitive(building.PrototypePrimitive);
|
||||
constructionPreview.name = "Tower Construction";
|
||||
constructionPreview.transform.SetParent(transform, false);
|
||||
if (constructionPreview.TryGetComponent<Renderer>(out var renderer))
|
||||
{
|
||||
renderer.sharedMaterial = building.PrototypeMaterial;
|
||||
}
|
||||
|
||||
if (constructionPreview.TryGetComponent<Collider>(out var collider))
|
||||
{
|
||||
collider.enabled = false;
|
||||
Destroy(collider);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshConstructionPreview()
|
||||
{
|
||||
if (constructionPreview == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var finalScale = constructionDefinition.Building.PrototypeScale;
|
||||
var height = Mathf.Lerp(0.08f, finalScale.y, buildProgress);
|
||||
constructionPreview.transform.localPosition = Vector3.up * height;
|
||||
constructionPreview.transform.localScale = new Vector3(finalScale.x, height, finalScale.z);
|
||||
}
|
||||
|
||||
private GameObject CreateCompletedObject()
|
||||
{
|
||||
var building = constructionDefinition.Building;
|
||||
if (building.CompletedPrefab != null)
|
||||
{
|
||||
var completed = Instantiate(building.CompletedPrefab, transform);
|
||||
completed.name = building.DisplayName;
|
||||
return completed;
|
||||
}
|
||||
|
||||
var prototype = GameObject.CreatePrimitive(building.PrototypePrimitive);
|
||||
prototype.name = building.DisplayName;
|
||||
prototype.transform.SetParent(transform, false);
|
||||
prototype.transform.localPosition = Vector3.up * building.PrototypeScale.y;
|
||||
prototype.transform.localScale = building.PrototypeScale;
|
||||
if (prototype.TryGetComponent<Renderer>(out var renderer))
|
||||
{
|
||||
renderer.sharedMaterial = building.PrototypeMaterial;
|
||||
}
|
||||
|
||||
return prototype;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Northbound.Gameplay.Buildings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Defense
|
||||
{
|
||||
[CreateAssetMenu(fileName = "TowerDefinition", menuName = "Northbound/Tower Definition")]
|
||||
public sealed class TowerDefinition : ScriptableObject
|
||||
{
|
||||
[SerializeField] private BuildingDefinition building;
|
||||
[SerializeField] private float range = 8f;
|
||||
[SerializeField] private float damage = 9f;
|
||||
[SerializeField] private float attackInterval = 1.2f;
|
||||
[SerializeField] private Color attackColor = new(1f, 0.75f, 0.15f, 0.95f);
|
||||
|
||||
public BuildingDefinition Building => building;
|
||||
public float Range => Mathf.Max(0f, range);
|
||||
public float Damage => Mathf.Max(0f, damage);
|
||||
public float AttackInterval => Mathf.Max(0.05f, attackInterval);
|
||||
public Color AttackColor => attackColor;
|
||||
|
||||
public void ConfigurePrototype(
|
||||
BuildingDefinition buildingDefinition,
|
||||
float attackRange,
|
||||
float attackDamage,
|
||||
float interval,
|
||||
Color tracerColor)
|
||||
{
|
||||
building = buildingDefinition;
|
||||
range = Mathf.Max(0f, attackRange);
|
||||
damage = Mathf.Max(0f, attackDamage);
|
||||
attackInterval = Mathf.Max(0.05f, interval);
|
||||
attackColor = tracerColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8cf5bfa976c4a68b37f6a2f2b9c10d6
|
||||
@@ -1,5 +1,6 @@
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay.Player;
|
||||
using Northbound.Gameplay.Presentation;
|
||||
using Northbound.Gameplay.World;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -15,6 +16,8 @@ namespace Northbound.Gameplay.Enemy
|
||||
|
||||
public sealed class EnemyAgent : MonoBehaviour, IDamageable
|
||||
{
|
||||
private static int nextAvoidanceSide;
|
||||
|
||||
[SerializeField] private EnemyRole role;
|
||||
[SerializeField] private float maximumHealth = 75f;
|
||||
[SerializeField] private float moveSpeed = 2.7f;
|
||||
@@ -27,15 +30,21 @@ namespace Northbound.Gameplay.Enemy
|
||||
private float currentHealth;
|
||||
private float readyAt;
|
||||
private Vector3 guardPosition;
|
||||
private CharacterController movementController;
|
||||
private float avoidanceDirection;
|
||||
|
||||
public EnemyRole Role => role;
|
||||
public Team Team => Team.Enemy;
|
||||
public bool IsAlive => currentHealth > 0f;
|
||||
public float CurrentHealth => currentHealth;
|
||||
public float MaximumHealth => maximumHealth;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
currentHealth = maximumHealth;
|
||||
guardPosition = transform.position;
|
||||
avoidanceDirection = (nextAvoidanceSide++ & 1) == 0 ? 1f : -1f;
|
||||
PrepareMovementController();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -51,19 +60,19 @@ namespace Northbound.Gameplay.Enemy
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPosition = target.transform.position;
|
||||
var targetPosition = GetClosestTargetPoint(target);
|
||||
var planar = targetPosition - transform.position;
|
||||
planar.y = 0f;
|
||||
if (planar.sqrMagnitude > attackRange * attackRange)
|
||||
{
|
||||
transform.position += planar.normalized * (moveSpeed * Time.deltaTime);
|
||||
transform.rotation = Quaternion.LookRotation(planar.normalized);
|
||||
MoveTowardsTarget(planar.normalized);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Time.time >= readyAt && target is IDamageable damageable)
|
||||
{
|
||||
readyAt = Time.time + attackInterval;
|
||||
AttackTracer.Spawn(transform.position, targetPosition, new Color(1f, 0.18f, 0.08f, 0.95f), 0.22f, 0.2f);
|
||||
damageable.TakeDamage(attackDamage, Team.Enemy);
|
||||
}
|
||||
}
|
||||
@@ -103,6 +112,68 @@ namespace Northbound.Gameplay.Enemy
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareMovementController()
|
||||
{
|
||||
foreach (var collider in GetComponents<Collider>())
|
||||
{
|
||||
if (collider is CharacterController)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
collider.enabled = false;
|
||||
Destroy(collider);
|
||||
}
|
||||
|
||||
movementController = GetComponent<CharacterController>();
|
||||
if (movementController == null)
|
||||
{
|
||||
movementController = gameObject.AddComponent<CharacterController>();
|
||||
}
|
||||
|
||||
movementController.center = Vector3.zero;
|
||||
movementController.height = 2f;
|
||||
movementController.radius = 0.45f;
|
||||
movementController.stepOffset = 0.3f;
|
||||
movementController.skinWidth = 0.08f;
|
||||
movementController.minMoveDistance = 0f;
|
||||
movementController.detectCollisions = true;
|
||||
movementController.enableOverlapRecovery = true;
|
||||
}
|
||||
|
||||
private Vector3 GetClosestTargetPoint(MonoBehaviour target)
|
||||
{
|
||||
var targetCollider = target.GetComponent<Collider>();
|
||||
if (targetCollider == null)
|
||||
{
|
||||
targetCollider = target.GetComponentInChildren<Collider>();
|
||||
}
|
||||
|
||||
var targetPoint = targetCollider == null
|
||||
? target.transform.position
|
||||
: targetCollider.ClosestPoint(transform.position);
|
||||
targetPoint.y = transform.position.y;
|
||||
return targetPoint;
|
||||
}
|
||||
|
||||
private void MoveTowardsTarget(Vector3 direction)
|
||||
{
|
||||
if (movementController == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var step = moveSpeed * Time.deltaTime;
|
||||
var collision = movementController.Move(direction * step);
|
||||
if ((collision & CollisionFlags.Sides) != 0)
|
||||
{
|
||||
var sideways = Vector3.Cross(Vector3.up, direction) * avoidanceDirection;
|
||||
movementController.Move(sideways * (step * 0.85f));
|
||||
}
|
||||
|
||||
transform.rotation = Quaternion.LookRotation(direction);
|
||||
}
|
||||
|
||||
private MonoBehaviour SelectTarget()
|
||||
{
|
||||
var player = FindAnyObjectByType<PlayerController>();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using Northbound.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Player
|
||||
{
|
||||
public sealed class PlayerActionController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private KeyCode cancelKey = KeyCode.Escape;
|
||||
|
||||
private PlayerActionDefinition currentAction;
|
||||
private float actionEndsAt;
|
||||
private bool hasActiveAction;
|
||||
private Action completion;
|
||||
private Action<float> cancellation;
|
||||
|
||||
public bool IsActing => hasActiveAction;
|
||||
public bool CanStartAction => !IsActing;
|
||||
public bool CanMove => !IsActing || currentAction.AllowsMovement;
|
||||
public string CurrentActionName => IsActing ? currentAction.ActionName : string.Empty;
|
||||
public bool CurrentActionCanBeCancelled => IsActing && currentAction.CanBeCancelled;
|
||||
public float RemainingTime => IsActing ? Mathf.Max(0f, actionEndsAt - Time.time) : 0f;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsActing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentAction.CanBeCancelled && Input.GetKeyDown(cancelKey))
|
||||
{
|
||||
TryCancelCurrentAction();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Time.time >= actionEndsAt)
|
||||
{
|
||||
CompleteCurrentAction();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryBeginAction(PlayerActionDefinition action, Action onCompleted = null, Action<float> onCancelled = null)
|
||||
{
|
||||
if (!CanStartAction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
currentAction = action;
|
||||
actionEndsAt = Time.time + action.Duration;
|
||||
completion = onCompleted;
|
||||
cancellation = onCancelled;
|
||||
hasActiveAction = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryCancelCurrentAction()
|
||||
{
|
||||
if (!CurrentActionCanBeCancelled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var callback = cancellation;
|
||||
var progress = currentAction.Duration <= 0f
|
||||
? 1f
|
||||
: Mathf.Clamp01(1f - RemainingTime / currentAction.Duration);
|
||||
ClearCurrentAction();
|
||||
callback?.Invoke(progress);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CompleteCurrentAction()
|
||||
{
|
||||
var callback = completion;
|
||||
ClearCurrentAction();
|
||||
callback?.Invoke();
|
||||
}
|
||||
|
||||
private void ClearCurrentAction()
|
||||
{
|
||||
hasActiveAction = false;
|
||||
actionEndsAt = 0f;
|
||||
currentAction = default;
|
||||
completion = null;
|
||||
cancellation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e697b8ae03934a50b1366870e44fdc0e
|
||||
@@ -1,31 +1,44 @@
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay.Presentation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Player
|
||||
{
|
||||
public sealed class PlayerCombat : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float normalDamage = 26f;
|
||||
[SerializeField] private float normalRange = 3.2f;
|
||||
[SerializeField] private float normalCooldown = 0.32f;
|
||||
[SerializeField] private float specialDamage = 58f;
|
||||
[SerializeField] private float specialRadius = 4.8f;
|
||||
[SerializeField] private float specialManaCost = 20f;
|
||||
[SerializeField] private float specialCooldown = 4f;
|
||||
[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);
|
||||
|
||||
private float normalReadyAt;
|
||||
private float specialReadyAt;
|
||||
private PlayerActionController actionController;
|
||||
|
||||
public float SpecialCooldownRemaining => Mathf.Max(0f, specialReadyAt - Time.time);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
actionController = GetComponent<PlayerActionController>();
|
||||
if (actionController == null)
|
||||
{
|
||||
actionController = gameObject.AddComponent<PlayerActionController>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetMouseButtonDown(0) && Time.time >= normalReadyAt)
|
||||
if (Input.GetMouseButtonDown(0) && Time.time >= normalReadyAt && actionController is { CanStartAction: true })
|
||||
{
|
||||
NormalAttack();
|
||||
}
|
||||
|
||||
if (Input.GetMouseButtonDown(1) && Time.time >= specialReadyAt)
|
||||
if (Input.GetMouseButtonDown(1) && Time.time >= specialReadyAt && actionController is { CanStartAction: true })
|
||||
{
|
||||
SpecialAttack();
|
||||
}
|
||||
@@ -33,20 +46,29 @@ namespace Northbound.Gameplay.Player
|
||||
|
||||
private void NormalAttack()
|
||||
{
|
||||
if (!actionController.TryBeginAction(normalAction))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
normalReadyAt = Time.time + normalCooldown;
|
||||
var center = transform.position + transform.forward * (normalRange * 0.55f);
|
||||
DamageTargets(Physics.OverlapSphere(center, normalRange * 0.55f), normalDamage);
|
||||
var radius = normalRange * 0.55f;
|
||||
AttackFlash.Spawn(center, radius, new Color(0.15f, 0.85f, 1f, 0.72f), 0.16f);
|
||||
DamageTargets(Physics.OverlapSphere(center, radius), normalDamage);
|
||||
}
|
||||
|
||||
private void SpecialAttack()
|
||||
{
|
||||
var gameState = SharedGameState.Instance;
|
||||
if (gameState == null || !gameState.TrySpendMana(specialManaCost))
|
||||
if (actionController == null || !actionController.CanStartAction || gameState == null || !gameState.TrySpendMana(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,31 +4,49 @@ using UnityEngine;
|
||||
namespace Northbound.Gameplay.Player
|
||||
{
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
[RequireComponent(typeof(PlayerActionController))]
|
||||
[DefaultExecutionOrder(-100)]
|
||||
public sealed class PlayerController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float moveSpeed = 8f;
|
||||
[SerializeField] private float moveSpeed = 5f;
|
||||
[SerializeField] private float horizontalLimit = 15f;
|
||||
[SerializeField] private float interactionRadius = 3.2f;
|
||||
|
||||
private CharacterController characterController;
|
||||
private PlayerActionController actionController;
|
||||
|
||||
public Vector3 LastMoveDirection { get; private set; } = Vector3.forward;
|
||||
public string NearbyPrompt { get; private set; } = string.Empty;
|
||||
public PlayerActionController Actions => actionController;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
characterController = GetComponent<CharacterController>();
|
||||
actionController = GetComponent<PlayerActionController>();
|
||||
if (actionController == null)
|
||||
{
|
||||
actionController = gameObject.AddComponent<PlayerActionController>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
Move();
|
||||
FaceMouse();
|
||||
UpdateInteraction();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
Move();
|
||||
}
|
||||
|
||||
private void Move()
|
||||
{
|
||||
if (actionController != null && !actionController.CanMove)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
|
||||
if (input.sqrMagnitude > 1f)
|
||||
{
|
||||
@@ -94,9 +112,33 @@ namespace Northbound.Gameplay.Player
|
||||
}
|
||||
|
||||
NearbyPrompt = nearest == null ? string.Empty : $"E: {nearest.GetInteractionPrompt()}";
|
||||
if (nearest != null && Input.GetKeyDown(KeyCode.E))
|
||||
if (nearest != null && Input.GetKeyDown(KeyCode.E) && actionController is { CanStartAction: true })
|
||||
{
|
||||
nearest.Interact(gameObject);
|
||||
var action = nearest.GetActionDefinition();
|
||||
if (nearest.CanStartInteraction(gameObject))
|
||||
{
|
||||
var interaction = nearest;
|
||||
var interactionBehaviour = nearest as MonoBehaviour;
|
||||
var progressive = nearest as IProgressiveInteraction;
|
||||
if (progressive != null && !progressive.BeginInteraction(gameObject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!actionController.TryBeginAction(
|
||||
action,
|
||||
() =>
|
||||
{
|
||||
if (interactionBehaviour != null)
|
||||
{
|
||||
interaction.Interact(gameObject);
|
||||
}
|
||||
},
|
||||
progress => progressive?.CancelInteraction(gameObject, progress)))
|
||||
{
|
||||
progressive?.CancelInteraction(gameObject, 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace Northbound.Gameplay.Presentation
|
||||
{
|
||||
public sealed class AttackFlash : MonoBehaviour
|
||||
{
|
||||
private Material flashMaterial;
|
||||
private Color flashColor;
|
||||
private float lifetime;
|
||||
private float diameter;
|
||||
private float startedAt;
|
||||
|
||||
public static void Spawn(Vector3 center, float radius, Color color, float duration)
|
||||
{
|
||||
var flash = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
||||
flash.name = "Attack Flash";
|
||||
flash.transform.position = new Vector3(center.x, 0.12f, center.z);
|
||||
|
||||
if (flash.TryGetComponent<Collider>(out var collider))
|
||||
{
|
||||
Destroy(collider);
|
||||
}
|
||||
|
||||
var renderer = flash.GetComponent<Renderer>();
|
||||
renderer.shadowCastingMode = ShadowCastingMode.Off;
|
||||
renderer.receiveShadows = false;
|
||||
|
||||
var shader = Shader.Find("Sprites/Default") ?? Shader.Find("Unlit/Color") ?? Shader.Find("Standard");
|
||||
var material = new Material(shader)
|
||||
{
|
||||
color = color
|
||||
};
|
||||
renderer.sharedMaterial = material;
|
||||
|
||||
flash.AddComponent<AttackFlash>().Configure(material, radius, color, duration);
|
||||
}
|
||||
|
||||
private void Configure(Material material, float radius, Color color, float duration)
|
||||
{
|
||||
flashMaterial = material;
|
||||
flashColor = color;
|
||||
lifetime = Mathf.Max(0.05f, duration);
|
||||
diameter = Mathf.Max(0.1f, radius * 2f);
|
||||
startedAt = Time.time;
|
||||
ApplyVisual(0f);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var progress = Mathf.Clamp01((Time.time - startedAt) / lifetime);
|
||||
ApplyVisual(progress);
|
||||
if (progress >= 1f)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (flashMaterial != null)
|
||||
{
|
||||
Destroy(flashMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyVisual(float progress)
|
||||
{
|
||||
var scale = Mathf.Lerp(0.65f, 1.05f, progress);
|
||||
transform.localScale = new Vector3(diameter * scale, 0.035f, diameter * scale);
|
||||
|
||||
if (flashMaterial != null)
|
||||
{
|
||||
var color = flashColor;
|
||||
color.a *= 1f - progress;
|
||||
flashMaterial.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 217dbed02e4f4b19884bfd2764d1e2bd
|
||||
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.Presentation
|
||||
{
|
||||
public sealed class AttackTracer : MonoBehaviour
|
||||
{
|
||||
private LineRenderer line;
|
||||
private Material tracerMaterial;
|
||||
private Color tracerColor;
|
||||
private float initialWidth;
|
||||
private float lifetime;
|
||||
private float startedAt;
|
||||
|
||||
public static void Spawn(Vector3 origin, Vector3 target, Color color, float width, float duration)
|
||||
{
|
||||
var tracerObject = new GameObject("Attack Tracer");
|
||||
var tracer = tracerObject.AddComponent<AttackTracer>();
|
||||
tracer.Configure(origin, target, color, width, duration);
|
||||
}
|
||||
|
||||
private void Configure(Vector3 origin, Vector3 target, Color color, float width, float duration)
|
||||
{
|
||||
tracerColor = color;
|
||||
initialWidth = Mathf.Max(0.02f, width);
|
||||
lifetime = Mathf.Max(0.05f, duration);
|
||||
startedAt = Time.time;
|
||||
|
||||
line = gameObject.AddComponent<LineRenderer>();
|
||||
line.positionCount = 2;
|
||||
line.useWorldSpace = true;
|
||||
line.alignment = LineAlignment.View;
|
||||
line.textureMode = LineTextureMode.Stretch;
|
||||
line.numCapVertices = 4;
|
||||
line.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
line.receiveShadows = false;
|
||||
|
||||
var shader = Shader.Find("Sprites/Default") ?? Shader.Find("Unlit/Color") ?? Shader.Find("Standard");
|
||||
tracerMaterial = new Material(shader)
|
||||
{
|
||||
color = Color.white
|
||||
};
|
||||
line.sharedMaterial = tracerMaterial;
|
||||
|
||||
line.SetPosition(0, origin + Vector3.up * 0.45f);
|
||||
line.SetPosition(1, target + Vector3.up * 0.45f);
|
||||
ApplyVisual(0f);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var progress = Mathf.Clamp01((Time.time - startedAt) / lifetime);
|
||||
ApplyVisual(progress);
|
||||
if (progress >= 1f)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (tracerMaterial != null)
|
||||
{
|
||||
Destroy(tracerMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyVisual(float progress)
|
||||
{
|
||||
if (line == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var color = tracerColor;
|
||||
color.a *= 1f - progress;
|
||||
line.startColor = color;
|
||||
line.endColor = color;
|
||||
|
||||
var width = initialWidth * (1f - progress);
|
||||
line.startWidth = width;
|
||||
line.endWidth = width * 0.55f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd96f758922941958231d1bcf62bfb53
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay.Player;
|
||||
using Northbound.Gameplay.World;
|
||||
using UnityEngine;
|
||||
@@ -9,6 +10,7 @@ namespace Northbound.Gameplay.Presentation
|
||||
{
|
||||
private GUIStyle panelStyle;
|
||||
private GUIStyle titleStyle;
|
||||
private GUIStyle healthTextStyle;
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
@@ -35,6 +37,13 @@ namespace Northbound.Gameplay.Presentation
|
||||
GUILayout.Label(player.NearbyPrompt, titleStyle);
|
||||
}
|
||||
|
||||
if (player?.Actions is { IsActing: true } actions)
|
||||
{
|
||||
GUILayout.Space(6f);
|
||||
var cancelHint = actions.CurrentActionCanBeCancelled ? " ESC Cancel" : string.Empty;
|
||||
GUILayout.Label($"Action: {actions.CurrentActionName} {actions.RemainingTime:0.0}s{cancelHint}", titleStyle);
|
||||
}
|
||||
|
||||
if (state.IsDefeated)
|
||||
{
|
||||
GUILayout.Label("DEFEAT — The southern core has fallen.", titleStyle);
|
||||
@@ -46,9 +55,56 @@ namespace Northbound.Gameplay.Presentation
|
||||
|
||||
GUILayout.EndArea();
|
||||
|
||||
DrawHealthBars();
|
||||
DrawWorldLabels();
|
||||
}
|
||||
|
||||
private void DrawHealthBars()
|
||||
{
|
||||
var camera = Camera.main;
|
||||
if (camera == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var behaviour in FindObjectsByType<MonoBehaviour>())
|
||||
{
|
||||
if (behaviour is not IDamageable health || health.MaximumHealth <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var renderer = behaviour.GetComponent<Renderer>();
|
||||
var worldPosition = renderer == null
|
||||
? behaviour.transform.position + Vector3.up * 2.2f
|
||||
: new Vector3(renderer.bounds.center.x, renderer.bounds.max.y + 0.4f, renderer.bounds.center.z);
|
||||
var screen = camera.WorldToScreenPoint(worldPosition);
|
||||
if (screen.z <= 0f || screen.x < 0f || screen.x > Screen.width || screen.y < 0f || screen.y > Screen.height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float width = 64f;
|
||||
const float height = 9f;
|
||||
var left = screen.x - width * 0.5f;
|
||||
var top = Screen.height - screen.y;
|
||||
var background = new Rect(left - 1f, top - 1f, width + 2f, height + 2f);
|
||||
var fill = new Rect(left, top, width * Mathf.Clamp01(health.CurrentHealth / health.MaximumHealth), height);
|
||||
|
||||
var previousColor = GUI.color;
|
||||
GUI.color = new Color(0.03f, 0.04f, 0.04f, 0.9f);
|
||||
GUI.DrawTexture(background, Texture2D.whiteTexture);
|
||||
GUI.color = health.Team == Team.Enemy
|
||||
? new Color(1f, 0.18f, 0.12f, 0.95f)
|
||||
: new Color(0.15f, 0.9f, 0.72f, 0.95f);
|
||||
GUI.DrawTexture(fill, Texture2D.whiteTexture);
|
||||
GUI.color = previousColor;
|
||||
|
||||
var textRectangle = new Rect(left - 12f, top - 2f, width + 24f, height + 4f);
|
||||
GUI.Label(textRectangle, $"{health.CurrentHealth:0}/{health.MaximumHealth:0}", healthTextStyle);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawWorldLabels()
|
||||
{
|
||||
var camera = Camera.main;
|
||||
@@ -97,6 +153,13 @@ namespace Northbound.Gameplay.Presentation
|
||||
fontStyle = FontStyle.Bold,
|
||||
normal = { textColor = Color.white }
|
||||
};
|
||||
healthTextStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 9,
|
||||
fontStyle = FontStyle.Bold,
|
||||
normal = { textColor = Color.white }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace Northbound.Gameplay.Presentation
|
||||
[SerializeField] private PlayerController target;
|
||||
[SerializeField] private float visibleWorldWidth = 38f;
|
||||
[SerializeField] private float forwardOffset = 6f;
|
||||
[SerializeField] private float minimumZ = -70f;
|
||||
[SerializeField] private float forwardOffsetRampDistance = 16f;
|
||||
[SerializeField] private float minimumZ = -76f;
|
||||
[SerializeField] private float maximumZ = 70f;
|
||||
[SerializeField] private float smoothTime = 0.18f;
|
||||
|
||||
@@ -35,7 +36,12 @@ namespace Northbound.Gameplay.Presentation
|
||||
return;
|
||||
}
|
||||
|
||||
var desiredZ = Mathf.Clamp(target.transform.position.z + forwardOffset, minimumZ, maximumZ);
|
||||
var offsetBlend = Mathf.InverseLerp(
|
||||
minimumZ,
|
||||
minimumZ + Mathf.Max(0.01f, forwardOffsetRampDistance),
|
||||
target.transform.position.z);
|
||||
var dynamicForwardOffset = forwardOffset * offsetBlend;
|
||||
var desiredZ = Mathf.Clamp(target.transform.position.z + dynamicForwardOffset, minimumZ, maximumZ);
|
||||
var desired = new Vector3(0f, transform.position.y, desiredZ);
|
||||
transform.position = Vector3.SmoothDamp(transform.position, desired, ref velocity, smoothTime);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,63 @@
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay.Buildings;
|
||||
using Northbound.Gameplay.Enemy;
|
||||
using Northbound.Gameplay.Presentation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.World
|
||||
{
|
||||
public sealed class CoreObjective : MonoBehaviour, IDamageable
|
||||
{
|
||||
[SerializeField] private float maximumHealth = 1000f;
|
||||
[SerializeField] private BuildingDefinition buildingDefinition;
|
||||
[SerializeField] private float defenseRange = 10f;
|
||||
[SerializeField] private float defenseDamage = 8f;
|
||||
[SerializeField] private float defenseInterval = 1.25f;
|
||||
|
||||
private float defenseReadyAt;
|
||||
|
||||
public Team Team => Team.Player;
|
||||
public bool IsAlive => CurrentHealth > 0f;
|
||||
public float CurrentHealth { get; private set; }
|
||||
public float MaximumHealth => maximumHealth;
|
||||
public float MaximumHealth => buildingDefinition == null ? 1f : buildingDefinition.MaximumHealth;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
CurrentHealth = maximumHealth;
|
||||
CurrentHealth = MaximumHealth;
|
||||
}
|
||||
|
||||
public void Configure(BuildingDefinition definition)
|
||||
{
|
||||
buildingDefinition = definition;
|
||||
CurrentHealth = MaximumHealth;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsAlive || Time.time < defenseReadyAt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyAgent nearest = null;
|
||||
var nearestDistance = defenseRange * defenseRange;
|
||||
foreach (var enemy in FindObjectsByType<EnemyAgent>())
|
||||
{
|
||||
var distance = Vector3.SqrMagnitude(enemy.transform.position - transform.position);
|
||||
if (enemy.IsAlive && distance < nearestDistance)
|
||||
{
|
||||
nearest = enemy;
|
||||
nearestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearest == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
defenseReadyAt = Time.time + defenseInterval;
|
||||
AttackTracer.Spawn(transform.position, nearest.transform.position, new Color(0.1f, 1f, 0.75f, 0.95f), 0.2f, 0.2f);
|
||||
nearest.TakeDamage(defenseDamage, Team.Player);
|
||||
}
|
||||
|
||||
public void TakeDamage(float amount, Team sourceTeam)
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay.Buildings;
|
||||
using Northbound.Gameplay.Enemy;
|
||||
using Northbound.Gameplay.Presentation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Gameplay.World
|
||||
{
|
||||
public sealed class CrystalOutpost : MonoBehaviour, IDamageable, IInteractable
|
||||
{
|
||||
[SerializeField] private float maximumHealth = 300f;
|
||||
[SerializeField] private BuildingDefinition buildingDefinition;
|
||||
[SerializeField] private float manaPerSecond = 1.25f;
|
||||
[SerializeField] private float captureSafetyRadius = 8f;
|
||||
[SerializeField] private float defenseRange = 7.5f;
|
||||
[SerializeField] private float defenseDamage = 6f;
|
||||
[SerializeField] private float defenseInterval = 1.4f;
|
||||
[SerializeField] private float captureActionDuration = 0.4f;
|
||||
[SerializeField] private bool allowsMovementWhileCapturing;
|
||||
[SerializeField] private bool canCancelCapturing = true;
|
||||
[SerializeField] private Renderer stateRenderer;
|
||||
[SerializeField] private Material dormantMaterial;
|
||||
[SerializeField] private Material capturedMaterial;
|
||||
@@ -18,10 +26,13 @@ namespace Northbound.Gameplay.World
|
||||
public bool IsCaptured { get; private set; }
|
||||
public bool CanInteract => !IsCaptured;
|
||||
public float CurrentHealth { get; private set; }
|
||||
public float MaximumHealth => buildingDefinition == null ? 1f : buildingDefinition.MaximumHealth;
|
||||
|
||||
private float defenseReadyAt;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
CurrentHealth = maximumHealth;
|
||||
CurrentHealth = MaximumHealth;
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
@@ -35,11 +46,17 @@ namespace Northbound.Gameplay.World
|
||||
if (IsCaptured && IsAlive)
|
||||
{
|
||||
SharedGameState.Instance?.AddMana(manaPerSecond * Time.deltaTime);
|
||||
DefendOutpost();
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(Renderer renderer, Material dormant, Material captured)
|
||||
public void Configure(
|
||||
BuildingDefinition definition,
|
||||
Renderer renderer,
|
||||
Material dormant,
|
||||
Material captured)
|
||||
{
|
||||
buildingDefinition = definition;
|
||||
stateRenderer = renderer;
|
||||
dormantMaterial = dormant;
|
||||
capturedMaterial = captured;
|
||||
@@ -51,16 +68,27 @@ namespace Northbound.Gameplay.World
|
||||
return HasNearbyEnemy() ? "Clear nearby enemies" : "Activate crystal outpost";
|
||||
}
|
||||
|
||||
public void Interact(GameObject actor)
|
||||
public PlayerActionDefinition GetActionDefinition()
|
||||
{
|
||||
return new PlayerActionDefinition("Activate Outpost", captureActionDuration, allowsMovementWhileCapturing, canCancelCapturing);
|
||||
}
|
||||
|
||||
public bool CanStartInteraction(GameObject actor)
|
||||
{
|
||||
return !IsCaptured && !HasNearbyEnemy();
|
||||
}
|
||||
|
||||
public bool Interact(GameObject actor)
|
||||
{
|
||||
if (IsCaptured || HasNearbyEnemy())
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
IsCaptured = true;
|
||||
CurrentHealth = maximumHealth;
|
||||
CurrentHealth = MaximumHealth;
|
||||
RefreshVisual();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void TakeDamage(float amount, Team sourceTeam)
|
||||
@@ -91,6 +119,35 @@ namespace Northbound.Gameplay.World
|
||||
return false;
|
||||
}
|
||||
|
||||
private void DefendOutpost()
|
||||
{
|
||||
if (Time.time < defenseReadyAt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyAgent nearest = null;
|
||||
var nearestDistance = defenseRange * defenseRange;
|
||||
foreach (var enemy in FindObjectsByType<EnemyAgent>())
|
||||
{
|
||||
var distance = Vector3.SqrMagnitude(enemy.transform.position - transform.position);
|
||||
if (enemy.IsAlive && distance < nearestDistance)
|
||||
{
|
||||
nearest = enemy;
|
||||
nearestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearest == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
defenseReadyAt = Time.time + defenseInterval;
|
||||
AttackTracer.Spawn(transform.position, nearest.transform.position, new Color(0.75f, 0.3f, 1f, 0.95f), 0.18f, 0.2f);
|
||||
nearest.TakeDamage(defenseDamage, Team.Player);
|
||||
}
|
||||
|
||||
private void RefreshVisual()
|
||||
{
|
||||
if (stateRenderer != null)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Northbound.Core;
|
||||
using Northbound.Gameplay.Buildings;
|
||||
using Northbound.Gameplay.Player;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -15,8 +16,14 @@ namespace Northbound.Gameplay.World
|
||||
{
|
||||
[SerializeField] private int networkIndex;
|
||||
[SerializeField] private PortalState state = PortalState.Enemy;
|
||||
[SerializeField] private float maximumHealth = 240f;
|
||||
[SerializeField] private BuildingDefinition buildingDefinition;
|
||||
[SerializeField] private float rebuildManaCost = 40f;
|
||||
[SerializeField] private float rebuildActionDuration = 0.5f;
|
||||
[SerializeField] private bool allowsMovementWhileRebuilding;
|
||||
[SerializeField] private bool canCancelRebuilding = true;
|
||||
[SerializeField] private float travelActionDuration = 0.25f;
|
||||
[SerializeField] private bool allowsMovementWhileTraveling;
|
||||
[SerializeField] private bool canCancelTraveling;
|
||||
[SerializeField] private Renderer stateRenderer;
|
||||
[SerializeField] private Material enemyMaterial;
|
||||
[SerializeField] private Material ruinedMaterial;
|
||||
@@ -28,10 +35,11 @@ namespace Northbound.Gameplay.World
|
||||
public bool IsAlive => state != PortalState.Ruined;
|
||||
public bool CanInteract => state != PortalState.Enemy;
|
||||
public float CurrentHealth { get; private set; }
|
||||
public float MaximumHealth => buildingDefinition == null ? 1f : buildingDefinition.MaximumHealth;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
CurrentHealth = maximumHealth;
|
||||
CurrentHealth = MaximumHealth;
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
@@ -43,6 +51,7 @@ namespace Northbound.Gameplay.World
|
||||
public void Configure(
|
||||
int index,
|
||||
PortalState initialState,
|
||||
BuildingDefinition definition,
|
||||
Renderer renderer,
|
||||
Material enemy,
|
||||
Material ruined,
|
||||
@@ -50,11 +59,12 @@ namespace Northbound.Gameplay.World
|
||||
{
|
||||
networkIndex = index;
|
||||
state = initialState;
|
||||
buildingDefinition = definition;
|
||||
stateRenderer = renderer;
|
||||
enemyMaterial = enemy;
|
||||
ruinedMaterial = ruined;
|
||||
alliedMaterial = allied;
|
||||
CurrentHealth = maximumHealth;
|
||||
CurrentHealth = MaximumHealth;
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
@@ -91,24 +101,48 @@ namespace Northbound.Gameplay.World
|
||||
return "Travel through adjacent portal";
|
||||
}
|
||||
|
||||
public void Interact(GameObject actor)
|
||||
public PlayerActionDefinition GetActionDefinition()
|
||||
{
|
||||
return state == PortalState.Ruined
|
||||
? new PlayerActionDefinition("Rebuild Portal", rebuildActionDuration, allowsMovementWhileRebuilding, canCancelRebuilding)
|
||||
: new PlayerActionDefinition("Travel Portal", travelActionDuration, allowsMovementWhileTraveling, canCancelTraveling);
|
||||
}
|
||||
|
||||
public bool CanStartInteraction(GameObject actor)
|
||||
{
|
||||
var gameState = SharedGameState.Instance;
|
||||
if (state == PortalState.Ruined)
|
||||
{
|
||||
Rebuild();
|
||||
return;
|
||||
return gameState != null && gameState.Mana + 0.001f >= rebuildManaCost;
|
||||
}
|
||||
|
||||
if (state != PortalState.Allied || !actor.TryGetComponent<PlayerController>(out var player))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var direction = player.LastMoveDirection.z >= 0f ? 1 : -1;
|
||||
var destination = gameState?.GetPortal(networkIndex + direction);
|
||||
return destination != null && destination.State == PortalState.Allied;
|
||||
}
|
||||
|
||||
public bool Interact(GameObject actor)
|
||||
{
|
||||
if (state == PortalState.Ruined)
|
||||
{
|
||||
return Rebuild();
|
||||
}
|
||||
|
||||
if (state != PortalState.Allied || !actor.TryGetComponent<PlayerController>(out var player))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var direction = player.LastMoveDirection.z >= 0f ? 1 : -1;
|
||||
var destination = SharedGameState.Instance?.GetPortal(networkIndex + direction);
|
||||
if (destination == null || destination.State != PortalState.Allied)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var controller = actor.GetComponent<CharacterController>();
|
||||
@@ -122,19 +156,22 @@ namespace Northbound.Gameplay.World
|
||||
{
|
||||
controller.enabled = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Rebuild()
|
||||
private bool Rebuild()
|
||||
{
|
||||
var gameState = SharedGameState.Instance;
|
||||
if (gameState == null || !gameState.TrySpendMana(rebuildManaCost))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
state = PortalState.Allied;
|
||||
CurrentHealth = maximumHealth;
|
||||
CurrentHealth = MaximumHealth;
|
||||
RefreshVisual();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RefreshVisual()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41d8f260b39346e5ac08604a31c47d9c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
%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: 85b66c82f35d4c4c9626b843a50e12f1, type: 3}
|
||||
m_Name: BasicTowerBuilding
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Buildings.BuildingDefinition
|
||||
buildingId: 2000
|
||||
displayName: Basic Tower
|
||||
description:
|
||||
maximumHealth: 150
|
||||
completedPrefab: {fileID: 0}
|
||||
constructionPreviewPrefab: {fileID: 0}
|
||||
prototypeMaterial: {fileID: 2100000, guid: d76804fbbf65d204aa21241e7792d9a8, type: 2}
|
||||
prototypePrimitive: 2
|
||||
prototypeScale: {x: 0.7, y: 1.25, z: 0.7}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e49a0dcf744247b8a77b63f797a21c4c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: 4ca73b95ed924661b0f122f634acb09f, type: 3}
|
||||
m_Name: BasicTowerConstruction
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Construction.ConstructionDefinition
|
||||
building: {fileID: 11400000, guid: e49a0dcf744247b8a77b63f797a21c4c, type: 2}
|
||||
manaCost: 35
|
||||
buildDuration: 3
|
||||
allowsMovementDuringBuild: 0
|
||||
canCancelBuild: 1
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9fc6f9b4ab54e279a6610db8167a0ab
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: b8cf5bfa976c4a68b37f6a2f2b9c10d6, type: 3}
|
||||
m_Name: BasicTowerDefinition
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Defense.TowerDefinition
|
||||
building: {fileID: 11400000, guid: e49a0dcf744247b8a77b63f797a21c4c, type: 2}
|
||||
range: 8
|
||||
damage: 9
|
||||
attackInterval: 1.2
|
||||
attackColor: {r: 1, g: 0.75, b: 0.15, a: 0.95}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5eb3436646143be94c9d95a9b10cd27
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
%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: 85b66c82f35d4c4c9626b843a50e12f1, type: 3}
|
||||
m_Name: CrystalOutpostBuilding
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Buildings.BuildingDefinition
|
||||
buildingId: 1100
|
||||
displayName: Crystal Outpost
|
||||
description:
|
||||
maximumHealth: 300
|
||||
completedPrefab: {fileID: 0}
|
||||
constructionPreviewPrefab: {fileID: 0}
|
||||
prototypeMaterial: {fileID: 2100000, guid: ad9405226d4d14f4c8d791a396f78042, type: 2}
|
||||
prototypePrimitive: 0
|
||||
prototypeScale: {x: 2, y: 3, z: 2}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b21f75f184fe474b81f01dc1d9b107d7
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
%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: 85b66c82f35d4c4c9626b843a50e12f1, type: 3}
|
||||
m_Name: PortalBuilding
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Buildings.BuildingDefinition
|
||||
buildingId: 1200
|
||||
displayName: Portal
|
||||
description:
|
||||
maximumHealth: 240
|
||||
completedPrefab: {fileID: 0}
|
||||
constructionPreviewPrefab: {fileID: 0}
|
||||
prototypeMaterial: {fileID: 2100000, guid: 08b78577b60196a4c886e390284f06c5, type: 2}
|
||||
prototypePrimitive: 2
|
||||
prototypeScale: {x: 3, y: 0.45, z: 3}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d342d3173a3f4ffcaf67ad6f2129b219
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
%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: 85b66c82f35d4c4c9626b843a50e12f1, type: 3}
|
||||
m_Name: SouthernCoreBuilding
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Buildings.BuildingDefinition
|
||||
buildingId: 1000
|
||||
displayName: Southern Core
|
||||
description:
|
||||
maximumHealth: 1000
|
||||
completedPrefab: {fileID: 0}
|
||||
constructionPreviewPrefab: {fileID: 0}
|
||||
prototypeMaterial: {fileID: 2100000, guid: 460f24417f2a5ca43afb996319e7666e, type: 2}
|
||||
prototypePrimitive: 3
|
||||
prototypeScale: {x: 8, y: 4, z: 5}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1f30d3740434a3f8a9a4b035222f77e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -423,7 +423,8 @@ MonoBehaviour:
|
||||
target: {fileID: 597054782}
|
||||
visibleWorldWidth: 38
|
||||
forwardOffset: 6
|
||||
minimumZ: -70
|
||||
forwardOffsetRampDistance: 16
|
||||
minimumZ: -76
|
||||
maximumZ: 70
|
||||
smoothTime: 0.18
|
||||
--- !u!81 &117007547
|
||||
@@ -494,7 +495,7 @@ Transform:
|
||||
m_GameObject: {fileID: 117007545}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068}
|
||||
m_LocalPosition: {x: 0, y: 30, z: -70}
|
||||
m_LocalPosition: {x: 0, y: 30, z: -76}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
@@ -743,7 +744,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.World.PortalSite
|
||||
networkIndex: 1
|
||||
state: 0
|
||||
maximumHealth: 240
|
||||
buildingDefinition: {fileID: 11400000, guid: d342d3173a3f4ffcaf67ad6f2129b219, type: 2}
|
||||
rebuildManaCost: 40
|
||||
stateRenderer: {fileID: 437779362}
|
||||
enemyMaterial: {fileID: 2100000, guid: 470f8a3887afc71428ecde93cb980daf, type: 2}
|
||||
@@ -878,7 +879,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 7e89beb034a288c45b59485f3ff66293, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Player.PlayerController
|
||||
moveSpeed: 8
|
||||
moveSpeed: 5
|
||||
horizontalLimit: 15
|
||||
interactionRadius: 3.2
|
||||
--- !u!114 &597054783
|
||||
@@ -893,13 +894,23 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 01dc4a815b7e62c4580a7b5b60a0a05e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Player.PlayerCombat
|
||||
normalDamage: 26
|
||||
normalRange: 3.2
|
||||
normalCooldown: 0.32
|
||||
specialDamage: 58
|
||||
specialRadius: 4.8
|
||||
specialManaCost: 20
|
||||
specialCooldown: 4
|
||||
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
|
||||
--- !u!114 &597054784
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -913,7 +924,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Combat.Health
|
||||
team: 1
|
||||
maximum: 180
|
||||
maximum: 100
|
||||
destroyOnDeath: 1
|
||||
--- !u!143 &597054785
|
||||
CharacterController:
|
||||
@@ -1180,8 +1191,8 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Defense.DefenseSlot
|
||||
outpost: {fileID: 1846946633}
|
||||
buildCost: 35
|
||||
towerMaterial: {fileID: 2100000, guid: d76804fbbf65d204aa21241e7792d9a8, type: 2}
|
||||
constructionDefinition: {fileID: 11400000, guid: c9fc6f9b4ab54e279a6610db8167a0ab, type: 2}
|
||||
towerDefinition: {fileID: 11400000, guid: f5eb3436646143be94c9d95a9b10cd27, type: 2}
|
||||
--- !u!23 &782583536
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1609,7 +1620,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.World.PortalSite
|
||||
networkIndex: 2
|
||||
state: 0
|
||||
maximumHealth: 240
|
||||
buildingDefinition: {fileID: 11400000, guid: d342d3173a3f4ffcaf67ad6f2129b219, type: 2}
|
||||
rebuildManaCost: 40
|
||||
stateRenderer: {fileID: 1174730708}
|
||||
enemyMaterial: {fileID: 2100000, guid: 470f8a3887afc71428ecde93cb980daf, type: 2}
|
||||
@@ -1742,9 +1753,12 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: af6e0031e23718641b82d717facdef1f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.World.CrystalOutpost
|
||||
maximumHealth: 300
|
||||
buildingDefinition: {fileID: 11400000, guid: b21f75f184fe474b81f01dc1d9b107d7, type: 2}
|
||||
manaPerSecond: 1.25
|
||||
captureSafetyRadius: 8
|
||||
defenseRange: 7.5
|
||||
defenseDamage: 6
|
||||
defenseInterval: 1.4
|
||||
stateRenderer: {fileID: 1191827590}
|
||||
dormantMaterial: {fileID: 2100000, guid: 0aaab0f7eb2740a4fa6812cdf06c1bcc, type: 2}
|
||||
capturedMaterial: {fileID: 2100000, guid: ad9405226d4d14f4c8d791a396f78042, type: 2}
|
||||
@@ -1875,7 +1889,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.World.PortalSite
|
||||
networkIndex: 3
|
||||
state: 0
|
||||
maximumHealth: 240
|
||||
buildingDefinition: {fileID: 11400000, guid: d342d3173a3f4ffcaf67ad6f2129b219, type: 2}
|
||||
rebuildManaCost: 40
|
||||
stateRenderer: {fileID: 1272044565}
|
||||
enemyMaterial: {fileID: 2100000, guid: 470f8a3887afc71428ecde93cb980daf, type: 2}
|
||||
@@ -2121,8 +2135,8 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Defense.DefenseSlot
|
||||
outpost: {fileID: 1191827589}
|
||||
buildCost: 35
|
||||
towerMaterial: {fileID: 2100000, guid: d76804fbbf65d204aa21241e7792d9a8, type: 2}
|
||||
constructionDefinition: {fileID: 11400000, guid: c9fc6f9b4ab54e279a6610db8167a0ab, type: 2}
|
||||
towerDefinition: {fileID: 11400000, guid: f5eb3436646143be94c9d95a9b10cd27, type: 2}
|
||||
--- !u!23 &1483313425
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -2248,9 +2262,12 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: af6e0031e23718641b82d717facdef1f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.World.CrystalOutpost
|
||||
maximumHealth: 300
|
||||
buildingDefinition: {fileID: 11400000, guid: b21f75f184fe474b81f01dc1d9b107d7, type: 2}
|
||||
manaPerSecond: 1.25
|
||||
captureSafetyRadius: 8
|
||||
defenseRange: 7.5
|
||||
defenseDamage: 6
|
||||
defenseInterval: 1.4
|
||||
stateRenderer: {fileID: 1846946634}
|
||||
dormantMaterial: {fileID: 2100000, guid: 0aaab0f7eb2740a4fa6812cdf06c1bcc, type: 2}
|
||||
capturedMaterial: {fileID: 2100000, guid: ad9405226d4d14f4c8d791a396f78042, type: 2}
|
||||
@@ -2379,9 +2396,12 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: af6e0031e23718641b82d717facdef1f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.World.CrystalOutpost
|
||||
maximumHealth: 300
|
||||
buildingDefinition: {fileID: 11400000, guid: b21f75f184fe474b81f01dc1d9b107d7, type: 2}
|
||||
manaPerSecond: 1.25
|
||||
captureSafetyRadius: 8
|
||||
defenseRange: 7.5
|
||||
defenseDamage: 6
|
||||
defenseInterval: 1.4
|
||||
stateRenderer: {fileID: 1940329550}
|
||||
dormantMaterial: {fileID: 2100000, guid: 0aaab0f7eb2740a4fa6812cdf06c1bcc, type: 2}
|
||||
capturedMaterial: {fileID: 2100000, guid: ad9405226d4d14f4c8d791a396f78042, type: 2}
|
||||
@@ -2781,8 +2801,8 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.Defense.DefenseSlot
|
||||
outpost: {fileID: 1940329549}
|
||||
buildCost: 35
|
||||
towerMaterial: {fileID: 2100000, guid: d76804fbbf65d204aa21241e7792d9a8, type: 2}
|
||||
constructionDefinition: {fileID: 11400000, guid: c9fc6f9b4ab54e279a6610db8167a0ab, type: 2}
|
||||
towerDefinition: {fileID: 11400000, guid: f5eb3436646143be94c9d95a9b10cd27, type: 2}
|
||||
--- !u!23 &2061713006
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -2910,7 +2930,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.World.PortalSite
|
||||
networkIndex: 0
|
||||
state: 2
|
||||
maximumHealth: 240
|
||||
buildingDefinition: {fileID: 11400000, guid: d342d3173a3f4ffcaf67ad6f2129b219, type: 2}
|
||||
rebuildManaCost: 40
|
||||
stateRenderer: {fileID: 2076976473}
|
||||
enemyMaterial: {fileID: 2100000, guid: 470f8a3887afc71428ecde93cb980daf, type: 2}
|
||||
@@ -3043,7 +3063,10 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 4486584e692a882489182d3b3f0d7116, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Northbound.Gameplay::Northbound.Gameplay.World.CoreObjective
|
||||
maximumHealth: 1000
|
||||
buildingDefinition: {fileID: 11400000, guid: a1f30d3740434a3f8a9a4b035222f77e, type: 2}
|
||||
defenseRange: 10
|
||||
defenseDamage: 8
|
||||
defenseInterval: 1.25
|
||||
--- !u!23 &2087299787
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
Reference in New Issue
Block a user