Character template szf object

This commit is contained in:
Chris Bell 2025-07-01 00:09:44 -05:00
parent 66dd01020c
commit d12474c01e
5 changed files with 315 additions and 84 deletions

View File

@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SessionZero.Data;
public class CharacterTemplate : SzfObject
{
public string GameSystem { get; set; } = string.Empty;
public List<RequiredDatasetReference> RequiredDatasets { get; set; } = new();
public List<TemplateSection> Sections { get; set; } = new(); // Top-level sections
public override SzfObjectType ObjectType => SzfObjectType.CharacterTemplate;
public override void PopulateFromMetadata(Dictionary<string, SzfField> metadata)
{
base.PopulateFromMetadata(metadata);
if (metadata.TryGetValue("GameSystem", out var gameSystemField))
GameSystem = gameSystemField.Value?.ToString() ?? string.Empty;
}
public override void ParseSections(List<SzfSection> sections)
{
// Parse "Required Datasets" section
var requiredDatasetsSection =
sections.FirstOrDefault(s => s.Name.Equals("Required Datasets", StringComparison.OrdinalIgnoreCase));
if (requiredDatasetsSection != null)
{
foreach (var field in requiredDatasetsSection.Fields)
{
var datasetReference = DatasetReference.Parse(field.Value?.ToString() ?? string.Empty);
if (datasetReference != null)
{
RequiredDatasets.Add(new RequiredDatasetReference
{
Alias = field.Name,
Reference = datasetReference
});
}
}
}
// Parse and build the hierarchical template sections
// Use a map to quickly find existing sections by their full path for nesting
var sectionPathMap = new Dictionary<string, TemplateSection>(StringComparer.OrdinalIgnoreCase);
foreach (var szfSection in sections)
{
// Only process sections that start with "Section:"
if (!szfSection.Name.StartsWith("Section:", StringComparison.OrdinalIgnoreCase)) continue;
// Normalize the section name (e.g., "Section:Ability Scores.Modifiers" to "Ability Scores.Modifiers")
var rawSectionPath = szfSection.Name.Substring("Section:".Length).Trim();
var nameParts = rawSectionPath.Split('.');
TemplateSection? currentParentSection = null;
List<TemplateSection> currentSectionList = Sections; // Start from the top-level Sections list
string accumulatedPath = "Section"; // Used for dictionary key to ensure uniqueness and hierarchy
for (int i = 0; i < nameParts.Length; i++)
{
var sectionNamePart = nameParts[i];
accumulatedPath += "." + sectionNamePart; // Build the full path for the map key
TemplateSection? targetSection = null;
// Check if this specific section (at this level of hierarchy) already exists
if (sectionPathMap.TryGetValue(accumulatedPath, out var existingSection))
{
targetSection = existingSection;
}
else
{
// Create a new section if it doesn't exist
var newSection = new TemplateSection { Name = sectionNamePart };
sectionPathMap[accumulatedPath] = newSection;
if (currentParentSection == null)
{
// This is a top-level section
currentSectionList.Add(newSection);
}
else
{
// This is a subsection of the current parent
currentParentSection.Subsections.Add(newSection);
}
targetSection = newSection;
}
currentParentSection = targetSection; // Move down the hierarchy
currentSectionList =
targetSection.Subsections; // Next iteration will look in subsections of this section
}
// Once we've traversed/created the full path, currentParentSection points to the innermost section.
// Now populate its fields from the SzfSection.
if (currentParentSection != null)
{
foreach (var field in szfSection.Fields)
{
currentParentSection.Fields.Add(ConvertToTemplateField(field));
}
}
}
}
private TemplateField ConvertToTemplateField(SzfField szfField)
{
return new TemplateField
{
Name = szfField.Name,
Type = ParseDatasetFieldType(szfField.Type), // Reusing DatasetFieldType for consistency
Value = szfField.Value,
};
}
// Helper method to parse DatasetFieldType, can be shared or defined here.
// Copying from Dataset.cs for now, consider a common utility.
private DatasetFieldType ParseDatasetFieldType(string typeString)
{
return typeString.ToLower() switch
{
"text" => DatasetFieldType.Text,
"text-field" => DatasetFieldType.TextField,
"number" => DatasetFieldType.Number,
"bool" => DatasetFieldType.Boolean,
"group" => DatasetFieldType.Group,
"calculated" => DatasetFieldType.Calculated,
"system" => DatasetFieldType.System,
"dataset-reference" => DatasetFieldType.DatasetReference,
"dataset-type" => DatasetFieldType.DatasetType,
"dataset-reference-multiple" => DatasetFieldType.DatasetReferenceMultiple,
"dataset-type-multiple" => DatasetFieldType.DatasetTypeMultiple,
_ => DatasetFieldType.Text
};
}
public override SzfValidationResult Validate()
{
var result = base.Validate();
// if (string.IsNullOrWhiteSpace(GameSystem))
// result.AddError("GameSystem is required for CharacterTemplate");
// Further validation for required datasets and template sections can be added here
// e.g., checking for duplicate section names or missing required fields.
return result;
}
}
public class RequiredDatasetReference
{
public string Alias { get; set; } = string.Empty;
public DatasetReference? Reference { get; set; }
}
public class TemplateSection
{
public string Name { get; set; } = string.Empty;
public List<TemplateField> Fields { get; set; } = new();
public List<TemplateSection> Subsections { get; set; } = new();
}
public class TemplateField
{
public string Name { get; set; } = string.Empty;
public DatasetFieldType Type { get; set; }
public object? Value { get; set; }
}

View File

@ -222,10 +222,10 @@ public enum DatasetFieldType
public class DatasetReference
{
public string Name { get; set; } = string.Empty;
public Guid DatasetId { get; set; }
public Guid Guid { get; set; }
public string Version { get; set; } = string.Empty;
public override string ToString() => $"{Name}|{DatasetId}|{Version}";
public override string ToString() => $"{Name}|{Guid}|{Version}";
public static DatasetReference? Parse(string reference)
{
@ -236,7 +236,7 @@ public class DatasetReference
return new DatasetReference
{
Name = parts[0],
DatasetId = id,
Guid = id,
Version = parts[2]
};
}

View File

@ -73,8 +73,7 @@ public class SzfParser
return objectType switch
{
SzfObjectType.Dataset => new Dataset(),
// Add other types as they're implemented
// SzfObjectType.CharacterTemplate => new CharacterTemplate(),
SzfObjectType.CharacterTemplate => new CharacterTemplate(), // UNCOMMENTED THIS LINE
// SzfObjectType.Character => new Character(),
// SzfObjectType.Session => new Session(),
_ => throw new SzfParseException($"Unsupported object type: {objectType}")
@ -254,7 +253,7 @@ public class SzfParser
var expectedType = typeof(T).Name switch
{
nameof(Dataset) => SzfObjectType.Dataset,
// Add other types as they're implemented
nameof(CharacterTemplate) => SzfObjectType.CharacterTemplate,
_ => throw new SzfParseException($"Unsupported SzfObject type: {typeof(T).Name}")
};
@ -289,7 +288,7 @@ public class SzfParser
/// <summary>
/// Internal class to hold parsed SZF structure
/// </summary>
internal class SzfParseResult
public class SzfParseResult // Changed from internal to public for broader accessibility if needed
{
public SzfObjectType ObjectType { get; set; }
public string SchemaVersion { get; set; } = "1.0.0";
@ -300,7 +299,7 @@ internal class SzfParseResult
/// <summary>
/// Internal class representing a parsed section
/// </summary>
public class SzfSection
public class SzfSection // Changed from internal to public for broader accessibility if needed
{
public string Name { get; set; } = string.Empty;
public List<SzfField> Fields { get; set; } = new();

View File

@ -4,20 +4,20 @@
@inject SzfParser SzfParser
@inject ILogger<SzfParser> Logger
<h1 class="page-title">SZF Parser Test Page</h1>
<h1>SZF Parser Test Page</h1>
<div class="row">
<div class="col-6">
<div class="form-group">
<textarea id="szf-input" class="form-control" @bind="szfCode" rows="20"></textarea>
<div>
<div>
<div>
<textarea id="szf-input" @bind="szfCode" rows="20"></textarea>
</div>
<button class="btn btn-primary mt-2" @onclick="ParseSzf">Parse SZF</button>
<button @onclick="ParseSzf">Parse SZF</button>
</div>
<div class="col-6">
<div>
@if (parsedObject != null)
{
<h4>Parsed Object Metadata</h4>
<table class="table">
<h2>Parsed Object Data</h2>
<table>
<tbody>
<tr><th>Object Type</th><td>@parsedObject.ObjectType</td></tr>
<tr><th>Schema Version</th><td>@parsedObject.SchemaVersion</td></tr>
@ -31,8 +31,8 @@
@if (parsedObject.ExtendedMetadata.Any())
{
<h5>Extended Metadata</h5>
<table class="table">
<h3>Extended Metadata</h3>
<table>
<thead>
<tr>
<th>Key</th>
@ -51,24 +51,24 @@
</table>
}
@if (parsedObject is Dataset dataset && dataset.Entries.Any())
@* Display Dataset specific data *@
@if (parsedObject is Dataset dataset)
{
<h4 class="mt-4">Entries</h4>
@foreach (var entry in dataset.Entries)
@if (dataset.Entries.Any())
{
<div class="card mb-3">
<div class="card-header">
<h5>@entry.Name</h5>
</div>
<div class="card-body">
<h3>Dataset Entries</h3>
@foreach (var entry in dataset.Entries)
{
<div>
<h4>Entry: @entry.Name</h4>
@if (entry.Fields.Any())
{
<table class="table table-bordered table-sm">
<table>
<tbody>
@foreach (var field in entry.Fields)
{
<tr>
<td style="width: 30%;"><strong>@field.Name</strong></td>
<td><strong>@field.Name</strong></td>
<td>@field.Value</td>
</tr>
}
@ -77,16 +77,55 @@
}
@foreach (var section in entry.Sections)
{
@RenderSection(section)
@RenderDatasetSection(section)
}
</div>
</div>
}
}
}
@* Display CharacterTemplate specific data *@
@if (parsedObject is CharacterTemplate characterTemplate)
{
@if (characterTemplate.RequiredDatasets.Any())
{
<h3>Required Datasets</h3>
<table>
<thead>
<tr>
<th>Alias</th>
<th>Name</th>
<th>GUID</th>
<th>Version</th>
</tr>
</thead>
<tbody>
@foreach (var reqDataset in characterTemplate.RequiredDatasets)
{
<tr>
<td>@reqDataset.Alias</td>
<td>@reqDataset.Reference?.Name</td>
<td>@reqDataset.Reference?.Guid</td> @* Changed from DatasetId to Guid *@
<td>@reqDataset.Reference?.Version</td>
</tr>
}
</tbody>
</table>
}
@if (characterTemplate.Sections.Any())
{
<h3>Template Sections</h3>
@foreach (var section in characterTemplate.Sections)
{
@RenderTemplateSection(section)
}
}
}
}
@if (errorMessages.Any())
{
<div class="alert alert-danger mt-3">
<div>
<h4>Errors</h4>
<ul>
@foreach (var error in errorMessages)
@ -99,11 +138,9 @@
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<h4>Logs</h4>
<pre class="bg-light p-3">@string.Join("\n", logMessages)</pre>
</div>
<div>
<h2>Logs</h2>
<pre>@string.Join("\n", logMessages)</pre>
</div>
@code {
@ -155,29 +192,49 @@
logMessages.Add($"[ERROR] {message}{(ex is not null ? $": {ex.Message}" : "")}");
}
private RenderFragment RenderSection(DatasetSection dataSection) =>@<div class="card mt-2" style="margin-left: 20px;">
<div class="card-header py-1">
<strong>@dataSection.Name</strong>
</div>
<div class="card-body py-2">
@if (dataSection.Fields.Any())
{
<table class="table table-bordered table-sm mb-0">
<tbody>
@foreach (var field in dataSection.Fields)
{
<tr>
<td style="width: 30%;"><strong>@field.Name</strong></td>
<td>@field.Value</td>
</tr>
}
</tbody>
</table>
}
@foreach (var subSection in dataSection.Subsections)
{
@RenderSection(subSection)
}
</div>
// Helper for rendering Dataset sections (retained from original, adapted for basic HTML)
private RenderFragment RenderDatasetSection(DatasetSection dataSection) =>@<div>
<strong>@dataSection.Name</strong>
@if (dataSection.Fields.Any())
{
<table>
<tbody>
@foreach (var field in dataSection.Fields)
{
<tr>
<td><strong>@field.Name</strong></td>
<td>@field.Value</td>
</tr>
}
</tbody>
</table>
}
@foreach (var subSection in dataSection.Subsections)
{
@RenderDatasetSection(subSection)
}
</div>;
// New helper for rendering CharacterTemplate sections
private RenderFragment RenderTemplateSection(TemplateSection templateSection) =>@<div>
<strong>@templateSection.Name</strong>
@if (templateSection.Fields.Any())
{
<table>
<tbody>
@foreach (var field in templateSection.Fields)
{
<tr>
<td><strong>@field.Name</strong></td>
<td>@field.Value</td>
</tr>
}
</tbody>
</table>
}
@foreach (var subSection in templateSection.Subsections)
{
@RenderTemplateSection(subSection)
}
</div>;
}

View File

@ -10,23 +10,23 @@ Author (text) = Template Master
GameSystem (text) = Fantasy RPG Universal
[Required Datasets]
PlayerCharacterClasses (text) = Player Character Classes|c3d4e5f6-g7h8-9012-cdef-123456789012|1.5.3
PlayableRaces (text) = Playable Races|d4e5f6g7-h8i9-0123-def0-234567890123|1.3.0
CoreSkillsList (text) = Core Skills List|e5f6g7h8-i9j0-1234-ef01-345678901234|1.0.2
PlayerCharacterClasses (text) = Player Character Classes|c3d4e5f6-b5a4-40c7-97d8-30bca3bda294|1.5.3
PlayableRaces (text) = Playable Races|d4e5f6a7-b8c9-40d0-e1f2-345678901234|1.3.0
CoreSkillsList (text) = Core Skills List|e5f6a7b8-c9d0-40e1-f2g3-456789012345|1.0.2
[Section: Core Identity]
CharacterName (text) =
PlayerName (text) =
CharacterName (text) =
PlayerName (text) =
Race (dataset-reference) = PlayableRaces
Class (dataset-reference) = PlayerCharacterClasses
Level (number) = 1
Experience (number) = 0
Alignment (text) = Neutral
Background (text) =
Background (text) =
Age (number) = 25
Height (text) =
Weight (text) =
Gender (text) =
Height (text) =
Weight (text) =
Gender (text) =
[Section: Ability Scores]
Strength (number) = 10
@ -177,32 +177,32 @@ Feats (group) = true
DarkvisionRange (number) = 0
Size (text) = Medium
Languages (text-field) = Common
SpecialAbilities (text-field) =
SpecialAbilities (text-field) =
[Section.Features and Traits.Class Features]
HitDie (text) = d8
ProficiencyBonus (number) = 2
ArmorProficiencies (text-field) =
WeaponProficiencies (text-field) =
ToolProficiencies (text-field) =
SavingThrowProficiencies (text) =
ArmorProficiencies (text-field) =
WeaponProficiencies (text-field) =
ToolProficiencies (text-field) =
SavingThrowProficiencies (text) =
[Section: Personal Journal]
CharacterDescription (text-field) =
Personality (text-field) =
Ideals (text-field) =
Bonds (text-field) =
Flaws (text-field) =
Backstory (text-field) =
Goals (text-field) =
Notes (text-field) =
CharacterDescription (text-field) =
Personality (text-field) =
Ideals (text-field) =
Bonds (text-field) =
Flaws (text-field) =
Backstory (text-field) =
Goals (text-field) =
Notes (text-field) =
[Section: Combat Tracker]
CurrentHitPoints (number) = 10
TemporaryHitPoints (number) = 0
HitDiceRemaining (text) = 1d8
DeathSaves (group) = true
Conditions (text-field) =
Conditions (text-field) =
ActionEconomy (group) = true
[Section.Combat Tracker.Death Saves]