Debug and spawn utilities for a "hellhound" zombie variant. Provides console commands to spawn a bare renderer test hound (nz_hound_test), spawn real AI-driven hellhounds from a variant (nz_hound), inspect live hellhounds (nz_hound_info), and clear test renderers (nz_hound_clear). It loads models, checks sequences, and logs results.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// Temporary: prove the ported hellhound loads and can play its clips.
///
/// ⚠️ EXISTS BECAUSE "IT COMPILED" IS NOT "IT ANIMATES". A vmdl with 15 `AnimFile`
/// entries can carry zero SEQUENCES — animations and sequences are different lists,
/// and `Sequence.Name` plays the latter. That gap has cost this project a day before
/// now, so it gets checked before any AI is written against the model.
/// </summary>
public static class HoundCheck
{
public const string ModelPath = "models/nz/hound/hound.vmdl";
/// <summary>Spawn one in front of you: `nz_hound_test [sequence]`.</summary>
[ConCmd( "nz_hound_test" )]
public static void Spawn( string sequence = "nz_dog_run" )
{
var player = NZPlayer.Local;
if ( !player.IsValid() ) { Log.Warning( "[nz] no player" ); return; }
var model = Model.Load( ModelPath );
if ( model is null || model.IsError )
{
Log.Warning( $"[nz] hound model '{ModelPath}' failed to load" );
return;
}
var controller = player.Components.Get<PlayerController>();
var eye = controller?.EyePosition ?? player.WorldPosition + Vector3.Up * 64f;
var rot = controller?.EyeAngles.ToRotation() ?? player.WorldRotation;
var from = eye + rot.Forward * 140f;
var tr = Game.ActiveScene.Trace.Ray( from, from + Vector3.Down * 300f )
.IgnoreGameObjectHierarchy( player.GameObject )
.Run();
var go = new GameObject( true, "hound_test" );
go.WorldPosition = tr.Hit ? tr.HitPosition : from;
// Face the player, so the test dog is looking at you rather than away.
go.WorldRotation = Rotation.LookAt( (player.WorldPosition - go.WorldPosition).WithZ( 0f ).Normal );
var r = go.Components.Create<SkinnedModelRenderer>();
// ⛔ ANIMGRAPH OFF — a ported model has none, and with the graph left on
// `Sequence.Name` is ignored and the model never animates, silently.
if ( model.AnimGraph is null ) r.UseAnimGraph = false;
r.Model = model;
Log.Info( $"[nz] hound spawned — animations={model.AnimationCount}, "
+ $"bounds {model.Bounds.Size}" );
// ⚠️ Sequences only exist once the renderer has a scene object, so this is
// read AFTER it is enabled — reading earlier always reports zero.
try
{
var names = r.Sequence.SequenceNames?.ToList();
if ( names is null || names.Count == 0 )
{
Log.Warning( "[nz] hound has NO SEQUENCES — the vmdl carries animations "
+ "but nothing playable" );
return;
}
Log.Info( $"[nz] hound sequences ({names.Count}): {string.Join( ", ", names )}" );
if ( !names.Contains( sequence ) )
{
sequence = names.FirstOrDefault( n => n.Contains( "run" ) ) ?? names[0];
Log.Info( $"[nz] falling back to '{sequence}'" );
}
r.Sequence.Name = sequence;
r.Sequence.Looping = true;
Log.Info( $"[nz] playing '{sequence}' ({r.Sequence.Duration:0.00}s)" );
}
catch ( Exception e )
{
Log.Warning( $"[nz] hound sequence check failed: {e.Message}" );
}
}
/// <summary>
/// Spawn a REAL hound — the zombie AI driving the hellhound variant:
/// `nz_hound [count]`.
///
/// ⛔ THIS IS A ZOMBIE, NOT A NEW ENTITY. `ZombieAI` already owns navmesh
/// chasing, damage, death, ragdolls and the points award, and `ZombieVariant` is
/// a GameResource built precisely so a new enemy is DATA — models, speed tiers,
/// animation names. Writing a parallel `HellhoundAI` would mean reimplementing
/// 3,700 lines of tested behaviour to change a model and three clip names.
///
/// ⚠️ `nz_hound_test` is the other command and does something different: it spawns
/// a bare renderer to prove the MODEL loads. That one cannot chase or be shot —
/// it has no AI and no collider, which is exactly what "runs in place and takes no
/// bullets" meant.
/// </summary>
[ConCmd( "nz_hound" )]
public static void SpawnReal( int count = 1 )
{
var scene = Game.ActiveScene;
if ( !scene.IsValid() ) { Log.Warning( "[nz] no active scene" ); return; }
var variant = ResourceLibrary.Get<ZombieVariant>( VariantPath );
if ( variant is null )
{
Log.Warning( $"[nz] hellhound variant '{VariantPath}' not found" );
return;
}
var player = NZPlayer.Local;
var origin = player.IsValid() ? player.WorldPosition : Vector3.Zero;
int made = 0;
for ( int i = 0; i < count; i++ )
{
var pos = origin + Vector3.Random.WithZ( 0 ).Normal * 220f;
// ⚠️ PASSED IN, NOT ASSIGNED AFTERWARDS. OnStart picks the body model
// and animation tier off the variant, so a later assignment produces a
// walker running at dog speed — the bug this command shipped with.
var z = ZombieCommands.SpawnAt( scene, pos, variant );
if ( !z.IsValid() ) continue;
z.GameObject.Name = "Hellhound";
made++;
}
// ⚠️ Deliberately does NOT claim the hound is correct. The renderer is
// built in OnStart, a frame from now, so nothing here can see what model
// it actually resolved to. `nz_hound_info` is the check that reads the
// live object — a spawn command reporting its own success is how the
// walker-bodied hound passed for working in the first place.
Log.Info( $"[nz] spawned {made} hellhound(s) — variant '{variant.ResourceName}' "
+ "· run nz_hound_info to confirm the body and clips" );
}
/// <summary>What the live hounds ACTUALLY resolved to: `nz_hound_info`.
/// Reads the model and sequence off the renderer rather than repeating what
/// the spawn asked for.</summary>
[ConCmd( "nz_hound_info" )]
public static void Info()
{
var scene = Game.ActiveScene;
if ( !scene.IsValid() ) { Log.Warning( "[nz] no active scene" ); return; }
var all = scene.GetAllComponents<ZombieAI>().ToList();
if ( all.Count == 0 ) { Log.Info( "[nz-hound] no zombies alive" ); return; }
foreach ( var z in all )
{
var r = z.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );
var model = r?.Model?.ResourcePath ?? "(no renderer yet)";
var seq = r?.Model is null ? "-" : r.Sequence?.Name ?? "-";
var variant = z.Variant?.ResourceName ?? "(none — plain walker)";
Log.Info( $"[nz-hound] {z.GameObject.Name,-10} variant={variant,-12} "
+ $"seq={seq,-20} {model}" );
}
}
/// <summary>Where the hellhound variant lives.</summary>
public const string VariantPath = "zombies/hellhound.zvar";
/// <summary>Remove the test hounds: `nz_hound_clear`.</summary>
[ConCmd( "nz_hound_clear" )]
public static void Clear()
{
int n = 0;
foreach ( var go in Game.ActiveScene?.GetAllObjects( true )
.Where( o => o.Name == "hound_test" ).ToList() ?? new() )
{
go.Destroy();
n++;
}
Log.Info( $"[nz] removed {n} test hound(s)" );
}
}