b451f83174
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
224 lines
8.6 KiB
C#
224 lines
8.6 KiB
C#
using System.Text.Json.Serialization;
|
||
|
||
namespace Theriapolis.Core.Data;
|
||
|
||
/// <summary>
|
||
/// Phase 7 M0 — a single hand-authored dungeon room. Loaded from
|
||
/// <c>Content/Data/room_templates/<type>/*.json</c>.
|
||
///
|
||
/// A template describes:
|
||
/// - The room's footprint in tactical tiles (perimeter walls included).
|
||
/// - The 2D ASCII <see cref="Grid"/>: one char per tactical tile.
|
||
/// Legend (per Phase 7 plan §5.1):
|
||
/// <c>#</c> wall, <c>.</c> floor, <c>,</c> rubble, <c>D</c> door slot,
|
||
/// <c>@</c> encounter slot, <c>C</c> container slot, <c>T</c> trap slot,
|
||
/// <c>P</c> pillar, <c>B</c> brazier, <c>M</c> mosaic (narrative),
|
||
/// <c>S</c> stairs (entry/exit only).
|
||
/// - Door positions on the perimeter (one entry per <c>D</c> in the grid).
|
||
/// - Encounter / container / trap slot positions (which the dungeon
|
||
/// populator fills with NPCs and loot).
|
||
/// - Optional narrative text surfaced by Scent Literacy / room-clear coda.
|
||
/// - The clade that <see cref="BuiltBy"/> the room — drives the
|
||
/// clade-responsive movement multiplier (Phase 7 plan §5.4).
|
||
///
|
||
/// Templates are designer-friendly to author: edit ASCII art + a couple
|
||
/// of metadata blocks. <see cref="ContentLoader.LoadRoomTemplates"/>
|
||
/// validates grid dimensions vs declared footprint, perimeter walls,
|
||
/// and that every <c>D</c>/<c>@</c>/<c>C</c>/<c>T</c> in the grid has
|
||
/// a matching slot record.
|
||
/// </summary>
|
||
public sealed record RoomTemplateDef
|
||
{
|
||
[JsonPropertyName("id")]
|
||
public string Id { get; init; } = "";
|
||
|
||
[JsonPropertyName("name")]
|
||
public string Name { get; init; } = "";
|
||
|
||
/// <summary>
|
||
/// Dungeon type the template belongs to: <c>imperium</c>, <c>mine</c>,
|
||
/// <c>cult</c>, <c>cave</c>, <c>overgrown</c>. Layout matchers filter
|
||
/// templates by type when assembling a dungeon.
|
||
/// </summary>
|
||
[JsonPropertyName("type")]
|
||
public string Type { get; init; } = "";
|
||
|
||
/// <summary>
|
||
/// Clade that built or originally inhabited the room. Drives Phase 7
|
||
/// clade-responsive movement (a Large PC in a Mustelid tunnel takes 2×
|
||
/// movement points). Allowed: <c>canid</c>, <c>felid</c>, <c>mustelid</c>,
|
||
/// <c>ursid</c>, <c>cervid</c>, <c>bovid</c>, <c>leporid</c>,
|
||
/// <c>imperium</c>, <c>none</c>. Phase-7 Imperium templates use
|
||
/// <c>imperium</c>; templates that would be at home in any dungeon use
|
||
/// <c>none</c>.
|
||
/// </summary>
|
||
[JsonPropertyName("built_by")]
|
||
public string BuiltBy { get; init; } = "none";
|
||
|
||
/// <summary>
|
||
/// Size class — <c>small</c>, <c>medium</c>, <c>large</c>. Used by
|
||
/// the layout matcher to pick room mixes appropriate to the dungeon's
|
||
/// size band (small dungeons prefer small rooms, etc.).
|
||
/// </summary>
|
||
[JsonPropertyName("size_class")]
|
||
public string SizeClass { get; init; } = "medium";
|
||
|
||
/// <summary>
|
||
/// Roles this template is eligible for: <c>entry</c>, <c>transit</c>,
|
||
/// <c>narrative</c>, <c>loot</c>, <c>boss</c>, <c>dead-end</c>. A
|
||
/// template can be eligible for multiple roles (a "pillar room" can
|
||
/// serve as transit OR as a loot stash).
|
||
/// </summary>
|
||
[JsonPropertyName("roles_eligible")]
|
||
public string[] RolesEligible { get; init; } = Array.Empty<string>();
|
||
|
||
/// <summary>Footprint width in tactical tiles. Includes perimeter walls. Must equal Grid[*].Length.</summary>
|
||
[JsonPropertyName("footprint_w_tiles")]
|
||
public int FootprintWTiles { get; init; } = 1;
|
||
|
||
/// <summary>Footprint height in tactical tiles. Includes perimeter walls. Must equal Grid.Length.</summary>
|
||
[JsonPropertyName("footprint_h_tiles")]
|
||
public int FootprintHTiles { get; init; } = 1;
|
||
|
||
/// <summary>
|
||
/// 2D ASCII art: one entry per row, one char per tactical tile.
|
||
/// Validated for perimeter wall completeness and slot-coordinate
|
||
/// matches at content-load time.
|
||
/// </summary>
|
||
[JsonPropertyName("grid")]
|
||
public string[] Grid { get; init; } = Array.Empty<string>();
|
||
|
||
/// <summary>Door positions in template-local coords (matches <c>D</c> chars in <see cref="Grid"/>).</summary>
|
||
[JsonPropertyName("doors")]
|
||
public RoomDoor[] Doors { get; init; } = Array.Empty<RoomDoor>();
|
||
|
||
/// <summary>Encounter slot positions (matches <c>@</c> chars).</summary>
|
||
[JsonPropertyName("encounter_slots")]
|
||
public RoomEncounterSlot[] EncounterSlots { get; init; } = Array.Empty<RoomEncounterSlot>();
|
||
|
||
/// <summary>Container slot positions (matches <c>C</c> chars).</summary>
|
||
[JsonPropertyName("container_slots")]
|
||
public RoomContainerSlot[] ContainerSlots { get; init; } = Array.Empty<RoomContainerSlot>();
|
||
|
||
/// <summary>Trap slot positions (matches <c>T</c> chars). Phase 7 ships only tripwire traps.</summary>
|
||
[JsonPropertyName("trap_slots")]
|
||
public RoomTrapSlot[] TrapSlots { get; init; } = Array.Empty<RoomTrapSlot>();
|
||
|
||
/// <summary>Decoration placements for non-slot decos (P pillar, B brazier, M mosaic).</summary>
|
||
[JsonPropertyName("decos")]
|
||
public RoomDecoPlacement[] Decos { get; init; } = Array.Empty<RoomDecoPlacement>();
|
||
|
||
/// <summary>
|
||
/// Environmental-story prose surfaced by Scent Literacy (Phase 6.5 M1)
|
||
/// in the InteractionScreen scent-overlay panel and by the dungeon-
|
||
/// clear coda. Null/empty for non-narrative templates.
|
||
/// </summary>
|
||
[JsonPropertyName("narrative_text")]
|
||
public string? NarrativeText { get; init; } = null;
|
||
|
||
/// <summary>Selection weight in layout assembly. Default 1.0.</summary>
|
||
[JsonPropertyName("weight")]
|
||
public float Weight { get; init; } = 1f;
|
||
}
|
||
|
||
public sealed record RoomDoor
|
||
{
|
||
[JsonPropertyName("x")]
|
||
public int X { get; init; }
|
||
|
||
[JsonPropertyName("y")]
|
||
public int Y { get; init; }
|
||
|
||
/// <summary>Compass facing: "N" / "E" / "S" / "W". Door always sits on a perimeter cell.</summary>
|
||
[JsonPropertyName("facing")]
|
||
public string Facing { get; init; } = "S";
|
||
|
||
/// <summary>
|
||
/// Optional lock difficulty for this door. Empty = unlocked. Allowed:
|
||
/// <c>trivial</c>, <c>easy</c>, <c>medium</c>, <c>hard</c> — mapped to
|
||
/// <c>LOCK_DC_*</c> constants in code.
|
||
/// </summary>
|
||
[JsonPropertyName("lock")]
|
||
public string Lock { get; init; } = "";
|
||
}
|
||
|
||
public sealed record RoomEncounterSlot
|
||
{
|
||
[JsonPropertyName("x")]
|
||
public int X { get; init; }
|
||
|
||
[JsonPropertyName("y")]
|
||
public int Y { get; init; }
|
||
|
||
/// <summary>
|
||
/// Spawn kind: <c>PoiGuard</c> / <c>WildAnimal</c> / <c>Brigand</c> /
|
||
/// <c>Boss</c>. Resolved against <c>npc_templates.json</c>'s
|
||
/// per-dungeon-type spawn-kind override map at populate time.
|
||
/// </summary>
|
||
[JsonPropertyName("kind")]
|
||
public string Kind { get; init; } = "PoiGuard";
|
||
|
||
/// <summary>Likelihood the slot fires when a layout calls for variability. 1.0 = always.</summary>
|
||
[JsonPropertyName("weight")]
|
||
public float Weight { get; init; } = 1f;
|
||
}
|
||
|
||
public sealed record RoomContainerSlot
|
||
{
|
||
[JsonPropertyName("x")]
|
||
public int X { get; init; }
|
||
|
||
[JsonPropertyName("y")]
|
||
public int Y { get; init; }
|
||
|
||
/// <summary>
|
||
/// Loot-table band: <c>t1</c> / <c>t2</c> / <c>t3</c>. The dungeon's
|
||
/// layout maps a band to a real loot-table id at populate time
|
||
/// (<see cref="DungeonLayoutDef.LootTablePerBand"/>).
|
||
/// </summary>
|
||
[JsonPropertyName("loot_table_band")]
|
||
public string LootTableBand { get; init; } = "t1";
|
||
|
||
/// <summary>True when the container is locked (key required, or STR/DEX check).</summary>
|
||
[JsonPropertyName("locked")]
|
||
public bool Locked { get; init; } = false;
|
||
|
||
/// <summary>Optional lock difficulty if <see cref="Locked"/>: trivial/easy/medium/hard.</summary>
|
||
[JsonPropertyName("lock")]
|
||
public string Lock { get; init; } = "";
|
||
}
|
||
|
||
public sealed record RoomTrapSlot
|
||
{
|
||
[JsonPropertyName("x")]
|
||
public int X { get; init; }
|
||
|
||
[JsonPropertyName("y")]
|
||
public int Y { get; init; }
|
||
|
||
/// <summary>Trap kind. Phase 7 ships only <c>tripwire</c>.</summary>
|
||
[JsonPropertyName("kind")]
|
||
public string Kind { get; init; } = "tripwire";
|
||
|
||
/// <summary>Disarm DC tier: <c>trivial</c>, <c>easy</c>, <c>medium</c>.</summary>
|
||
[JsonPropertyName("disarm_dc")]
|
||
public string DisarmDc { get; init; } = "easy";
|
||
}
|
||
|
||
public sealed record RoomDecoPlacement
|
||
{
|
||
[JsonPropertyName("x")]
|
||
public int X { get; init; }
|
||
|
||
[JsonPropertyName("y")]
|
||
public int Y { get; init; }
|
||
|
||
/// <summary>
|
||
/// Deco kind name. Allowed: <c>pillar</c>, <c>brazier</c>, <c>mosaic</c>,
|
||
/// <c>imperium_statue</c>. Trap / container / door / stairs decos are
|
||
/// declared via their respective slot collections, not here.
|
||
/// </summary>
|
||
[JsonPropertyName("deco")]
|
||
public string Deco { get; init; } = "";
|
||
}
|