Monster WPF Data File Builder
Opened this issue · 5 comments
Made from a project note. it a very dirty solution, but it is a WIP
`
namespace MonsterCreator
{
using System.Collections.Generic;
using System.IO;
using System.Xml;
public static class BuilderExtensions
{
public static MonsterBuilder CreateMonster(this MonsterBuilder builder, string monsterName, int MaximumHitPoints, string WeaponId, int RewardXP, int Gold,
string ImageName, int Dexterity, IEnumerable<(string ID, int Percentage)> lootBag)
{
builder.Monster = builder.XmlFile.CreateElement("Monster");
builder.Monster.SetAttribute("ID", (builder.Monsters.ChildNodes.Count + 1).ToString());
builder.Monster.SetAttribute("Name", monsterName);
builder.Monster.SetAttribute(nameof(MaximumHitPoints), MaximumHitPoints.ToString());
builder.Monster.SetAttribute(nameof(WeaponId), WeaponId);
builder.Monster.SetAttribute(nameof(RewardXP), RewardXP.ToString());
builder.Monster.SetAttribute(nameof(Gold), Gold.ToString());
builder.Monster.SetAttribute(nameof(ImageName), ImageName);
var dex = builder.XmlFile.CreateElement(nameof(Dexterity));
dex.InnerText = Dexterity.ToString();
builder.Monster.AppendChild(dex);
var lootItems = builder.XmlFile.CreateElement("LootItems");
foreach ((string ID, int Percentage) in lootBag)
{
var item = builder.XmlFile.CreateElement("LootItem");
item.SetAttribute(nameof(ID), ID);
item.SetAttribute(nameof(Percentage), Percentage.ToString());
lootItems.AppendChild(item);
}
builder.Monster.AppendChild(lootItems);
builder.Monsters.AppendChild(builder.Monster);
builder.Monster = null;
builder.XmlFile.Save(".\\GameData\\Monsters.xml");
return builder;
}
}
public class MonsterBuilder
{
private const string MONSTER_DATA_FILENAME = ".\\GameData\\Monsters.xml";
private const string BaseNode = "/Monsters";
internal readonly XmlDocument XmlFile = new XmlDocument();
internal XmlElement Monsters { get; }
internal XmlElement Monster { get; set; }
public MonsterBuilder()
{
if (File.Exists(MONSTER_DATA_FILENAME))
{
XmlFile.Load(MONSTER_DATA_FILENAME);
if (!string.IsNullOrEmpty(BaseNode))
{
if (XmlFile.SelectSingleNode(BaseNode).NodeType == XmlNodeType.Element)
{
Monsters = (XmlElement)XmlFile.SelectSingleNode(BaseNode);
}
}
}
}
}
}
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Engine.Models;
namespace MonsterCreator
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
//Range of Hit Points
public ObservableCollection<int> HitPoints = new ObservableCollection<int>(Enumerable.Range(1, 25));
// Complete collection of Monsters so far.
public ObservableCollection<Monster> Monsters = new ObservableCollection<Monster>();
public ObservableCollection<(string ItemName, int ID, int Percentage)> Loots = new ObservableCollection<(string, int, int)>
{
("Rusty Sword", 1002, 25),
("Snake fang", 1501, 35),
("Rat Claw", 1502, 35),
("Spider fang", 1503, 35),
("Granola bar", 2001, 50),
("Oats", 3001, 50),
("Honey", 3002, 50),
("Raisins", 3003, 50)
};
public MainWindow()
{
InitializeComponent();
MonsterHP.ItemsSource = HitPoints;
MonsterXP.ItemsSource = HitPoints;
MonsterXP.SelectedIndex = 0;
MonsterHP.SelectedIndex = 0;
Weapons.ItemsSource = Loots.Where(x => x.ID > 1500 && x.ID < 1505).Select(x => x.ItemName);
Weapons.SelectedIndex = 0;
Gold1.ItemsSource = HitPoints;
Gold1.SelectedIndex = 0;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MonsterBuilder builder = new MonsterBuilder();
textBox1.Text = (builder.Monsters.ChildNodes.Count + 1).ToString();
builder.CreateMonster(
MonsterName.Content.ToString(),
MonsterHP.SelectedIndex + 1,
Loots.FirstOrDefault(x => x.ItemName == Weapons.Text).ID.ToString(),
MonsterXP.SelectedIndex + 1,
int.Parse(Gold1.Text),
"",
10,
Loots.Select(x => (x.ID.ToString(), x.Percentage))
);
}
}
}
`
XAML
`
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MonsterCreator"
xmlns:System="clr-namespace:System;assembly=System.Runtime" x:Class="MonsterCreator.MainWindow"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid Margin="0,0,-3,0" HorizontalAlignment="Right" Width="800">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="16*"/>
<RowDefinition Height="0*"/>
<RowDefinition Height="5*"/>
<RowDefinition Height="10*"/>
</Grid.RowDefinitions>
<Label x:Name="Monster_Creator" Content="SOSCSRPG Monster Creator 0.1a" HorizontalAlignment="Left" Height="24" Margin="308,10,0,0" VerticalAlignment="Top" Width="184" Grid.ColumnSpan="2"/>
<Grid HorizontalAlignment="Center" Height="384" Margin="0,34,0,0" VerticalAlignment="Top" Width="380" Grid.RowSpan="4">
<TextBox x:Name="textBox" HorizontalAlignment="Left" Margin="7,29,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="106" Height="25"/>
<ComboBox x:Name="MonsterHP" HorizontalAlignment="Left" Height="25" Margin="7,87,0,0" VerticalAlignment="Top" Width="106" />
<Label x:Name="MonsterName" Content="Name" HorizontalAlignment="Left" Height="24" VerticalAlignment="Top" Width="86" Margin="8,0,0,0"/>
<Label x:Name="MonsterHitPoints" Content="Hit Points" HorizontalAlignment="Left" Height="24" Margin="8,60,0,0" VerticalAlignment="Top" Width="86"/>
<Label x:Name="MonsterId" Content="ID" HorizontalAlignment="Left" Height="23" Margin="127,1,0,0" VerticalAlignment="Top" Width="85"/>
<TextBox x:Name="textBox1" HorizontalAlignment="Left" Height="25" Margin="127,29,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="105" IsEnabled="False"/>
<Label x:Name="Expierence" Content="Expierence" HorizontalAlignment="Left" Height="25" Margin="127,59,0,0" VerticalAlignment="Top" Width="85"/>
<ComboBox x:Name="MonsterXP" HorizontalAlignment="Left" Height="25" Margin="127,86,0,0" VerticalAlignment="Top" Width="106" />
<Label x:Name="Weapon" Content="Weapon" HorizontalAlignment="Left" Margin="10,115,0,0" VerticalAlignment="Top" Width="84"/>
<ComboBox x:Name="Weapons" HorizontalAlignment="Left" Margin="7,141,0,0" VerticalAlignment="Top" Width="106"/>
<Label x:Name="Gold" Content="Gold" HorizontalAlignment="Left" Margin="127,114,0,0" VerticalAlignment="Top" Width="75"/>
<ComboBox x:Name="Gold1" HorizontalAlignment="Left" Margin="127,141,0,0" VerticalAlignment="Top" Width="105"/>
<Button x:Name="button" Content="Create Monster" HorizontalAlignment="Left" Height="30" Margin="7,240,0,0" VerticalAlignment="Top" Width="106" Click="Button_Click"/>
</Grid>
</Grid>
</Window>
`
Example appended to file
<Monster ID="4" Name="Name" MaximumHitPoints="8" WeaponId="1502" RewardXP="8" Gold="4" ImageName=""> <Dexterity>10</Dexterity> <LootItems> <LootItem ID="1002" Percentage="25" /> <LootItem ID="1501" Percentage="35" /> <LootItem ID="1502" Percentage="35" /> <LootItem ID="1503" Percentage="35" /> <LootItem ID="2001" Percentage="50" /> <LootItem ID="3001" Percentage="50" /> <LootItem ID="3002" Percentage="50" /> <LootItem ID="3003" Percentage="50" /> </LootItems> </Monster>
This is very rough draft, so improvements and eventual push will come
Restructured MonsterCreator to static class, added comments.
`
using System.Collections.Generic;
using System.IO;
using System.Xml;
namespace MonsterCreator
{
/// <summary>
/// Monster Creator is a static class responsible for load the Monsters XML File and locating the Monsters Element.
/// it also houses the XmlElement used to create new monsters from.
/// </summary>
public static class MonsterCreator
{
private const string MONSTER_DATA_FILENAME = ".\\GameData\\Monsters.xml";
private const string RootNodeName = "/Monsters";
private const string NodeName = "Monster";
internal static readonly XmlDocument XmlFile = new XmlDocument();
internal static XmlElement Monsters { get; }
internal static XmlElement Monster { get; set; }
static MonsterCreator()
{
if (File.Exists(MONSTER_DATA_FILENAME))
{
XmlFile.Load(MONSTER_DATA_FILENAME);
if (!string.IsNullOrEmpty(RootNodeName))
{
if (XmlFile.SelectSingleNode(RootNodeName).NodeType == XmlNodeType.Element)
{
Monsters = (XmlElement)XmlFile.SelectSingleNode(RootNodeName);
}
}
}
}
/// <summary>
/// Creates a monster and saves it to Monsters.xml in the local GameData folder
/// </summary>
/// <param name="Name"></param>
/// <param name="MaximumHitPoints"></param>
/// <param name="WeaponId"></param>
/// <param name="RewardXP"></param>
/// <param name="Gold"></param>
/// <param name="ImageName"></param>
/// <param name="Dexterity"></param>
/// <param name="lootBag"></param>
public static void CreateMonster(string Name, int MaximumHitPoints, string WeaponId, int RewardXP, int Gold,
string ImageName, int Dexterity, IEnumerable<(string ID, int Percentage)> lootBag)
{
Monster = XmlFile.CreateElement(NodeName);
// Monster Attributes
Monster.SetAttribute("ID", (Monsters.ChildNodes.Count + 1).ToString());
Monster.SetAttribute(nameof(Name), Name);
Monster.SetAttribute(nameof(MaximumHitPoints), MaximumHitPoints.ToString());
Monster.SetAttribute(nameof(WeaponId), WeaponId);
Monster.SetAttribute(nameof(RewardXP), RewardXP.ToString());
Monster.SetAttribute(nameof(Gold), Gold.ToString());
Monster.SetAttribute(nameof(ImageName), ImageName);
// Monster Dexterity Element
var dex = XmlFile.CreateElement(nameof(Dexterity));
dex.InnerText = Dexterity.ToString();
Monster.AppendChild(dex);
// Monster Loot
var lootItems = XmlFile.CreateElement("LootItems");
foreach ((string ID, int Percentage) in lootBag)
{
var item = XmlFile.CreateElement("LootItem");
item.SetAttribute(nameof(ID), ID);
item.SetAttribute(nameof(Percentage), Percentage.ToString());
lootItems.AppendChild(item);
}
Monster.AppendChild(lootItems);
// Append Monster to Monsters Element
Monsters.AppendChild(Monster);
// reset Monster;
Monster = null;
// Save modifications.
XmlFile.Save(MONSTER_DATA_FILENAME);
}
}
}
`
Thanks! After I convert the project to .NET 5 (what I'm currently working on), I'll add another WPF project for this data file editor. This is a feature I've wanted to see for a long time.
Hello, Scott,
I am really happy that this is something you wanted and that I am able to give back something from what i learned. :D
So, I put in more effort to reduce the specialization of this class (Just monsters) and take a broader swipe at creating the various XML files, and I have come up with XmlCreator and CreatorConfig combination that allows us to produce most such files, in a fluent way, with the structure of the data not influencing the layout of code, but is a product the code the user calls. I definitely prefer this method, as it will allow me to build the other xml data files.
`
using System.Collections.Generic;
using System.IO;
using System.Xml;
namespace XmlCreation
{
public class CreatorConfig
{
public XmlDocument XmlFile { get; }
public string PathToFile { get; }
public string ParentNodeName { get; internal set; }
public string ChildNodeName { get; }
public XmlElement XmlParentNode { get; }
public XmlElement XmlChildNode { get; internal set; }
public CreatorConfig(string pathToFile, string rootNodeName, string childNodeName)
{
PathToFile = File.Exists(pathToFile) ? pathToFile : string.Empty;
if (!string.IsNullOrEmpty(PathToFile))
{
XmlFile = new XmlDocument();
XmlFile.Load(pathToFile);
XmlParentNode = (XmlElement)XmlFile.SelectSingleNode(rootNodeName);
if (XmlParentNode != null)
{
ParentNodeName = rootNodeName;
ChildNodeName = childNodeName;
XmlChildNode = XmlFile.CreateElement(childNodeName);
}
}
}
internal void Save()
{
XmlFile.Save(PathToFile);
}
}
public class XmlCreator
{
internal CreatorConfig Config { get; }
public XmlCreator(CreatorConfig config)
{
Config = config;
}
public XmlCreator AddAttribute(string Name, string Value)
{
Config.XmlChildNode.SetAttribute(Name, Value);
return this;
}
public XmlCreator AddElement(string ElementName, string InnerText = default, List<(string, string)> attributes = default)
{
var element = Config.XmlFile.CreateElement(ElementName);
if (InnerText != default)
{
element.InnerText = InnerText;
}
if(attributes != default)
{
foreach ((string ID, string Value) in attributes)
{
element.SetAttribute(ID, Value);
}
}
Config.XmlChildNode.AppendChild(element);
return this;
}
public XmlCreator AddElementTo(string ParentName, string ChildElementName, IEnumerable<(string key, string value)> attributes)
{
var Parent = Config.XmlChildNode.SelectSingleNode(ParentName);
XmlElement Child = Config.XmlFile.CreateElement(ChildElementName);
foreach ((string key, string value) in attributes)
{
Child.SetAttribute(key, value);
}
Parent.AppendChild(Child);
return this;
}
public XmlCreator ResetChild()
{
Config.XmlChildNode = Config.XmlFile.CreateElement(Config.ChildNodeName);
return this;
}
public XmlCreator Save()
{
Config.XmlParentNode.AppendChild(Config.XmlChildNode);
Config.Save();
return this;
}
}
}
`
Initialization
public XmlCreator creator = new XmlCreator(new CreatorConfig(".\\GameData\\Monsters.xml", "/Monsters", "Monster"));
Usage
`
textBox1.Text = (creator.Config.XmlParentNode.ChildNodes.Count + 1).ToString();
creator
.AddAttribute("ID", textBox1.Text)
.AddAttribute("Name", MonsterName1.Text)
.AddAttribute("MaximumHitPoints", (MonsterHP.SelectedIndex + 1).ToString())
.AddAttribute("WeaponID", Loots.FirstOrDefault(x => x.ItemName == Weapons.Text).ID.ToString())
.AddAttribute("RewardXP", (MonsterXP.SelectedIndex + 1).ToString())
.AddAttribute("Gold", Gold1.Text)
.AddAttribute("ImageName", "")
.AddElement("Dexterity", "10")
.AddElement("LootItems");
foreach (var (ItemName, ID, Percentage) in Loots)
{
creator.AddElementTo("LootItems", "LootItem", new (string, string)[] { ("ID", ID.ToString()), ("Percentage", Percentage.ToString()) });
}
creator.Save();
creator.ResetChild();
`
I am going to try and make this UI more closely match the UI of the Trader window, this project will also be .net 5.0, in hopes that it makes for a smooth transition upon integration.
I will try and get this together as a proper Issue this week.
Regards.
Have you done much work with JSON? I'm wondering if we should convert the XML files to JSON, since JSON is more common nowadays.
I've also been thinking about a different way of handling models. The model classes would only be properties - no functions (or very few functions). All the game logic would be handled by service classes. Those service classes might be good places to do the serialization/de-serialization, and could be used by the main game and your editor app.
Now that my new computer is built, and all my software is installed, I'm going to start on the conversion to .NET 5.
Yes, I have done a little work with JSON. converting to JSON should also reduce the load time and size of configuration files, so I am all for that. Adding a converter from XML to JSON could also reduce any manual work you might have to do.
http://www.binaryintellect.net/articles/d56c7798-703d-45cf-be74-a8b0cec94a3c.aspx
Ok, I think I understand that as you want a more data-oriented approach to handling the game logic? Moving to models as pure data and services as a way to interact with the data?