122 lines
4.7 KiB
C#
122 lines
4.7 KiB
C#
|
|
using System.Text.Json.Serialization;
|
||
|
|
|
||
|
|
namespace Theriapolis.Core.Data;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Immutable NPC stat-block template loaded from npc_templates.json.
|
||
|
|
/// Phase 5 instantiates one per <see cref="Tactical.SpawnKind"/> per
|
||
|
|
/// chunk, with the actual template id chosen via the per-zone lookup
|
||
|
|
/// table on <see cref="NpcTemplateContent.SpawnKindToTemplateByZone"/>.
|
||
|
|
/// </summary>
|
||
|
|
public sealed record NpcTemplateDef
|
||
|
|
{
|
||
|
|
[JsonPropertyName("id")]
|
||
|
|
public string Id { get; init; } = "";
|
||
|
|
|
||
|
|
[JsonPropertyName("name")]
|
||
|
|
public string Name { get; init; } = "";
|
||
|
|
|
||
|
|
/// <summary>Body size category, snake_case (small / medium / medium_large / large).</summary>
|
||
|
|
[JsonPropertyName("size")]
|
||
|
|
public string Size { get; init; } = "medium";
|
||
|
|
|
||
|
|
/// <summary>STR/DEX/CON/INT/WIS/CHA absolute values (10 = average).</summary>
|
||
|
|
[JsonPropertyName("ability_scores")]
|
||
|
|
public Dictionary<string, int> AbilityScores { get; init; } = new();
|
||
|
|
|
||
|
|
[JsonPropertyName("hp")]
|
||
|
|
public int Hp { get; init; } = 1;
|
||
|
|
|
||
|
|
[JsonPropertyName("ac")]
|
||
|
|
public int Ac { get; init; } = 10;
|
||
|
|
|
||
|
|
[JsonPropertyName("speed_ft")]
|
||
|
|
public int SpeedFt { get; init; } = 30;
|
||
|
|
|
||
|
|
[JsonPropertyName("attacks")]
|
||
|
|
public NpcAttack[] Attacks { get; init; } = Array.Empty<NpcAttack>();
|
||
|
|
|
||
|
|
/// <summary>Behavior id ("brigand", "wild_animal", "poi_guard"). Maps to <c>INpcBehavior</c> in Phase 5 M5.</summary>
|
||
|
|
[JsonPropertyName("behavior")]
|
||
|
|
public string Behavior { get; init; } = "brigand";
|
||
|
|
|
||
|
|
/// <summary>Starts as Hostile / Neutral / Friendly. Phase 5 reads this on instantiation.</summary>
|
||
|
|
[JsonPropertyName("default_allegiance")]
|
||
|
|
public string DefaultAllegiance { get; init; } = "hostile";
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Phase 6 M5 — faction id this NPC owes allegiance to (matches
|
||
|
|
/// FactionDef.Id). Empty for unaligned templates (wild animals,
|
||
|
|
/// brigands). Drives M5 patrol-aggression: a non-hostile NPC with a
|
||
|
|
/// faction flips to Hostile when the player's local standing with
|
||
|
|
/// that faction crosses the HOSTILE threshold.
|
||
|
|
/// </summary>
|
||
|
|
[JsonPropertyName("faction")]
|
||
|
|
public string Faction { get; init; } = "";
|
||
|
|
|
||
|
|
/// <summary>Loot table id (Phase 5 ships ~5; lookup deferred to M6).</summary>
|
||
|
|
[JsonPropertyName("loot_table")]
|
||
|
|
public string LootTable { get; init; } = "";
|
||
|
|
|
||
|
|
[JsonPropertyName("xp_award")]
|
||
|
|
public int XpAward { get; init; } = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
public sealed record NpcAttack
|
||
|
|
{
|
||
|
|
[JsonPropertyName("name")]
|
||
|
|
public string Name { get; init; } = "";
|
||
|
|
|
||
|
|
[JsonPropertyName("to_hit")]
|
||
|
|
public int ToHit { get; init; } = 0;
|
||
|
|
|
||
|
|
/// <summary>Damage dice expression (e.g. "1d6+2", "2d8").</summary>
|
||
|
|
[JsonPropertyName("damage")]
|
||
|
|
public string Damage { get; init; } = "";
|
||
|
|
|
||
|
|
[JsonPropertyName("damage_type")]
|
||
|
|
public string DamageType { get; init; } = "bludgeoning";
|
||
|
|
|
||
|
|
[JsonPropertyName("reach_tiles")]
|
||
|
|
public int ReachTiles { get; init; } = 1;
|
||
|
|
|
||
|
|
[JsonPropertyName("range_short_tiles")]
|
||
|
|
public int RangeShortTiles { get; init; } = 0;
|
||
|
|
|
||
|
|
[JsonPropertyName("range_long_tiles")]
|
||
|
|
public int RangeLongTiles { get; init; } = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Top-level wrapper for npc_templates.json: the template list plus the
|
||
|
|
/// per-spawnkind, per-zone template-id lookup table.
|
||
|
|
/// </summary>
|
||
|
|
public sealed record NpcTemplateContent
|
||
|
|
{
|
||
|
|
[JsonPropertyName("templates")]
|
||
|
|
public NpcTemplateDef[] Templates { get; init; } = Array.Empty<NpcTemplateDef>();
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// SpawnKind name (e.g. "Brigand") → array of template ids indexed by
|
||
|
|
/// DangerZone (0..4). Length should equal <c>C.DANGER_ZONE_MAX + 1</c>.
|
||
|
|
/// </summary>
|
||
|
|
[JsonPropertyName("spawn_kind_to_template_by_zone")]
|
||
|
|
public Dictionary<string, string[]> SpawnKindToTemplateByZone { get; init; } = new();
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Phase 7 M2 — per-dungeon-type override. Resolves a
|
||
|
|
/// <c>(PoiType, SpawnKind)</c> pair to a single template id, used by
|
||
|
|
/// <see cref="Theriapolis.Core.Dungeons.DungeonPopulator"/> when filling
|
||
|
|
/// in-room encounter slots. Per Phase 7 plan §10 open-decision #6:
|
||
|
|
/// DungeonType supersedes DangerZone entirely once the player is inside
|
||
|
|
/// a dungeon, so this map's value is a single template id (no zone
|
||
|
|
/// indexing). Outer key matches the <see cref="World.PoiType"/> enum
|
||
|
|
/// name (e.g. <c>"ImperiumRuin"</c>); inner key is the spawn-kind name
|
||
|
|
/// (e.g. <c>"PoiGuard"</c> / <c>"WildAnimal"</c> / <c>"Brigand"</c> /
|
||
|
|
/// <c>"Boss"</c>). Missing keys fall back to
|
||
|
|
/// <see cref="SpawnKindToTemplateByZone"/> at <c>DangerZone</c>=2 mid.
|
||
|
|
/// </summary>
|
||
|
|
[JsonPropertyName("spawn_kind_to_template_by_dungeon_type")]
|
||
|
|
public Dictionary<string, Dictionary<string, string>> SpawnKindToTemplateByDungeonType { get; init; } = new();
|
||
|
|
}
|