Files
Soul_Game/scripts/TradeSystem.cs
T
mmcghen ed78d560ff 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>
2026-07-21 13:07:33 -04:00

105 lines
4.1 KiB
C#

namespace WorldSim;
/// <summary>
/// Trade evaluation and execution (SPEC §9). South MVP: generous, voluntary,
/// no currency — a gift economy where the ratio favors the receiver.
///
/// Soul consequences: a generous completed exchange eases both parties'
/// imprints (external perception — the world pressed kindly on them).
/// Later soul types change the ratio logic only; the mechanism is shared.
/// </summary>
public static class TradeSystem
{
/// <summary>Trades since the last daily stats snapshot.</summary>
public static int TradesToday;
/// <summary>Refusals since the last daily stats snapshot.</summary>
public static int RefusalsToday;
/// <summary>
/// A hungry villager asks another for food, face to face.
///
/// Willingness comes from the soul (or from a warm history with this
/// particular person). Refusing the hungry while holding food is a
/// deliberate darkening of another — it marks the refuser's soul
/// (others-regard debris, per DESIGN.md) and leaves a cold echo in the
/// asker's memory. The world keeps score the only way it knows how.
/// </summary>
public static void RequestGift(Npc giver, Npc asker)
{
giver.Inventory.TryGetValue(MaterialKind.Food, out float giverFood);
bool hasFood = giverFood > 3f;
bool willing = giver.Soul.Generosity >= 0.3f || giver.GetWarmth(asker.Name) > 0.5f;
if (hasFood && willing)
{
Execute(giver, asker, MaterialKind.Food);
return;
}
if (hasFood && !willing)
{
// Refused with a full pack — the act that costs.
RefusalsToday++;
asker.RecordEcho(giver.Name, -0.25f);
giver.Imprint.OthersRegard = Godot.Mathf.Clamp(giver.Imprint.OthersRegard + 0.02f, 0f, 1f);
StatsLogger.LogRefusal(giver, asker);
}
// Empty-handed is not refusal: no debris, no echo. You cannot darken
// your soul with what you do not have.
}
/// <summary>
/// Need-based: the trigger is a person in need, not an inventory gap.
/// Food flows to the hungry first; raw material surplus vs. genuine
/// lack remains as a fallback.
/// </summary>
public static bool CanTrade(Npc giver, Npc receiver, out MaterialKind material)
{
material = MaterialKind.Food;
// The ungenerous do not give. Not from an empty pack — from a full one.
if (giver.Soul.Generosity < 0.3f) return false;
giver.Inventory.TryGetValue(MaterialKind.Food, out float giverFood);
receiver.Inventory.TryGetValue(MaterialKind.Food, out float receiverFood);
if (receiver.Nourishment < 0.6f && receiverFood < 2f && giverFood > 3f)
return true;
foreach (var kv in giver.Inventory)
{
bool receiverLacks = !receiver.Inventory.TryGetValue(kv.Key, out float has) || has < 2f;
if (kv.Value > 5f && receiverLacks) { material = kv.Key; return true; }
}
return false;
}
public static void Execute(Npc giver, Npc receiver, MaterialKind material)
{
TradesToday++;
// Generosity decides how much of the surplus is given.
float surplus = giver.Inventory[material] - 2f;
float given = surplus * giver.Soul.Generosity;
giver.Inventory[material] -= given;
receiver.Inventory.TryGetValue(material, out float has);
receiver.Inventory[material] = has + given;
StatsLogger.LogGift(giver, receiver, material, given);
// Warmth lands on both sides — more on the one who was fed.
receiver.RecordEcho(giver.Name, +0.15f);
giver.RecordEcho(receiver.Name, +0.05f);
// The kindness lands on both atmospheres — gently.
giver.Imprint.ExternalPerception -= 0.002f;
receiver.Imprint.ExternalPerception -= 0.002f;
giver.Imprint.Clamp();
receiver.Imprint.Clamp();
// TODO(Week 3): soul-imprint-weighted PARTNER selection (trusted vs
// tainted history) — the constrained-emergence reference pattern (SPEC §7).
}
}