Files
Soul_Game/scripts/Building.cs
T
mmcghen 10e772d087 Run 15: known-nodes exploration (addendum stage 1), per-town debug, O(n^2) fix
Exploration addendum stage 1: per-NPC known-nodes. NPCs gather only from
resources they've personally sensed (radius 22) or been told about;
founders seed with home ground, children inherit parents' maps, node
knowledge diffuses between neighbors like skills. Starving NPCs venture
outward and sense as they go - non-random needs-driven exploration, the
seed of first contact. Repopulation preserved (seeding + survival food
fallback): all towns fed, population stable 86-109 over 240 days.

Debug screen rebuilt: the ~68-row per-villager roster (overflowing at
four-town scale) replaced with a per-town summary - pop, fed%, knowledge,
debris, state breakdown, buildings, culture-colored, plus world
births/deaths. Scales to any population.

Performance: added phase timers and measured instead of guessing - npc.Tick
was 94% of cost, O(n^2) via TickResting calling RestingOccupancy (full
population scan) per building per resting NPC. Fixed with a once-per-tick
occupancy cache on Building. npc phase ~4x faster and now linear; full
240-day four-town run in 180s, faster than the pre-exploration baseline.
Profiling scaffolding removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 21:17:24 -04:00

224 lines
7.8 KiB
C#

using System.Collections.Generic;
using Godot;
namespace WorldSim;
public enum BuildingKind { Shelter, Granary }
/// <summary>
/// Modular buildings (SPEC §6): physically-sequential construction
/// (foundation → walls → roof) with multi-NPC material delivery.
///
/// Buildings are the world's metabolism:
/// Shelters — burn firewood daily, decay into ruins if neglected, and
/// accept expansion modules that permanently raise their burn.
/// Granary — the village's winter insurance: stores food surplus in the
/// abundant seasons, feeds the hungry when the land gives nothing.
/// Its walls are clay — the first real use for it.
/// </summary>
public class Building
{
public BuildingKind Kind = BuildingKind.Shelter;
public Vector2 Site;
/// <summary>Which settlement this building belongs to.</summary>
public string Town = "";
public static Building NewShelter(Vector2 site) => new()
{
Kind = BuildingKind.Shelter,
Site = site,
Stages = new()
{
new("foundation", MaterialKind.Stone, 10f),
new("walls", MaterialKind.Wood, 20f),
new("roof", MaterialKind.Wood, 10f),
},
};
public static Building NewGranary(Vector2 site) => new()
{
Kind = BuildingKind.Granary,
Site = site,
Stages = new()
{
new("foundation", MaterialKind.Stone, 10f),
new("walls", MaterialKind.Clay, 25f),
new("roof", MaterialKind.Wood, 10f),
},
};
// --- Construction ----------------------------------------------------
public record Stage(string Name, MaterialKind Material, float Cost);
public List<Stage> Stages = new();
public int CurrentStage;
public float Delivered; // toward the current stage's cost
public bool IsComplete => CurrentStage >= Stages.Count;
public MaterialKind? NeededMaterial => IsComplete ? null : Stages[CurrentStage].Material;
public float StageProgress =>
IsComplete ? 1f : Delivered / Stages[CurrentStage].Cost;
// --- Shelter: upkeep -------------------------------------------------
public const float UpkeepStockCap = 10f;
public float UpkeepStock; // delivered wood reserve
public float Condition = 1f; // 1 = sound; 0 = ruin
public bool IsRuined => Condition <= 0f;
/// <summary>Base burn plus half a unit per expansion module, forever.</summary>
public float DailyBurn => Kind == BuildingKind.Shelter ? 2f + Modules * 0.5f : 0f;
/// <summary>True when this shelter wants a firewood delivery.</summary>
public bool WantsUpkeepWood =>
Kind == BuildingKind.Shelter && IsComplete && !IsRuined &&
UpkeepStock < UpkeepStockCap * 0.7f;
// --- Shelter: expansion modules --------------------------------------
public const int ModuleCap = 4;
public const float ModuleCost = 15f; // wood per module
public int Modules;
public float ModuleDelivered; // toward the next module
/// <summary>Sound, stocked shelters accept expansion work.</summary>
public bool WantsExpansion =>
Kind == BuildingKind.Shelter && IsComplete && !IsRuined &&
Modules < ModuleCap && !WantsUpkeepWood;
/// <summary>How many can sleep here. Expansion adds beds — growth has a
/// human meaning, not just a bigger square.</summary>
/// <summary>Sleepers here, recomputed once per tick by GameManager —
/// so resting NPCs read a cached count instead of each re-scanning the
/// whole population per building (that was the sim's O(n²) hot spot).</summary>
public int Occupancy;
public int RestCapacity =>
Kind == BuildingKind.Shelter && IsComplete && !IsRuined ? 3 + Modules : 0;
// --- Granary: food store ---------------------------------------------
public const float FoodStockCap = 150f;
public float FoodStock;
/// <summary>True when the granary has room for more food.</summary>
public bool WantsFood =>
Kind == BuildingKind.Granary && IsComplete && FoodStock < FoodStockCap;
/// <summary>Take food out for a hungry villager. Returns amount granted.</summary>
public float WithdrawFood(float requested)
{
float granted = Mathf.Min(requested, FoodStock);
FoodStock -= granted;
return granted;
}
// --- Daily tick -------------------------------------------------------
/// <summary>Called once per in-game day by GameManager.</summary>
public void DailyUpkeep()
{
if (Kind != BuildingKind.Shelter || !IsComplete || IsRuined) return;
if (UpkeepStock >= DailyBurn)
{
UpkeepStock -= DailyBurn;
Condition = Mathf.Min(1f, Condition + 0.05f); // tended buildings mend
}
else
{
UpkeepStock = 0f;
Condition -= 0.1f; // neglect shows in ~10 days
}
}
// --- Delivery --------------------------------------------------------
/// <summary>
/// The visitor delivers what the building currently needs — construction
/// material while building; then firewood/expansion (shelter) or food
/// (granary). Returns false if they had nothing useful to give.
/// </summary>
public bool Deliver(Npc npc)
{
if (IsRuined) return false;
if (!IsComplete)
{
var stage = Stages[CurrentStage];
if (!npc.Inventory.TryGetValue(stage.Material, out float have) || have <= 0f)
return false;
// A practiced mason wastes less: the same materials go further
// in skilled hands — and the building is the teacher.
float masonry = npc.Know[KnowledgeDomain.Masonry];
float take = Mathf.Min(have, (stage.Cost - Delivered) / (1f + masonry * 0.5f));
npc.Inventory[stage.Material] = have - take;
Delivered += take * (1f + masonry * 0.5f);
npc.Know.Gain(KnowledgeDomain.Masonry, 0.002f);
if (Delivered >= stage.Cost)
{
CurrentStage++;
Delivered = 0f;
}
return take > 0f;
}
if (Kind == BuildingKind.Granary)
{
// Deposit real surplus only (keep 5) — and never redeposit what
// was just withdrawn: depositors must arrive carrying > 8.
if (!npc.Inventory.TryGetValue(MaterialKind.Food, out float carried) || carried <= 8f)
return false;
float space = FoodStockCap - FoodStock;
float taken = Mathf.Min(carried - 5f, space);
if (taken <= 0f) return false;
npc.Inventory[MaterialKind.Food] = carried - taken;
FoodStock += taken;
return true;
}
// Shelter: reserve first — never expand a shelter you can't heat.
if (!npc.Inventory.TryGetValue(MaterialKind.Wood, out float wood) || wood <= 0f)
return false;
float woodSpace = UpkeepStockCap - UpkeepStock;
if (woodSpace > 0f)
{
float taken = Mathf.Min(wood, woodSpace);
npc.Inventory[MaterialKind.Wood] = wood - taken;
UpkeepStock += taken;
return taken > 0f;
}
if (Modules < ModuleCap)
{
float take = Mathf.Min(wood, ModuleCost - ModuleDelivered);
npc.Inventory[MaterialKind.Wood] = wood - take;
ModuleDelivered += take;
if (ModuleDelivered >= ModuleCost)
{
Modules++;
ModuleDelivered = 0f;
}
return take > 0f;
}
return false;
}
// Ruins persist in the world — they are not removed. What a place was
// remains visible in what it left behind.
}