using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; namespace Character_Builder; /// Reads/writes a composition as a human-readable .md file. public static class BuildMd { public static void Save(string path, BuildDefinition b) { var sb = new StringBuilder(); sb.AppendLine($"# Character Build — {b.Name}"); sb.AppendLine(); sb.AppendLine("> Saved by Character_Builder. Layers are composited: body + head(expression) + hair tint + accessories."); sb.AppendLine("> Coordinate convention (for the Dansori app): stage 520x680. Each layer image is drawn Height=680 (Stretch=Uniform),"); sb.AppendLine("> centered in the stage, scaled about its center by 'scale', then translated by (x, y) pixels. transform = x, y, scale."); sb.AppendLine("> The app can either reuse these transforms directly (same convention) or re-run alpha neck-alignment and treat these as deltas."); sb.AppendLine(); sb.AppendLine($"name: {b.Name}"); sb.AppendLine($"character: {b.Character}"); sb.AppendLine($"body: {b.BodyFile}"); sb.AppendLine($"body.transform: {F(b.BodyOffsetX)}, {F(b.BodyOffsetY)}, {F(b.BodyScale)}"); sb.AppendLine($"head: {b.HeadFile}"); sb.AppendLine($"expression: {b.ExpressionFile}"); sb.AppendLine($"hairmask: {b.HairmaskFile}"); sb.AppendLine($"hairColor: {b.HairColor}"); sb.AppendLine($"head.transform: {F(b.HeadOffsetX)}, {F(b.HeadOffsetY)}, {F(b.HeadScale)}"); sb.AppendLine(); sb.AppendLine("## accessories"); foreach (var a in b.Accessories) sb.AppendLine($"- {a.FileName} | {F(a.OffsetX)}, {F(a.OffsetY)}, {F(a.Scale)}"); File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false)); } public static BuildDefinition Load(string path) { var b = new BuildDefinition(); bool inAcc = false; foreach (var raw in File.ReadAllLines(path)) { var line = raw.Trim(); if (line.Length == 0) continue; if (line.StartsWith("## accessories", StringComparison.OrdinalIgnoreCase)) { inAcc = true; continue; } if (line.StartsWith("#")) continue; if (line.StartsWith(">")) continue; if (inAcc && line.StartsWith("-")) { var body = line.TrimStart('-', ' '); var parts = body.Split('|'); var a = new AccessoryLayer { FileName = parts[0].Trim() }; if (parts.Length > 1) ApplyTransform(parts[1], (x, y, s) => { a.OffsetX = x; a.OffsetY = y; a.Scale = s; }); if (!string.IsNullOrEmpty(a.FileName)) b.Accessories.Add(a); continue; } int c = line.IndexOf(':'); if (c < 0) continue; var key = line.Substring(0, c).Trim().ToLowerInvariant(); var val = line.Substring(c + 1).Trim(); switch (key) { case "name": b.Name = val; break; case "character": b.Character = val; break; case "body": b.BodyFile = Empty(val); break; case "body.transform": ApplyTransform(val, (x, y, s) => { b.BodyOffsetX = x; b.BodyOffsetY = y; b.BodyScale = s; }); break; case "head": b.HeadFile = Empty(val); break; case "expression": b.ExpressionFile = Empty(val); break; case "hairmask": b.HairmaskFile = Empty(val); break; case "haircolor": b.HairColor = Empty(val); break; case "head.transform": ApplyTransform(val, (x, y, s) => { b.HeadOffsetX = x; b.HeadOffsetY = y; b.HeadScale = s; }); break; } } return b; } private static string? Empty(string v) => string.IsNullOrWhiteSpace(v) ? null : v; private static void ApplyTransform(string s, Action set) { var t = s.Split(','); double x = t.Length > 0 ? D(t[0]) : 0; double y = t.Length > 1 ? D(t[1]) : 0; double sc = t.Length > 2 ? D(t[2]) : 1; set(x, y, sc); } private static double D(string s) => double.TryParse(s.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out var v) ? v : 0; private static string F(double v) => v.ToString("0.###", CultureInfo.InvariantCulture); }