Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -7085,6 +7085,33 @@ is **pre-approved** (keys in `tools/ai-assets/.env`, run via `uv`).

---

## ✅ Done (2026-08-04): NPCs grounded, brighter and individual (#711)

Three related NPC problems fixed in one branch (issue #711):

- **Floating NPCs (regression from #492)** — settlement/camp markers sit centred in the air cell
above the floor (+0.5 from the cell-centre conversion); the settlement spawner's
`Max(Min.Y+1, markerY)` clamp never fired, so every settlement NPC hovered exactly half a block
over the floor. Now `Floor()`ed like the station-crew spawner (template upper-floor markers keep
their storey). Bandit-camp guards got the same `Floor()`.
- **Real-block grounding** — bandits + planet enemies snapped to the pure noise
`SurfaceHeight`, blind to stamped structures and player edits (guards floated over carved camp
floors). They now use the creatures' `GroundFeetYAt` probe (new `TryGroundFeetYAt` variant);
settlement NPCs re-ground each step (±2 blocks, doorsteps lift them, mined-out floors drop them)
with the home floor Y as the unloaded-chunk fallback — never the noise surface.
- **Uniform darkness** — the tintable avatar textures averaged ~100/255 grey and LitColor computes
`_Color * tex`, so every avatar rendered at ~40 % of its authored brightness; the NPC outfit
palette was dark on top. Textures are now mean-normalised to ~200/255 at load, the outfit
palettes were widened 3 → 6 tones per theme and lifted, and bandit/Guardian materials get the
standard `_Floor 0.62 / _Fill 0.3` (they ran at the 0.35/0 shader defaults — ~2× darker than
every other humanoid on the shadow side).
- **Identical clones** — per-NPC `Size` 0.92–1.08 (was hardcoded 1), independent trouser colour
(additive `NetNpc.LegsRgb`, 0 = old-server fallback), android chassis tone spread + only ~60 % of
researchers are robots now, and client-side deterministic per-NPC face jitter + hair (7 tones,
some bald) seeded from id+name via a stable hash (string `GetHashCode` is per-process randomised).
- **Tests** — settlement NPC feet must land on the floored marker Y (regression guard); stroll
leash assert switched to horizontal (the code leash is XZ-only) + a ±3 vertical bound.

## ✅ Done (2026-08-04): worldgen perf pass — per-world wonder profile (#712)

The #698–#709 wave made `SurfaceHeight` ~2.6× costlier (PR CI test job 6.5 → 17 min; same cost in
Expand Down Expand Up @@ -7146,7 +7173,7 @@ like #576–#580; **continents are NEW WORLDS ONLY** via `WorldDescription.Terra
escarpment storeys, caldera rim/floor, crater-trait distribution, chains, salt-ridge fraction,
continents gating (flag/size/ocean-identity) + bimodality + basin sea, band compat (tier-0 ==
classic query), cenote sheerness, tunnel determinism + surface mouths, chunk smoke on all wonder
worlds. Status: **local branch `terrain-wonders` only — NO PR yet** (user decision).
worlds. Status: **merged to `main` via PR #710 (2026-08-04)**.

## ✅ Done (2026-08-03): beaches along seas and larger lakes (#679)

Expand Down
40 changes: 36 additions & 4 deletions client/Assets/BlocksBeyondTheStars/Scripts/NpcView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,18 @@ private void OnNpcs(NpcList m)
go.transform.position = Game != null ? Game.ScenePos(nd.X, nd.Y, nd.Z) : new Vector3(nd.X, nd.Y, nd.Z);

var avatar = go.AddComponent<PlayerAvatar>();
Color skin = nd.IsRobot ? new Color(0.75f, 0.78f, 0.82f) : Rgb(nd.SkinRgb);
// The server owns the whole palette (#711): skin covers robot chassis tones too, and
// trousers are an independent colour (LegsRgb 0 = older server → derive from the outfit).
Color skin = nd.SkinRgb != 0 ? Rgb(nd.SkinRgb) : new Color(0.75f, 0.78f, 0.82f);
Color outfit = Rgb(nd.OutfitRgb);
// Deliberately NO spacesuit: civilians stay bare-headed so suited players are
// recognisable at a glance (suit = player, bare face = NPC, face mask = bandit).
avatar.Build(skin, outfit, outfit * 0.85f, outfit * 0.7f);
Color legs = nd.LegsRgb != 0 ? Rgb(nd.LegsRgb) : outfit * 0.7f;
// Per-NPC variety (#711), deterministic from the NPC id + name so it survives rejoins:
// a face-feature jitter and a hair colour (some stay bald); androids keep the uniform
// manufactured look. Deliberately NO spacesuit: civilians stay bare-headed so suited
// players are recognisable at a glance (suit = player, bare face = NPC, mask = bandit).
int seed = unchecked((nd.Id * 486187739) ^ StableHash(nd.Name));
Color? hair = !nd.IsRobot && (seed & 0x7) != 0 ? HairTones[(int)((uint)(seed >> 8) % (uint)HairTones.Length)] : (Color?)null;
avatar.Build(skin, outfit, outfit * 0.9f, legs, spacesuit: false, variantSeed: nd.IsRobot ? 0 : seed, hair: hair);
avatar.SetVisible(true);

if (nd.Size > 0f && !Mathf.Approximately(nd.Size, 1f))
Expand Down Expand Up @@ -228,5 +235,30 @@ private void OnDestroy()

private static Color Rgb(uint rgb)
=> new Color(((rgb >> 16) & 0xFF) / 255f, ((rgb >> 8) & 0xFF) / 255f, (rgb & 0xFF) / 255f);

/// <summary>Order-dependent string hash that is stable across sessions and machines — .NET string
/// hash codes are randomised per process, which would re-roll every NPC's hair each login.</summary>
private static int StableHash(string s)
{
int h = 17;
foreach (char c in s ?? string.Empty)
{
h = unchecked(h * 31 + c);
}

return h;
}

/// <summary>Civilian hair tones — muted human colours (no reds: red accents belong to the Guardians).</summary>
private static readonly Color[] HairTones =
{
new Color(0.10f, 0.09f, 0.08f), // black
new Color(0.28f, 0.19f, 0.12f), // dark brown
new Color(0.45f, 0.32f, 0.18f), // chestnut
new Color(0.62f, 0.48f, 0.25f), // dark blond
new Color(0.75f, 0.65f, 0.40f), // blond
new Color(0.55f, 0.55f, 0.55f), // grey
new Color(0.35f, 0.20f, 0.12f), // auburn
};
}
}
80 changes: 71 additions & 9 deletions client/Assets/BlocksBeyondTheStars/Scripts/PlayerAvatar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public sealed class PlayerAvatar : MonoBehaviour

public void Build(ClientSettings s) => Build(s.SkinColor, s.TorsoColor, s.ArmColor, s.LegColor, spacesuit: true);

public void Build(Color skin, Color torso, Color arms, Color legs, bool spacesuit = false)
public void Build(Color skin, Color torso, Color arms, Color legs, bool spacesuit = false, int variantSeed = 0, Color? hair = null)
{
EnsureTextures();
_suit = spacesuit;
Expand All @@ -74,6 +74,21 @@ public void Build(Color skin, Color torso, Color arms, Color legs, bool spacesui
_arms = Lit(arms, _suitTex);
_legs = Lit(legs, _suitTex);

// Per-NPC face variation (#711): tiny deterministic offsets from the seed so a settlement crowd
// isn't a row of identical clones. Seed 0 (players, previews) keeps the exact stock face.
float eyeDX = 0.11f, eyeY = 0.075f, browY = 0.175f, browW = 0.38f;
float mouthW = 0.20f, mouthY = -0.175f, pupilW = 0.085f;
if (variantSeed != 0)
{
eyeDX += Jit(variantSeed, 1) * 0.02f;
eyeY += Jit(variantSeed, 2) * 0.02f;
browY += Jit(variantSeed, 3) * 0.025f;
browW += Jit(variantSeed, 4) * 0.06f;
mouthW += Jit(variantSeed, 5) * 0.06f;
mouthY += Jit(variantSeed, 6) * 0.015f;
pupilW += Jit(variantSeed, 7) * 0.012f;
}

// Torso, tapered: pelvis → abdomen → wider chest.
AddCube("Pelvis", transform, new Vector3(0f, 0.97f, 0f), new Vector3(0.46f, 0.22f, 0.28f), _legs);
AddCube("Abdomen", transform, new Vector3(0f, 1.18f, 0f), new Vector3(0.46f, 0.26f, 0.30f), _torso);
Expand All @@ -94,19 +109,29 @@ public void Build(Color skin, Color torso, Color arms, Color legs, bool spacesui
// Eyes (whites + pupils + a brow + a mouth) so the face reads clearly — bigger/clearer (B20).
var eyeWhite = Lit(new Color(0.96f, 0.97f, 1f), null);
var pupil = Lit(new Color(0.04f, 0.04f, 0.07f), null);
var brow = Lit(new Color(0.18f, 0.14f, 0.11f), null);
var brow = hair is { } bc ? Lit(bc, null) : Lit(new Color(0.18f, 0.14f, 0.11f), null);
var mouth = Lit(new Color(0.32f, 0.16f, 0.14f), null);
// Default procedural features — collected so a custom pixel face (SetFace) can hide them. The
// visor (above) is included so a drawn face fully replaces the stock look.
// z values are head-LOCAL and must clear the head front (0.5); the +0.255 shift over the old
// 0.235–0.275 lifts them just proud of the surface while preserving the relief (pupils ahead of the
// whites, etc.). See the Visor note above.
_faceFeatures.Add(AddCube("EyeL", _head, new Vector3(-0.11f, 0.075f, 0.50f), new Vector3(0.17f, 0.13f, 0.05f), eyeWhite));
_faceFeatures.Add(AddCube("EyeR", _head, new Vector3(0.11f, 0.075f, 0.50f), new Vector3(0.17f, 0.13f, 0.05f), eyeWhite));
_faceFeatures.Add(AddCube("PupilL", _head, new Vector3(-0.11f, 0.06f, 0.53f), new Vector3(0.085f, 0.10f, 0.03f), pupil));
_faceFeatures.Add(AddCube("PupilR", _head, new Vector3(0.11f, 0.06f, 0.53f), new Vector3(0.085f, 0.10f, 0.03f), pupil));
_faceFeatures.Add(AddCube("Brow", _head, new Vector3(0f, 0.175f, 0.50f), new Vector3(0.38f, 0.05f, 0.045f), brow));
_faceFeatures.Add(AddCube("Mouth", _head, new Vector3(0f, -0.175f, 0.49f), new Vector3(0.20f, 0.045f, 0.04f), mouth));
_faceFeatures.Add(AddCube("EyeL", _head, new Vector3(-eyeDX, eyeY, 0.50f), new Vector3(0.17f, 0.13f, 0.05f), eyeWhite));
_faceFeatures.Add(AddCube("EyeR", _head, new Vector3(eyeDX, eyeY, 0.50f), new Vector3(0.17f, 0.13f, 0.05f), eyeWhite));
_faceFeatures.Add(AddCube("PupilL", _head, new Vector3(-eyeDX, eyeY - 0.015f, 0.53f), new Vector3(pupilW, 0.10f, 0.03f), pupil));
_faceFeatures.Add(AddCube("PupilR", _head, new Vector3(eyeDX, eyeY - 0.015f, 0.53f), new Vector3(pupilW, 0.10f, 0.03f), pupil));
_faceFeatures.Add(AddCube("Brow", _head, new Vector3(0f, browY, 0.50f), new Vector3(browW, 0.05f, 0.045f), brow));
_faceFeatures.Add(AddCube("Mouth", _head, new Vector3(0f, mouthY, 0.49f), new Vector3(mouthW, 0.045f, 0.04f), mouth));

// Optional hair cap + back (civilian NPCs): head-LOCAL units — the head is a 0.46-scaled unit
// cube, so anything wrapping its ±0.5 surfaces needs a scale > 1 (see the face-feature note).
// NOT in _faceFeatures: a custom pixel face replaces the face, not the hair.
if (hair is { } hairCol)
{
var hairMat = Lit(hairCol, null);
AddCube("HairTop", _head, new Vector3(0f, 0.56f, -0.02f), new Vector3(1.08f, 0.14f, 1.06f), hairMat);
AddCube("HairBack", _head, new Vector3(0f, 0.18f, -0.53f), new Vector3(1.08f, 0.92f, 0.10f), hairMat);
}

// Jointed arms (shoulder → elbow → hand) and legs (hip → knee → foot).
_armL = AddArm("ArmLeft", -0.32f, out _elbowL, out _);
Expand Down Expand Up @@ -573,16 +598,53 @@ private static Texture2D LoadTex(string key)
return null;
}

// Normalise the greyscale tint texture toward white (#711): the authored bytes average ~100/255,
// and LitColor computes _Color * tex — so every avatar surface rendered at ~40 % of its tint's
// perceptual brightness and whole outfits sank to near-black. Scaling the mean to ~200/255 keeps
// the pixel detail (weave, panels) but stops the texture eating the colour.
var data = (byte[])asset.bytes.Clone();
long sum = 0;
for (int i = 0; i < data.Length; i += 4)
{
sum += data[i] + data[i + 1] + data[i + 2];
}

float mean = sum / (data.Length * 3f / 4f);
if (mean > 1f && mean < 200f)
{
float k = 200f / mean;
for (int i = 0; i < data.Length; i += 4)
{
data[i] = (byte)Mathf.Min(255f, data[i] * k);
data[i + 1] = (byte)Mathf.Min(255f, data[i + 1] * k);
data[i + 2] = (byte)Mathf.Min(255f, data[i + 2] * k);
}
}

var tex = new Texture2D(64, 64, TextureFormat.RGBA32, false)
{
wrapMode = TextureWrapMode.Repeat,
filterMode = FilterMode.Point,
};
tex.LoadRawTextureData(asset.bytes);
tex.LoadRawTextureData(data);
tex.Apply();
return tex;
}

/// <summary>Deterministic jitter in [-1, 1] from a (seed, salt) pair — stable across sessions and
/// machines (unlike string hash codes), so an NPC keeps the same face every visit.</summary>
private static float Jit(int seed, int salt)
{
unchecked
{
uint h = (uint)(seed * 73856093) ^ (uint)(salt * 19349663);
h ^= h >> 13;
h *= 0x5bd1e995;
h ^= h >> 15;
return ((h & 0xFFFF) / 32767.5f) - 1f;
}
}

// Ambient floor + opposite-flank fill for avatar materials, same failure mode and values as
// creatures (see CreatureBuilder): LitColor's single FIXED key light leaves camera-away /
// backlit faces at the floor, and in Linear colour space the tinted suit textures then sink
Expand Down
39 changes: 29 additions & 10 deletions client/Assets/BlocksBeyondTheStars/Scripts/WorldEntities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -540,18 +540,37 @@ private static void EnsureBanditMaterials()

var lit = Shader.Find("BlocksBeyondTheStars/LitColor") ?? Shader.Find("Unlit/Color");
var unlit = Shader.Find("Unlit/Color") ?? lit;
// Skin and cloth are TEMPLATES — every bandit gets its own tinted instance (see BuildBandit).
_banditSkinMat = new Material(lit) { color = ShaderColor.Srgb(new Color(0.78f, 0.6f, 0.45f)) };
_banditClothMat = new Material(lit) { color = ShaderColor.Srgb(new Color(0.3f, 0.25f, 0.2f)) };
_banditDarkMat = new Material(lit) { color = ShaderColor.Srgb(new Color(0.14f, 0.13f, 0.14f)) };
_banditEyeMat = new Material(lit) { color = ShaderColor.Srgb(new Color(0.95f, 0.96f, 0.98f)) }; // eye whites — LIT, so they never glow
_banditEnergyMat = new Material(unlit) { color = ShaderColor.Srgb(BanditEnergyColor) }; // cold blue weapon glow (bloom picks it up)
// Skin and cloth are TEMPLATES — every bandit gets its own tinted instance (see BuildBandit),
// which copies the _Floor/_Fill lift along with the rest of the material.
_banditSkinMat = WithFill(new Material(lit) { color = ShaderColor.Srgb(new Color(0.78f, 0.6f, 0.45f)) });
_banditClothMat = WithFill(new Material(lit) { color = ShaderColor.Srgb(new Color(0.3f, 0.25f, 0.2f)) });
_banditDarkMat = WithFill(new Material(lit) { color = ShaderColor.Srgb(new Color(0.14f, 0.13f, 0.14f)) });
_banditEyeMat = WithFill(new Material(lit) { color = ShaderColor.Srgb(new Color(0.95f, 0.96f, 0.98f)) }); // eye whites — LIT, so they never glow
_banditEnergyMat = new Material(unlit) { color = ShaderColor.Srgb(BanditEnergyColor) }; // cold blue weapon glow (bloom picks it up)
}

/// <summary>The bandits' weapon/tracer glow: cold blue human tech, kept clearly apart from the Guardian
/// machines' red sensor glow (<see cref="_eyeMat"/>).</summary>
private static readonly Color BanditEnergyColor = new(0.30f, 0.68f, 1f);

// LitColor's shader DEFAULTS are _Floor 0.35 / _Fill 0 — every other humanoid/creature material in
// the game runs at 0.62 / 0.3 (see PlayerAvatar.AvatarFloor); without this lift, bandits and
// Guardians rendered ~2× darker than everyone else on their shadow side (#711).
private const float EntityFloor = 0.62f;
private const float EntityFill = 0.3f;

/// <summary>Applies the shared ambient floor + fill to a LitColor material (no-op on the Unlit fallback).</summary>
private static Material WithFill(Material m)
{
if (m.HasProperty("_Floor"))
{
m.SetFloat("_Floor", EntityFloor);
m.SetFloat("_Fill", EntityFill);
}

return m;
}

private static Transform Pivot(Transform parent, Vector3 localPos)
{
var t = new GameObject("Pivot").transform;
Expand Down Expand Up @@ -590,16 +609,16 @@ private static void EnsureMaterials()
var lit = Shader.Find("BlocksBeyondTheStars/LitColor") ?? Shader.Find("Unlit/Color");
var unlit = Shader.Find("Unlit/Color") ?? lit;
var plateTex = LoadTex("enemy_robot"); // optional metal-plating tile (flat dark if absent)
_hideMat = new Material(lit) { color = ShaderColor.Srgb(new Color(0.13f, 0.14f, 0.16f)) }; // dark plating
_hideDarkMat = new Material(lit) { color = ShaderColor.Srgb(new Color(0.08f, 0.085f, 0.10f)) }; // darker joints
_hideMat = WithFill(new Material(lit) { color = ShaderColor.Srgb(new Color(0.13f, 0.14f, 0.16f)) }); // dark plating
_hideDarkMat = WithFill(new Material(lit) { color = ShaderColor.Srgb(new Color(0.08f, 0.085f, 0.10f)) }); // darker joints
if (plateTex != null)
{
_hideMat.mainTexture = plateTex;
_hideDarkMat.mainTexture = plateTex;
}

_clawMat = new Material(lit) { color = ShaderColor.Srgb(new Color(0.34f, 0.36f, 0.40f)) }; // metal trim / antennae / feet
_eyeMat = new Material(unlit) { color = ShaderColor.Srgb(new Color(1f, 0.18f, 0.14f)) }; // glowing red sensors (bloom picks it up)
_clawMat = WithFill(new Material(lit) { color = ShaderColor.Srgb(new Color(0.34f, 0.36f, 0.40f)) }); // metal trim / antennae / feet
_eyeMat = new Material(unlit) { color = ShaderColor.Srgb(new Color(1f, 0.18f, 0.14f)) }; // glowing red sensors (bloom picks it up)
}

private static Texture2D LoadTex(string key)
Expand Down
Loading