Feel: attack distinctness — per-kind telegraph colours, windup voices, spit light
Every enemy kind now has a unique read on the existing replicated state (client-only, zero netcode): - Telegraph COLOURS per EnemyTelegraph.Kind: grunt orange, charger crimson (boss shares it - shapes already differ: cone/wedge/ring), spitter toxic-green lane, swarmer yellow. Shapes were already per-kind; they all rendered the same HDR red. - Windup VOICES at the onset edge (the existing _prevWindup edge + strike-beep distance gate): grunt low thud, charger rising roar, spitter wet hiss, swarmer chitter - kinds read by EAR before the telegraph ramps. - Projectile light colour by ownership in DynamicLightSystem: owned shots stay player-cyan, un-owned (Spitter spit) glows toxic green (EnemyProjectileColor knob). Verified live: 12 s combat histogram shows zones rendering with EnemyDanger_K0 (grunt) + EnemyDanger_K1 (charger) materials keyed to real windups (screenshot); 466/466 EditMode; console clean. Remaining distinctness depth (per-kind anim clips, creature locomotion) rides the parallel Blender workstream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,8 @@ namespace ProjectM.Client
|
||||
[Header("Projectile lights (pooled, follow every live projectile ghost)")]
|
||||
[Min(0)] public int MaxProjectileLights = 24;
|
||||
public Color ProjectileColor = new Color(0.45f, 0.85f, 1f, 1f);
|
||||
[Tooltip("Un-owned (enemy) projectiles — Spitter spit reads toxic green vs the player's cyan.")]
|
||||
public Color EnemyProjectileColor = new Color(0.55f, 1f, 0.35f, 1f);
|
||||
[Min(0f)] public float ProjectileIntensity = 2.6f;
|
||||
[Min(0.5f)] public float ProjectileRange = 7f;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -74,6 +75,7 @@ namespace ProjectM.Client
|
||||
Color projColor = cfg != null ? cfg.ProjectileColor : new Color(0.45f, 0.85f, 1f, 1f);
|
||||
float projIntensity = cfg != null ? cfg.ProjectileIntensity : 2.6f;
|
||||
float projRange = cfg != null ? cfg.ProjectileRange : 7f;
|
||||
Color enemyProjColor = cfg != null ? cfg.EnemyProjectileColor : new Color(0.55f, 1f, 0.35f, 1f);
|
||||
_seen.Clear();
|
||||
foreach (var (lt, entity) in SystemAPI.Query<RefRO<LocalTransform>>().WithAll<Projectile>().WithEntityAccess())
|
||||
{
|
||||
@@ -84,7 +86,8 @@ namespace ProjectM.Client
|
||||
light = Rent();
|
||||
_projectileLights[entity] = light;
|
||||
}
|
||||
light.color = projColor;
|
||||
// Attack-distinctness: owned shots glow the player cyan; un-owned (enemy spit) glows toxic green.
|
||||
light.color = EntityManager.HasComponent<GhostOwner>(entity) ? projColor : enemyProjColor;
|
||||
light.intensity = projIntensity;
|
||||
light.range = projRange;
|
||||
var p = lt.ValueRO.Position;
|
||||
|
||||
@@ -35,11 +35,22 @@ namespace ProjectM.Client
|
||||
readonly Dictionary<Entity, uint> _prevWindup = new(); // self-detect the windup-onset edge (was the core _cache.Windup)
|
||||
readonly HashSet<Entity> _enemySeen = new(); // ALL enemies this frame (prunes _pulseStart/_strikeBeeped/_prevWindup)
|
||||
AudioClip _strikeBeepClip; // near-impact "dodge NOW" beep
|
||||
AudioClip[] _kindGrowls; // per-kind windup voice (attack distinctness)
|
||||
Material[] _kindMats; // per-kind telegraph colours (attack distinctness)
|
||||
Entity _localPlayer = Entity.Null;
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
_strikeBeepClip = MakeClip("strike", 1150f, 1500f, 0.05f, 0.30f, noise: false); // near-impact beep
|
||||
// Attack-distinctness: per-kind windup VOICES so kinds read by EAR at onset (0 grunt thud,
|
||||
// 1 charger rising roar, 2 spitter wet hiss, 3 swarmer chitter).
|
||||
_kindGrowls = new AudioClip[]
|
||||
{
|
||||
MakeClip("growl_grunt", 180f, 110f, 0.14f, 0.30f, noise: false),
|
||||
MakeClip("growl_charger", 90f, 420f, 0.32f, 0.34f, noise: false),
|
||||
MakeClip("growl_spitter", 1900f, 500f, 0.18f, 0.26f, noise: true),
|
||||
MakeClip("growl_swarmer", 1500f, 2100f, 0.07f, 0.22f, noise: false),
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnStartRunning()
|
||||
@@ -48,13 +59,32 @@ namespace ProjectM.Client
|
||||
_fxRoot = new GameObject("~EnemyDangerFX").transform;
|
||||
_dangerMat = MakeParticleMaterial();
|
||||
_dangerMat.name = "EnemyDanger";
|
||||
_dangerMat.color = new Color(3.2f, 0.28f, 0.18f, 1f); // HDR red (per-zone intensity carried in vertex alpha)
|
||||
_dangerMat.color = new Color(3.2f, 0.28f, 0.18f, 1f); // HDR red fallback (per-zone intensity in vertex alpha)
|
||||
// Attack-distinctness: per-kind telegraph COLORS (0 grunt orange, 1 charger crimson,
|
||||
// 2 spitter toxic green, 3 swarmer yellow; the boss shares the charger crimson — shapes differ).
|
||||
_kindMats = new Material[4];
|
||||
var kindColors = new Color[]
|
||||
{
|
||||
new Color(3.2f, 0.85f, 0.15f, 1f),
|
||||
new Color(3.2f, 0.18f, 0.12f, 1f),
|
||||
new Color(0.55f, 2.9f, 0.35f, 1f),
|
||||
new Color(2.9f, 2.3f, 0.22f, 1f),
|
||||
};
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
_kindMats[i] = MakeParticleMaterial();
|
||||
_kindMats[i].name = "EnemyDanger_K" + i;
|
||||
_kindMats[i].color = kindColors[i];
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
if (_fxRoot != null) Object.Destroy(_fxRoot.gameObject);
|
||||
if (_dangerMat != null) Object.Destroy(_dangerMat);
|
||||
if (_kindMats != null)
|
||||
for (int i = 0; i < _kindMats.Length; i++)
|
||||
if (_kindMats[i] != null) Object.Destroy(_kindMats[i]);
|
||||
foreach (var kv in _dangerZones)
|
||||
if (kv.Value != null) { var mf = kv.Value.GetComponent<MeshFilter>(); if (mf != null && mf.sharedMesh != null) Object.Destroy(mf.sharedMesh); }
|
||||
}
|
||||
@@ -107,7 +137,18 @@ namespace ProjectM.Client
|
||||
// transition of WindUpUntilTick arms the anticipation scale-pulse (Feature C). Requires a prior 0
|
||||
// record so a mid-windup relevancy re-entry doesn't spuriously pulse (matches the old prev.Windup==0).
|
||||
bool hadPrev = _prevWindup.TryGetValue(entity, out var pw);
|
||||
if (until != 0u && hadPrev && pw == 0u) _pulseStart[entity] = (float)SystemAPI.Time.ElapsedTime;
|
||||
if (until != 0u && hadPrev && pw == 0u)
|
||||
{
|
||||
_pulseStart[entity] = (float)SystemAPI.Time.ElapsedTime;
|
||||
// Attack-distinctness: the per-kind windup VOICE at onset (same gate family as the strike beep).
|
||||
if (FeelConfig.StrikeBeepEnabled && _localPlayer != Entity.Null && _kindGrowls != null
|
||||
&& math.distancesq(xf.ValueRO.Position, localPos) <= FeelConfig.StrikeBeepMaxDistSq)
|
||||
{
|
||||
byte gk = tele.ValueRO.Kind;
|
||||
if (gk < _kindGrowls.Length && _kindGrowls[gk] != null)
|
||||
PlayClip(_kindGrowls[gk], (Vector3)xf.ValueRO.Position, 0.22f);
|
||||
}
|
||||
}
|
||||
_prevWindup[entity] = until;
|
||||
|
||||
// Feature D: a committed Charger lunge keeps the cue ALIVE past windup (AttackWindup zeroes at commit).
|
||||
@@ -163,7 +204,8 @@ namespace ProjectM.Client
|
||||
go.transform.SetParent(_fxRoot, false);
|
||||
go.AddComponent<MeshFilter>().sharedMesh = new Mesh { name = "EnemyDanger" };
|
||||
var mr = go.AddComponent<MeshRenderer>();
|
||||
mr.sharedMaterial = _dangerMat;
|
||||
mr.sharedMaterial = _kindMats != null && tele.ValueRO.Kind < _kindMats.Length && _kindMats[tele.ValueRO.Kind] != null
|
||||
? _kindMats[tele.ValueRO.Kind] : _dangerMat; // per-kind telegraph colour (kind is fixed per entity)
|
||||
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
mr.receiveShadows = false;
|
||||
_dangerZones[entity] = go;
|
||||
|
||||
Reference in New Issue
Block a user