Soul layer: imprints, warmth echoes, and consequences
Ports the Unity POC soul systems into the sim: - Warmth echoes: villagers remember who fed them (+0.15) and who turned them away (-0.25). Ask-for-food targeting runs on memory, not omniscience; the remembered-cold are not asked. Echoes fade daily. - Refusing the hungry while holding food marks the refuser (others-regard debris) and is logged; empty-handed is not refusal. - Harvesting below the 50% commons line marks the taker per unit taken. - Soul-to-soul atmospheric pressure (SoulFieldSystem port): encased souls darken nearby atmospheres, clear souls lighten them. - Debris costs biologically: nourishment drains faster and meals restore less as the soul encases. - Instrumentation: avg_debris + refusals in daily stats; per-villager nourish/health/debris; refusals in the contact log; map dots dim with encasement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+36
-11
@@ -52,6 +52,26 @@ public class Npc
|
||||
/// souls honor the 50% commons floor; selfish souls strip to ~15%.</summary>
|
||||
public float HarvestSeekFloor => Mathf.Max(0.05f, Soul.Sustainability * 0.5f) + 0.05f;
|
||||
|
||||
// --- Social memory: warmth echoes (Unity POC port) --------------------
|
||||
// Not knowledge — feeling. A villager doesn't know who is generous;
|
||||
// they remember who fed them and who turned them away.
|
||||
|
||||
private readonly Dictionary<string, float> _echoes = new();
|
||||
|
||||
public float GetWarmth(string name) => _echoes.TryGetValue(name, out float w) ? w : 0f;
|
||||
|
||||
public void RecordEcho(string name, float delta) =>
|
||||
_echoes[name] = Mathf.Clamp(GetWarmth(name) + delta, -1f, 1f);
|
||||
|
||||
/// <summary>Echoes drift back toward neutral — called once per day.
|
||||
/// A single contact fades in weeks; a pattern of contact holds.</summary>
|
||||
public void FadeEchoes(float amount)
|
||||
{
|
||||
var keys = new List<string>(_echoes.Keys);
|
||||
foreach (var key in keys)
|
||||
_echoes[key] = Mathf.MoveToward(_echoes[key], 0f, amount);
|
||||
}
|
||||
|
||||
/// <summary>Ticks spent in each state since the last daily snapshot —
|
||||
/// the raw material of emergent roles: what a person mostly does is
|
||||
/// who they are becoming. Indexed by (int)NpcState.</summary>
|
||||
@@ -107,13 +127,16 @@ public class Npc
|
||||
}
|
||||
|
||||
// Metabolism: nourishment drains slowly; eat from the pack when hungry.
|
||||
// This is the per-capita demand that keeps the economy from saturating.
|
||||
Nourishment -= 0.35f / 1440f;
|
||||
// The soul layer bites here (Unity POC rule): a burdened soul wears
|
||||
// faster, and the same meal gives it less. Debris is not a scoreboard —
|
||||
// it is a way of being that costs.
|
||||
float debris = Imprint.Total;
|
||||
Nourishment -= (0.35f / 1440f) * (1f + debris);
|
||||
if (Nourishment < 0.7f &&
|
||||
Inventory.TryGetValue(MaterialKind.Food, out float food) && food >= 1f)
|
||||
{
|
||||
Inventory[MaterialKind.Food] = food - 1f;
|
||||
Nourishment = Mathf.Min(1f, Nourishment + 0.25f);
|
||||
Nourishment = Mathf.Min(1f, Nourishment + 0.25f * (1f - debris * 0.5f));
|
||||
}
|
||||
if (Nourishment <= 0.05f) Health = Mathf.Max(0f, Health - 0.01f);
|
||||
Nourishment = Mathf.Clamp(Nourishment, 0f, 1f);
|
||||
@@ -244,8 +267,7 @@ public class Npc
|
||||
MoveToward(_giftSource.Position);
|
||||
if (Position.DistanceTo(_giftSource.Position) < ArriveDist)
|
||||
{
|
||||
if (TradeSystem.CanTrade(_giftSource, this, out var mat))
|
||||
TradeSystem.Execute(_giftSource, this, mat);
|
||||
TradeSystem.RequestGift(_giftSource, this);
|
||||
_giftSource = null;
|
||||
}
|
||||
return;
|
||||
@@ -280,15 +302,18 @@ public class Npc
|
||||
return;
|
||||
}
|
||||
|
||||
float bestDist = float.MaxValue;
|
||||
// Ask whoever might help — chosen by memory, not omniscience.
|
||||
// The warm are asked first; the remembered-cold aren't asked
|
||||
// at all. A stranger gets the benefit of the doubt once.
|
||||
float bestScore = float.MinValue;
|
||||
foreach (var other in gm.Npcs)
|
||||
{
|
||||
if (other == this) continue;
|
||||
// Everyone knows better than to ask the ungenerous.
|
||||
if (other.Soul.Generosity < 0.3f) continue;
|
||||
float warmth = GetWarmth(other.Name);
|
||||
if (warmth < -0.3f) continue;
|
||||
if (!other.Inventory.TryGetValue(MaterialKind.Food, out float f) || f <= 3f) continue;
|
||||
float d = Position.DistanceSquaredTo(other.Position);
|
||||
if (d < bestDist) { bestDist = d; _giftSource = other; }
|
||||
float score = warmth * 40f - Position.DistanceTo(other.Position);
|
||||
if (score > bestScore) { bestScore = score; _giftSource = other; }
|
||||
}
|
||||
if (_giftSource != null) return;
|
||||
}
|
||||
@@ -308,7 +333,7 @@ public class Npc
|
||||
else
|
||||
{
|
||||
Activity = $"harvesting {_targetNode.Kind.ToString().ToLower()}";
|
||||
float taken = world.Harvest(_targetNode, requested: 2f, Soul);
|
||||
float taken = world.Harvest(_targetNode, requested: 2f, this);
|
||||
if (taken <= 0f) { _targetNode = null; return; } // node at its floor — re-seek
|
||||
Inventory.TryGetValue(_targetNode.Kind, out float have);
|
||||
Inventory[_targetNode.Kind] = have + taken;
|
||||
|
||||
Reference in New Issue
Block a user