Initial Combat Implementation

This commit is contained in:
Luis Gonzalez
2026-05-31 21:35:12 -07:00
parent 7fa77ce821
commit 1f647dd5e1
166 changed files with 93337 additions and 91 deletions
@@ -0,0 +1,92 @@
using System.Collections.Generic;
using ProjectM.Simulation;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Bakes the designer-authored ability + character definitions into a single AbilityDatabase blob
/// singleton (immutable, shared, Burst-fast) plus a companion AbilityPrefabElement buffer holding the
/// per-ability projectile ghost prefab entity refs (entity refs cannot live in a blob). Place ONE of
/// these in the gameplay subscene; it streams identically into the client and server worlds (config,
/// not replicated). DependsOn each definition so a value change in an SO re-bakes the blob.
/// </summary>
public class AbilityDatabaseAuthoring : MonoBehaviour
{
[Tooltip("All ability definitions available in the game. Indexed at runtime by AbilityId.")]
public List<AbilityDefinition> Abilities = new List<AbilityDefinition>();
[Tooltip("All character-stats definitions. Indexed at runtime by CharacterId.")]
public List<CharacterStatsDefinition> Characters = new List<CharacterStatsDefinition>();
private class DatabaseBaker : Baker<AbilityDatabaseAuthoring>
{
public override void Bake(AbilityDatabaseAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.None);
int abilityCount = authoring.Abilities != null ? authoring.Abilities.Count : 0;
int charCount = authoring.Characters != null ? authoring.Characters.Count : 0;
var builder = new BlobBuilder(Allocator.Temp);
ref var root = ref builder.ConstructRoot<AbilityDatabaseBlob>();
var abilityArray = builder.Allocate(ref root.Abilities, abilityCount);
for (int i = 0; i < abilityCount; i++)
{
var def = authoring.Abilities[i];
if (def == null) { abilityArray[i] = default; continue; }
DependsOn(def);
abilityArray[i] = new AbilityDefBlob
{
Id = (byte)def.Id,
Damage = def.Damage,
ProjectileSpeed = def.ProjectileSpeed,
Range = def.Range,
AutoTargetRange = def.AutoTargetRange,
AutoTargetConeRadians = math.radians(def.AutoTargetConeDegrees),
CooldownTicks = def.CooldownTicks,
Name = def.DisplayName,
};
}
var charArray = builder.Allocate(ref root.Characters, charCount);
for (int i = 0; i < charCount; i++)
{
var def = authoring.Characters[i];
if (def == null) { charArray[i] = default; continue; }
DependsOn(def);
charArray[i] = new CharacterStatsBlob
{
Id = (byte)def.Id,
MoveSpeed = def.MoveSpeed,
TurnRateRadiansPerSec = math.radians(def.TurnRateDegreesPerSec),
MaxHealth = def.MaxHealth,
Name = def.DisplayName,
};
}
var blob = builder.CreateBlobAssetReference<AbilityDatabaseBlob>(Allocator.Persistent);
builder.Dispose();
AddBlobAsset(ref blob, out _);
AddComponent(entity, new AbilityDatabase { Value = blob });
// Companion entity-ref buffer: per-ability projectile ghost prefab (resolved via GetEntity).
var prefabBuffer = AddBuffer<AbilityPrefabElement>(entity);
for (int i = 0; i < abilityCount; i++)
{
var def = authoring.Abilities[i];
if (def == null || def.ProjectilePrefab == null) continue;
prefabBuffer.Add(new AbilityPrefabElement
{
Id = (byte)def.Id,
Prefab = GetEntity(def.ProjectilePrefab, TransformUsageFlags.Dynamic),
});
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4f60b41d8ec8d4c24b4d4f54af919080
@@ -0,0 +1,33 @@
using ProjectM.Simulation;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Designer-facing definition of one ability. Numeric fields are baked into the AbilityDatabase blob
/// (immutable, Burst-fast runtime config); the projectile prefab is baked into the companion
/// AbilityPrefabElement buffer (entity refs cannot live inside a blob). UI fields (icon/description)
/// are deliberately deferred to a later managed lookup keyed by id.
/// </summary>
[CreateAssetMenu(menuName = "Project M/Ability Definition", fileName = "Ability_")]
public class AbilityDefinition : ScriptableObject
{
public AbilityId Id = AbilityId.Primary;
public string DisplayName = "Ability";
[Header("Combat")]
[Min(0f)] public float Damage = 20f;
[Min(0f)] public float ProjectileSpeed = 25f;
[Min(0f)] public float Range = 20f;
[Header("Auto-target assist")]
[Min(0f)] public float AutoTargetRange = 12f;
[Min(0f)] public float AutoTargetConeDegrees = 35f;
[Header("Timing")]
[Min(1)] public int CooldownTicks = 12;
[Header("Prefab (baked into the prefab buffer, not the blob)")]
public GameObject ProjectilePrefab;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 84b20ca889a744e888c8c3b3b723ec69
@@ -0,0 +1,21 @@
using ProjectM.Simulation;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Designer-facing definition of a character's base stats (movement + survivability), baked into the
/// AbilityDatabase blob and looked up at runtime by CharacterStatsRef. The single source of these
/// values - PlayerAuthoring also seeds the player's starting Health from MaxHealth.
/// </summary>
[CreateAssetMenu(menuName = "Project M/Character Stats Definition", fileName = "Character_")]
public class CharacterStatsDefinition : ScriptableObject
{
public CharacterId Id = CharacterId.Default;
public string DisplayName = "Character";
[Min(0f)] public float MoveSpeed = 6f;
[Min(0f)] public float TurnRateDegreesPerSec = 720f;
[Min(0f)] public float MaxHealth = 100f;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c63201f1fd9f24efd9ccae3e20cd2364
@@ -0,0 +1,38 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the projectile ghost prefab fired by the player's primary ability. Bakes the
/// baked-once tunables (<see cref="Projectile.Speed"/>, <see cref="Projectile.Damage"/>,
/// <see cref="Projectile.Range"/>) onto the entity; the replicated <c>Direction</c>/<c>SpawnId</c>
/// and the integrated <c>DistanceTravelled</c> are left at their default 0 and written at spawn
/// time by AbilityFireSystem. Ghost replication and <c>GhostOwner</c> are supplied by the
/// GhostAuthoringComponent added on the same prefab GameObject (not added here, nor is Health).
/// <c>GetEntity(TransformUsageFlags.Dynamic)</c> ensures a runtime-mutable LocalTransform exists.
/// </summary>
public class ProjectileAuthoring : MonoBehaviour
{
[Min(0f)] public float Speed = 25f;
[Min(0f)] public float Damage = 20f;
[Min(0f)] public float Range = 20f;
private class ProjectileBaker : Baker<ProjectileAuthoring>
{
public override void Bake(ProjectileAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
// Direction / SpawnId / DistanceTravelled default to 0 — set at spawn by AbilityFireSystem.
AddComponent(entity, new Projectile
{
Speed = authoring.Speed,
Damage = authoring.Damage,
Range = authoring.Range
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1429bc3b3a1da44e4a11065be0733a8f
@@ -0,0 +1,34 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring placed once in the gameplay subscene. Bakes a <see cref="ProjectileSpawner"/>
/// singleton holding the projectile ghost prefab entity, which the predicted AbilityFireSystem
/// instantiates whenever a player fires. The spawner itself carries no transform (it is a pure
/// data singleton) so it is baked with <c>TransformUsageFlags.None</c>, while the referenced
/// prefab is baked with <c>TransformUsageFlags.Dynamic</c> so the spawned projectile has a
/// runtime-mutable LocalTransform.
/// </summary>
public class ProjectileSpawnerAuthoring : MonoBehaviour
{
[Tooltip("The projectile ghost prefab spawned when a player fires.")]
public GameObject ProjectilePrefab;
private class ProjectileSpawnerBaker : Baker<ProjectileSpawnerAuthoring>
{
public override void Bake(ProjectileSpawnerAuthoring authoring)
{
// The spawner itself needs no transform; it is a data singleton.
var entity = GetEntity(authoring, TransformUsageFlags.None);
AddComponent(entity, new ProjectileSpawner
{
Prefab = GetEntity(authoring.ProjectilePrefab, TransformUsageFlags.Dynamic)
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: aa1b7c054d5a043f2801cddf90567acf
@@ -0,0 +1,41 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the training-dummy enemy prefab. Bakes a stationary, damageable auto-target
/// candidate: <see cref="TrainingDummyTag"/> marks it for the ability auto-target cone,
/// <see cref="Health"/> and <see cref="HitRadius"/> make it a valid projectile hit target, and a
/// <see cref="DamageEvent"/> buffer receives server-authoritative hits. Dummies are NOT ghosts and
/// carry no <c>GhostOwner</c>, so projectiles never treat them as the firing owner.
/// <c>GetEntity(TransformUsageFlags.Dynamic)</c> ensures a runtime LocalTransform for hit tests
/// and spawn placement.
/// </summary>
public class TrainingDummyAuthoring : MonoBehaviour
{
[Min(0f), Tooltip("Starting and maximum health for the dummy.")]
public float MaxHealth = 60f;
[Min(0f), Tooltip("World-unit radius used by the projectile hit test.")]
public float HitRadius = 0.8f;
private class TrainingDummyBaker : Baker<TrainingDummyAuthoring>
{
public override void Bake(TrainingDummyAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
AddComponent<TrainingDummyTag>(entity);
AddComponent(entity, new Health
{
Current = authoring.MaxHealth,
Max = authoring.MaxHealth
});
AddComponent(entity, new HitRadius { Value = authoring.HitRadius });
AddBuffer<DamageEvent>(entity);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c32aebeb7bfbb464898dfee8e6e87e6c
@@ -0,0 +1,48 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the baked <see cref="TrainingDummySpawner"/> singleton. Place this on a single
/// GameObject in the gameplay subscene; at runtime the server-only
/// <c>TrainingDummySpawnSystem</c> reads the singleton, instantiates <see cref="Count"/> dummies
/// laid out along +X from <see cref="Origin"/> at <see cref="Spacing"/> intervals, then destroys
/// the singleton so it fires exactly once. The entity itself carries no transform
/// (<c>TransformUsageFlags.None</c>); only the referenced <see cref="DummyPrefab"/> needs a
/// runtime-mutable LocalTransform (<c>TransformUsageFlags.Dynamic</c>).
/// </summary>
public class TrainingDummySpawnerAuthoring : MonoBehaviour
{
[Tooltip("Training dummy prefab to instantiate. Must carry TrainingDummyAuthoring.")]
public GameObject DummyPrefab;
[Min(0)]
[Tooltip("How many dummies to spawn.")]
public int Count = 3;
[Min(0f)]
[Tooltip("World-unit spacing between consecutive dummies along +X.")]
public float Spacing = 3f;
[Tooltip("World-space position of the first dummy.")]
public Vector3 Origin = new Vector3(0, 0, 8);
private class TrainingDummySpawnerBaker : Baker<TrainingDummySpawnerAuthoring>
{
public override void Bake(TrainingDummySpawnerAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.None);
AddComponent(entity, new TrainingDummySpawner
{
Prefab = GetEntity(authoring.DummyPrefab, TransformUsageFlags.Dynamic),
Count = authoring.Count,
Spacing = authoring.Spacing,
Origin = authoring.Origin
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 51cc543c146d84239ba6dc219221df18
@@ -0,0 +1,43 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for an upgrade pickup ghost prefab: a world object that grants one stat modifier to the
/// first player that overlaps it (server-authoritative, applied by <c>UpgradePickupSystem</c>) and
/// then despawns. Bake the prefab as an interpolated ghost (add a GhostAuthoringComponent) so clients
/// see it appear and despawn. <c>GetEntity(TransformUsageFlags.Dynamic)</c> gives it a world transform.
/// </summary>
public class UpgradePickupAuthoring : MonoBehaviour
{
[Tooltip("Which stat the granted modifier targets.")]
public StatTarget Target = StatTarget.Damage;
[Tooltip("How the granted modifier combines.")]
public ModOp Op = ModOp.Flat;
[Tooltip("Modifier magnitude: flat amount, or fractional percent (0.1 = +10%).")]
public float Value = 10f;
[Tooltip("Overlap radius (world units) for the player pickup test.")]
[Min(0f)] public float HitRadius = 1f;
private class UpgradePickupBaker : Baker<UpgradePickupAuthoring>
{
public override void Bake(UpgradePickupAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
AddComponent(entity, new UpgradePickup
{
Target = (byte)authoring.Target,
Op = (byte)authoring.Op,
Value = authoring.Value,
SourceId = 0u,
});
AddComponent(entity, new HitRadius { Value = authoring.HitRadius });
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 07e6d40378fcb43c5be706ef96cb4bb2
@@ -0,0 +1,48 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the baked <see cref="UpgradePickupSpawner"/> singleton (mirrors
/// TrainingDummySpawnerAuthoring). Place on a single GameObject in the gameplay subscene; the
/// server-only <c>UpgradePickupSpawnSystem</c> reads it, spawns <see cref="Count"/> pickups along +X
/// from <see cref="Origin"/> at <see cref="Spacing"/> intervals, then destroys the singleton so it
/// fires exactly once. The entity carries no transform; only the prefab needs a runtime transform.
/// </summary>
public class UpgradePickupSpawnerAuthoring : MonoBehaviour
{
[Tooltip("Upgrade pickup prefab to instantiate. Must carry UpgradePickupAuthoring.")]
public GameObject PickupPrefab;
[Min(0)]
[Tooltip("How many pickups to spawn.")]
public int Count = 2;
[Min(0f)]
[Tooltip("World-unit spacing between consecutive pickups along +X.")]
public float Spacing = 3f;
[Tooltip("World-space position of the first pickup.")]
public Vector3 Origin = new Vector3(-4f, 0f, 6f);
private class UpgradePickupSpawnerBaker : Baker<UpgradePickupSpawnerAuthoring>
{
public override void Bake(UpgradePickupSpawnerAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.None);
AddComponent(entity, new UpgradePickupSpawner
{
Prefab = authoring.PickupPrefab != null
? GetEntity(authoring.PickupPrefab, TransformUsageFlags.Dynamic)
: Entity.Null,
Count = authoring.Count,
Spacing = authoring.Spacing,
Origin = authoring.Origin,
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e367eb55e2c2248f18be10d6c3c9ad67