Compare commits

...

2 Commits

Author SHA1 Message Date
kronic cca8c1d44e Docs: session log Part I + roadmap — attack distinctness; 1.5 short-term complete
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:18:27 -07:00
kronic a335ca3f21 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>
2026-07-09 23:17:43 -07:00
5 changed files with 72 additions and 12 deletions
@@ -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;
@@ -139,9 +139,9 @@ attacks/animations/effects**. Art direction deliberately UNDECIDED — lighting
5. ~~Breakable clutter with drops~~ **DONE (2026-07-09, `53bc21143`)** — the BlightClutter chain already
existed end-to-end (seeding/unified-sweep/drops); the gap was feel: barrels (SM_Prop_Barrel_01) replace the
rock chunk, ONE-hit pop (Remaining 8→2), density 6→8/room. Live-verified pop + drop credit.
6. **Attack distinctness:** every enemy kind + class ability gets a unique read — anim + VFX + effect shape (not more
hit-stop). Builds on the shipped Blender clip pipeline + sword-combo work; creature locomotion for monsters still
open from Phase 1.3.
6. **Attack distinctness — CODE-SIDE DONE (2026-07-09, `a335ca3f2`):** per-kind telegraph colours (shapes
were already per-kind), per-kind windup voices, ownership-keyed projectile lights. The anim-side depth
(per-kind clips, creature locomotion — still open from Phase 1.3) rides the parallel Blender workstream.
### Long-term (server-sim; adversarial design-review REQUIRED per slice)
@@ -202,13 +202,26 @@ a combat room; an injected projectile through the real sweep popped one in ONE h
466/466; console clean. Also committed the operator's own F3-panel slider drags from the art-look lap
(`8c2bf3c37` — master stays OFF).
## Part I — attack distinctness pass (`a335ca3f2`)
Per-kind unique reads on the existing replicated state (client-only, zero netcode): **telegraph COLOURS**
per `EnemyTelegraph.Kind` (grunt orange / charger+boss crimson / spitter toxic-green lane / swarmer yellow —
shapes were already per-kind, colours were uniformly red) · **windup VOICES** at the existing onset edge
(thud / rising roar / wet hiss / chitter, strike-beep distance gate) · **projectile light colour by
ownership** in DynamicLightSystem (player cyan vs toxic-green enemy spit, `EnemyProjectileColor` knob).
Verified live: a 12 s combat histogram showed zones rendering K0+K1 materials keyed to real windups
(screenshot); 466/466; console clean. Gotcha: bare `GhostOwner` needs `using Unity.NetCode;` (CS0246 in a
Client presentation file that only used Simulation types before). Remaining distinctness depth (per-kind
anim clips, creature locomotion) rides the parallel Blender workstream.
## Next-session intent
Phase 1.5 short-term wins remaining: **attack distinctness** (unique read per enemy kind + class ability —
anim + VFX + effect shape, on the Blender clip pipeline). Long-term 1.5 (hazards, destructible cover) =
adversarial design-review-first. Queued taste items: re-enable curated base flora (barren hub) · arid
brightness · craters-as-blocks call · B5 split-panels smoke. Then Phase 1.7 boon overhaul (design review
required — replicated ability swaps).
**Phase 1.5 short-term wins are COMPLETE** (collision ✓ lighting ✓ art-look ✓ ambient motion ✓ breakables ✓
attack distinctness ✓). Next: either the 1.5 long-term slices (**environmental hazards** / **destructible
cover** — BOTH adversarial design-review-first, and cover must re-validate the enemy backstop) or jump to
**Phase 1.7 boon overhaul** (~12 mechanic-changers; design review required — replicated ability swaps).
Operator gate: a feel-lap over the whole 1.5 package ("no longer static"?) decides. Queued taste items:
re-enable curated base flora · arid brightness · craters call · B5 split-panels smoke.
Related: [[Iteration_2026-07_CoopHades]] · [[2026-07-07_Workflow_Consolidation]] ·
[[2026-07-07_Expedition_Enemy_Stuck_Harvest_Feedback]]