Last active
July 26, 2022 00:17
-
-
Save ScottLilly/f3ffb55c2fd8ea87204d839cee39be45 to your computer and use it in GitHub Desktop.
Lesson 18.4: Adding Player Attributes to Living Entities
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.ObjectModel; | |
using System.Linq; | |
using Engine.Factories; | |
using Engine.Models; | |
using Engine.Services; | |
namespace Engine.ViewModels | |
{ | |
public class CharacterCreationViewModel : BaseNotificationClass | |
{ | |
private Race _selectedRace; | |
public GameDetails GameDetails { get; } | |
public Race SelectedRace | |
{ | |
get => _selectedRace; | |
set | |
{ | |
_selectedRace = value; | |
OnPropertyChanged(); | |
} | |
} | |
public string Name { get; set; } | |
public ObservableCollection<PlayerAttribute> PlayerAttributes { get; set; } = | |
new ObservableCollection<PlayerAttribute>(); | |
public bool HasRaces => | |
GameDetails.Races.Any(); | |
public bool HasRaceAttributeModifiers => | |
HasRaces && GameDetails.Races.Any(r => r.PlayerAttributeModifiers.Any()); | |
public CharacterCreationViewModel() | |
{ | |
GameDetails = GameDetailsService.ReadGameDetails(); | |
if(HasRaces) | |
{ | |
SelectedRace = GameDetails.Races.First(); | |
} | |
RollNewCharacter(); | |
} | |
public void RollNewCharacter() | |
{ | |
PlayerAttributes.Clear(); | |
foreach(PlayerAttribute playerAttribute in GameDetails.PlayerAttributes) | |
{ | |
playerAttribute.ReRoll(); | |
PlayerAttributes.Add(playerAttribute); | |
} | |
ApplyAttributeModifiers(); | |
} | |
public void ApplyAttributeModifiers() | |
{ | |
foreach(PlayerAttribute playerAttribute in PlayerAttributes) | |
{ | |
var attributeRaceModifier = | |
SelectedRace.PlayerAttributeModifiers | |
.FirstOrDefault(pam => pam.AttributeKey.Equals(playerAttribute.Key)); | |
playerAttribute.ModifiedValue = | |
playerAttribute.BaseValue + (attributeRaceModifier?.Modifier ?? 0); | |
} | |
} | |
public Player GetPlayer() | |
{ | |
Player player = new Player(Name, 0, 10, 10, PlayerAttributes, 10); | |
// Give player default inventory items, weapons, recipes, etc. | |
player.AddItemToInventory(ItemFactory.CreateGameItem(1001)); | |
player.AddItemToInventory(ItemFactory.CreateGameItem(2001)); | |
player.LearnRecipe(RecipeFactory.RecipeByID(1)); | |
player.AddItemToInventory(ItemFactory.CreateGameItem(3001)); | |
player.AddItemToInventory(ItemFactory.CreateGameItem(3002)); | |
player.AddItemToInventory(ItemFactory.CreateGameItem(3003)); | |
return player; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Engine.Models; | |
using Engine.Shared; | |
namespace Engine.Services | |
{ | |
public static class CombatService | |
{ | |
public enum Combatant | |
{ | |
Player, | |
Opponent | |
} | |
public static Combatant FirstAttacker(Player player, Monster opponent) | |
{ | |
// Formula is: ((Dex(player)^2 - Dex(monster)^2)/10) + Random(-10/10) | |
// For dexterity values from 3 to 18, this should produce an offset of +/- 41.5 | |
int playerDexterity = player.GetAttribute("DEX").ModifiedValue * | |
player.GetAttribute("DEX").ModifiedValue; | |
int opponentDexterity = opponent.GetAttribute("DEX").ModifiedValue * | |
opponent.GetAttribute("DEX").ModifiedValue; | |
decimal dexterityOffset = (playerDexterity - opponentDexterity) / 10m; | |
int randomOffset = DiceService.Instance.Roll(20).Value - 10; | |
decimal totalOffset = dexterityOffset + randomOffset; | |
return DiceService.Instance.Roll(100).Value <= 50 + totalOffset | |
? Combatant.Player | |
: Combatant.Opponent; | |
} | |
public static bool AttackSucceeded(LivingEntity attacker, LivingEntity target) | |
{ | |
// Currently using the same formula as FirstAttacker initiative. | |
// This will change as we include attack/defense skills, | |
// armor, weapon bonuses, enchantments/curses, etc. | |
int playerDexterity = attacker.GetAttribute("DEX").ModifiedValue * | |
attacker.GetAttribute("DEX").ModifiedValue; | |
int opponentDexterity = target.GetAttribute("DEX").ModifiedValue * | |
target.GetAttribute("DEX").ModifiedValue; | |
decimal dexterityOffset = (playerDexterity - opponentDexterity) / 10m; | |
int randomOffset = DiceService.Instance.Roll(20).Value - 10; | |
decimal totalOffset = dexterityOffset + randomOffset; | |
return DiceService.Instance.Roll(100).Value <= 50 + totalOffset; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using System.Xml; | |
using Engine.Models; | |
using Newtonsoft.Json.Linq; | |
namespace Engine.Shared | |
{ | |
public static class ExtensionMethods | |
{ | |
public static int AttributeAsInt(this XmlNode node, string attributeName) | |
{ | |
return Convert.ToInt32(node.AttributeAsString(attributeName)); | |
} | |
public static string AttributeAsString(this XmlNode node, string attributeName) | |
{ | |
XmlAttribute attribute = node.Attributes?[attributeName]; | |
if(attribute == null) | |
{ | |
throw new ArgumentException($"The attribute '{attributeName}' does not exist"); | |
} | |
return attribute.Value; | |
} | |
public static string StringValueOf(this JObject jsonObject, string key) | |
{ | |
return jsonObject[key].ToString(); | |
} | |
public static string StringValueOf(this JToken jsonToken, string key) | |
{ | |
return jsonToken[key].ToString(); | |
} | |
public static int IntValueOf(this JToken jsonToken, string key) | |
{ | |
return Convert.ToInt32(jsonToken[key]); | |
} | |
public static PlayerAttribute GetAttribute(this LivingEntity entity, string attributeKey) | |
{ | |
return entity.Attributes | |
.First(pa => pa.Key.Equals(attributeKey, | |
StringComparison.CurrentCultureIgnoreCase)); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Linq; | |
using Engine.Factories; | |
using Engine.Models; | |
using Engine.Services; | |
using Newtonsoft.Json; | |
namespace Engine.ViewModels | |
{ | |
public class GameSession : BaseNotificationClass | |
{ | |
private readonly MessageBroker _messageBroker = MessageBroker.GetInstance(); | |
#region Properties | |
private GameDetails _gameDetails; | |
private Player _currentPlayer; | |
private Location _currentLocation; | |
private Battle _currentBattle; | |
private Monster _currentMonster; | |
private Trader _currentTrader; | |
[JsonIgnore] | |
public GameDetails GameDetails | |
{ | |
get => _gameDetails; | |
set | |
{ | |
_gameDetails = value; | |
OnPropertyChanged(); | |
} | |
} | |
[JsonIgnore] | |
public World CurrentWorld { get; } | |
public Player CurrentPlayer | |
{ | |
get => _currentPlayer; | |
set | |
{ | |
if(_currentPlayer != null) | |
{ | |
_currentPlayer.OnLeveledUp -= OnCurrentPlayerLeveledUp; | |
_currentPlayer.OnKilled -= OnPlayerKilled; | |
} | |
_currentPlayer = value; | |
if(_currentPlayer != null) | |
{ | |
_currentPlayer.OnLeveledUp += OnCurrentPlayerLeveledUp; | |
_currentPlayer.OnKilled += OnPlayerKilled; | |
} | |
} | |
} | |
public Location CurrentLocation | |
{ | |
get => _currentLocation; | |
set | |
{ | |
_currentLocation = value; | |
OnPropertyChanged(); | |
OnPropertyChanged(nameof(HasLocationToNorth)); | |
OnPropertyChanged(nameof(HasLocationToEast)); | |
OnPropertyChanged(nameof(HasLocationToWest)); | |
OnPropertyChanged(nameof(HasLocationToSouth)); | |
CompleteQuestsAtLocation(); | |
GivePlayerQuestsAtLocation(); | |
CurrentMonster = CurrentLocation.GetMonster(); | |
CurrentTrader = CurrentLocation.TraderHere; | |
} | |
} | |
[JsonIgnore] | |
public Monster CurrentMonster | |
{ | |
get => _currentMonster; | |
set | |
{ | |
if(_currentBattle != null) | |
{ | |
_currentBattle.OnCombatVictory -= OnCurrentMonsterKilled; | |
_currentBattle.Dispose(); | |
_currentBattle = null; | |
} | |
_currentMonster = value; | |
if(_currentMonster != null) | |
{ | |
_currentBattle = new Battle(CurrentPlayer, CurrentMonster); | |
_currentBattle.OnCombatVictory += OnCurrentMonsterKilled; | |
} | |
OnPropertyChanged(); | |
OnPropertyChanged(nameof(HasMonster)); | |
} | |
} | |
[JsonIgnore] | |
public Trader CurrentTrader | |
{ | |
get => _currentTrader; | |
set | |
{ | |
_currentTrader = value; | |
OnPropertyChanged(); | |
OnPropertyChanged(nameof(HasTrader)); | |
} | |
} | |
[JsonIgnore] | |
public bool HasLocationToNorth => | |
CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null; | |
[JsonIgnore] | |
public bool HasLocationToEast => | |
CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null; | |
[JsonIgnore] | |
public bool HasLocationToSouth => | |
CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null; | |
[JsonIgnore] | |
public bool HasLocationToWest => | |
CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null; | |
[JsonIgnore] | |
public bool HasMonster => CurrentMonster != null; | |
[JsonIgnore] | |
public bool HasTrader => CurrentTrader != null; | |
#endregion | |
public GameSession(Player player, int xCoordinate, int yCoordinate) | |
{ | |
PopulateGameDetails(); | |
CurrentWorld = WorldFactory.CreateWorld(); | |
CurrentPlayer = player; | |
CurrentLocation = CurrentWorld.LocationAt(xCoordinate, yCoordinate); | |
} | |
public void MoveNorth() | |
{ | |
if(HasLocationToNorth) | |
{ | |
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1); | |
} | |
} | |
public void MoveEast() | |
{ | |
if(HasLocationToEast) | |
{ | |
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate); | |
} | |
} | |
public void MoveSouth() | |
{ | |
if(HasLocationToSouth) | |
{ | |
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1); | |
} | |
} | |
public void MoveWest() | |
{ | |
if(HasLocationToWest) | |
{ | |
CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate); | |
} | |
} | |
private void PopulateGameDetails() | |
{ | |
GameDetails = GameDetailsService.ReadGameDetails(); | |
} | |
private void CompleteQuestsAtLocation() | |
{ | |
foreach(Quest quest in CurrentLocation.QuestsAvailableHere) | |
{ | |
QuestStatus questToComplete = | |
CurrentPlayer.Quests.FirstOrDefault(q => q.PlayerQuest.ID == quest.ID && | |
!q.IsCompleted); | |
if(questToComplete != null) | |
{ | |
if(CurrentPlayer.Inventory.HasAllTheseItems(quest.ItemsToComplete)) | |
{ | |
CurrentPlayer.RemoveItemsFromInventory(quest.ItemsToComplete); | |
_messageBroker.RaiseMessage(""); | |
_messageBroker.RaiseMessage($"You completed the '{quest.Name}' quest"); | |
// Give the player the quest rewards | |
_messageBroker.RaiseMessage($"You receive {quest.RewardExperiencePoints} experience points"); | |
CurrentPlayer.AddExperience(quest.RewardExperiencePoints); | |
_messageBroker.RaiseMessage($"You receive {quest.RewardGold} gold"); | |
CurrentPlayer.ReceiveGold(quest.RewardGold); | |
foreach(ItemQuantity itemQuantity in quest.RewardItems) | |
{ | |
GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemID); | |
_messageBroker.RaiseMessage($"You receive a {rewardItem.Name}"); | |
CurrentPlayer.AddItemToInventory(rewardItem); | |
} | |
// Mark the Quest as completed | |
questToComplete.IsCompleted = true; | |
} | |
} | |
} | |
} | |
private void GivePlayerQuestsAtLocation() | |
{ | |
foreach(Quest quest in CurrentLocation.QuestsAvailableHere) | |
{ | |
if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID)) | |
{ | |
CurrentPlayer.Quests.Add(new QuestStatus(quest)); | |
_messageBroker.RaiseMessage(""); | |
_messageBroker.RaiseMessage($"You receive the '{quest.Name}' quest"); | |
_messageBroker.RaiseMessage(quest.Description); | |
_messageBroker.RaiseMessage("Return with:"); | |
foreach(ItemQuantity itemQuantity in quest.ItemsToComplete) | |
{ | |
_messageBroker | |
.RaiseMessage($" {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}"); | |
} | |
_messageBroker.RaiseMessage("And you will receive:"); | |
_messageBroker.RaiseMessage($" {quest.RewardExperiencePoints} experience points"); | |
_messageBroker.RaiseMessage($" {quest.RewardGold} gold"); | |
foreach(ItemQuantity itemQuantity in quest.RewardItems) | |
{ | |
_messageBroker | |
.RaiseMessage($" {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}"); | |
} | |
} | |
} | |
} | |
public void AttackCurrentMonster() | |
{ | |
_currentBattle?.AttackOpponent(); | |
} | |
public void UseCurrentConsumable() | |
{ | |
if(CurrentPlayer.CurrentConsumable != null) | |
{ | |
if (_currentBattle == null) | |
{ | |
CurrentPlayer.OnActionPerformed += OnConsumableActionPerformed; | |
} | |
CurrentPlayer.UseCurrentConsumable(); | |
if (_currentBattle == null) | |
{ | |
CurrentPlayer.OnActionPerformed -= OnConsumableActionPerformed; | |
} | |
} | |
} | |
private void OnConsumableActionPerformed(object sender, string result) | |
{ | |
_messageBroker.RaiseMessage(result); | |
} | |
public void CraftItemUsing(Recipe recipe) | |
{ | |
if(CurrentPlayer.Inventory.HasAllTheseItems(recipe.Ingredients)) | |
{ | |
CurrentPlayer.RemoveItemsFromInventory(recipe.Ingredients); | |
foreach(ItemQuantity itemQuantity in recipe.OutputItems) | |
{ | |
for(int i = 0; i < itemQuantity.Quantity; i++) | |
{ | |
GameItem outputItem = ItemFactory.CreateGameItem(itemQuantity.ItemID); | |
CurrentPlayer.AddItemToInventory(outputItem); | |
_messageBroker.RaiseMessage($"You craft 1 {outputItem.Name}"); | |
} | |
} | |
} | |
else | |
{ | |
_messageBroker.RaiseMessage("You do not have the required ingredients:"); | |
foreach(ItemQuantity itemQuantity in recipe.Ingredients) | |
{ | |
_messageBroker | |
.RaiseMessage($" {itemQuantity.Quantity} {ItemFactory.ItemName(itemQuantity.ItemID)}"); | |
} | |
} | |
} | |
private void OnPlayerKilled(object sender, System.EventArgs e) | |
{ | |
_messageBroker.RaiseMessage(""); | |
_messageBroker.RaiseMessage("You have been killed."); | |
CurrentLocation = CurrentWorld.LocationAt(0, -1); | |
CurrentPlayer.CompletelyHeal(); | |
} | |
private void OnCurrentMonsterKilled(object sender, System.EventArgs eventArgs) | |
{ | |
// Get another monster to fight | |
CurrentMonster = CurrentLocation.GetMonster(); | |
} | |
private void OnCurrentPlayerLeveledUp(object sender, System.EventArgs eventArgs) | |
{ | |
_messageBroker.RaiseMessage($"You are now level {CurrentPlayer.Level}!"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Collections.ObjectModel; | |
using Engine.Services; | |
using Newtonsoft.Json; | |
namespace Engine.Models | |
{ | |
public abstract class LivingEntity : BaseNotificationClass | |
{ | |
#region Properties | |
private string _name; | |
private int _currentHitPoints; | |
private int _maximumHitPoints; | |
private int _gold; | |
private int _level; | |
private GameItem _currentWeapon; | |
private GameItem _currentConsumable; | |
private Inventory _inventory; | |
public ObservableCollection<PlayerAttribute> Attributes { get; } = | |
new ObservableCollection<PlayerAttribute>(); | |
public string Name | |
{ | |
get => _name; | |
private set | |
{ | |
_name = value; | |
OnPropertyChanged(); | |
} | |
} | |
public int CurrentHitPoints | |
{ | |
get => _currentHitPoints; | |
private set | |
{ | |
_currentHitPoints = value; | |
OnPropertyChanged(); | |
} | |
} | |
public int MaximumHitPoints | |
{ | |
get => _maximumHitPoints; | |
protected set | |
{ | |
_maximumHitPoints = value; | |
OnPropertyChanged(); | |
} | |
} | |
public int Gold | |
{ | |
get => _gold; | |
private set | |
{ | |
_gold = value; | |
OnPropertyChanged(); | |
} | |
} | |
public int Level | |
{ | |
get => _level; | |
protected set | |
{ | |
_level = value; | |
OnPropertyChanged(); | |
} | |
} | |
public Inventory Inventory | |
{ | |
get => _inventory; | |
private set | |
{ | |
_inventory = value; | |
OnPropertyChanged(); | |
} | |
} | |
public GameItem CurrentWeapon | |
{ | |
get => _currentWeapon; | |
set | |
{ | |
if (_currentWeapon != null) | |
{ | |
_currentWeapon.Action.OnActionPerformed -= RaiseActionPerformedEvent; | |
} | |
_currentWeapon = value; | |
if (_currentWeapon != null) | |
{ | |
_currentWeapon.Action.OnActionPerformed += RaiseActionPerformedEvent; | |
} | |
OnPropertyChanged(); | |
} | |
} | |
public GameItem CurrentConsumable | |
{ | |
get => _currentConsumable; | |
set | |
{ | |
if(_currentConsumable != null) | |
{ | |
_currentConsumable.Action.OnActionPerformed -= RaiseActionPerformedEvent; | |
} | |
_currentConsumable = value; | |
if (_currentConsumable != null) | |
{ | |
_currentConsumable.Action.OnActionPerformed += RaiseActionPerformedEvent; | |
} | |
OnPropertyChanged(); | |
} | |
} | |
[JsonIgnore] | |
public bool IsAlive => CurrentHitPoints > 0; | |
[JsonIgnore] | |
public bool IsDead => !IsAlive; | |
#endregion | |
public event EventHandler<string> OnActionPerformed; | |
public event EventHandler OnKilled; | |
protected LivingEntity(string name, int maximumHitPoints, int currentHitPoints, | |
IEnumerable<PlayerAttribute> attributes, int gold, int level = 1) | |
{ | |
Name = name; | |
MaximumHitPoints = maximumHitPoints; | |
CurrentHitPoints = currentHitPoints; | |
Gold = gold; | |
Level = level; | |
foreach (PlayerAttribute attribute in attributes) | |
{ | |
Attributes.Add(attribute); | |
} | |
Inventory = new Inventory(); | |
} | |
public void UseCurrentWeaponOn(LivingEntity target) | |
{ | |
CurrentWeapon.PerformAction(this, target); | |
} | |
public void UseCurrentConsumable() | |
{ | |
CurrentConsumable.PerformAction(this, this); | |
RemoveItemFromInventory(CurrentConsumable); | |
} | |
public void TakeDamage(int hitPointsOfDamage) | |
{ | |
CurrentHitPoints -= hitPointsOfDamage; | |
if(IsDead) | |
{ | |
CurrentHitPoints = 0; | |
RaiseOnKilledEvent(); | |
} | |
} | |
public void Heal(int hitPointsToHeal) | |
{ | |
CurrentHitPoints += hitPointsToHeal; | |
if(CurrentHitPoints > MaximumHitPoints) | |
{ | |
CurrentHitPoints = MaximumHitPoints; | |
} | |
} | |
public void CompletelyHeal() | |
{ | |
CurrentHitPoints = MaximumHitPoints; | |
} | |
public void ReceiveGold(int amountOfGold) | |
{ | |
Gold += amountOfGold; | |
} | |
public void SpendGold(int amountOfGold) | |
{ | |
if(amountOfGold > Gold) | |
{ | |
throw new ArgumentOutOfRangeException($"{Name} only has {Gold} gold, and cannot spend {amountOfGold} gold"); | |
} | |
Gold -= amountOfGold; | |
} | |
public void AddItemToInventory(GameItem item) | |
{ | |
Inventory = Inventory.AddItem(item); | |
} | |
public void RemoveItemFromInventory(GameItem item) | |
{ | |
Inventory = Inventory.RemoveItem(item); | |
} | |
public void RemoveItemsFromInventory(IEnumerable<ItemQuantity> itemQuantities) | |
{ | |
Inventory = Inventory.RemoveItems(itemQuantities); | |
} | |
#region Private functions | |
private void RaiseOnKilledEvent() | |
{ | |
OnKilled?.Invoke(this, new System.EventArgs()); | |
} | |
private void RaiseActionPerformedEvent(object sender, string result) | |
{ | |
OnActionPerformed?.Invoke(this, result); | |
} | |
#endregion | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Window x:Class="WPFUI.MainWindow" | |
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:viewModels="clr-namespace:Engine.ViewModels;assembly=Engine" | |
d:DataContext="{d:DesignInstance viewModels:GameSession}" | |
mc:Ignorable="d" | |
FontSize="11pt" | |
Title="{Binding GameDetails.Title}" Height="768" Width="1024" | |
KeyDown="MainWindow_OnKeyDown" | |
Closing="MainWindow_OnClosing"> | |
<Window.Resources> | |
<BooleanToVisibilityConverter x:Key="BooleanToVisibility" /> | |
</Window.Resources> | |
<Grid> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="*"/> | |
<RowDefinition Height="225"/> | |
</Grid.RowDefinitions> | |
<Grid.ColumnDefinitions> | |
<ColumnDefinition Width="250"/> | |
<ColumnDefinition Width="*"/> | |
</Grid.ColumnDefinitions> | |
<!-- Menu --> | |
<Menu Grid.Row="0" Grid.Column="0" | |
Grid.ColumnSpan="2" | |
FontSize="11pt" | |
Background="AliceBlue"> | |
<MenuItem Header="File"> | |
<MenuItem Header="Start New Game" | |
Click="StartNewGame_OnClick" | |
IsEnabled="False"/> | |
<Separator/> | |
<MenuItem Header="Load Game" | |
Click="LoadGame_OnClick" | |
IsEnabled="False"/> | |
<MenuItem Header="Save Game" | |
Click="SaveGame_OnClick" | |
IsEnabled="False"/> | |
<Separator/> | |
<MenuItem Header="Exit" | |
Click="Exit_OnClick"/> | |
</MenuItem> | |
<MenuItem Header="Help"> | |
<MenuItem Header="Help" | |
IsEnabled="False"/> | |
<Separator/> | |
<MenuItem Header="About" | |
IsEnabled="False"/> | |
</MenuItem> | |
</Menu> | |
<!-- Player stats --> | |
<Grid Grid.Row="1" Grid.Column="0" Background="Aquamarine"> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
</Grid.RowDefinitions> | |
<Grid.ColumnDefinitions> | |
<ColumnDefinition Width="Auto"/> | |
<ColumnDefinition Width="Auto"/> | |
</Grid.ColumnDefinitions> | |
<Label Grid.Row="0" Grid.Column="0" Content="Name:"/> | |
<Label Grid.Row="0" Grid.Column="1" Content="{Binding CurrentPlayer.Name}"/> | |
<Label Grid.Row="1" Grid.Column="0" Content="Hit points:"/> | |
<Label Grid.Row="1" Grid.Column="1" Content="{Binding CurrentPlayer.CurrentHitPoints}"/> | |
<Label Grid.Row="2" Grid.Column="0" Content="Gold:"/> | |
<Label Grid.Row="2" Grid.Column="1" Content="{Binding CurrentPlayer.Gold}"/> | |
<Label Grid.Row="3" Grid.Column="0" Content="XP:"/> | |
<Label Grid.Row="3" Grid.Column="1" Content="{Binding CurrentPlayer.ExperiencePoints}"/> | |
<Label Grid.Row="4" Grid.Column="0" Content="Level:"/> | |
<Label Grid.Row="4" Grid.Column="1" Content="{Binding CurrentPlayer.Level}"/> | |
<!-- Player Attributes --> | |
<ListBox Grid.Row="5" Grid.Column="0" | |
Grid.ColumnSpan="2" | |
Background="Aquamarine" | |
BorderThickness="0" | |
Grid.IsSharedSizeScope="True" | |
ItemsSource="{Binding CurrentPlayer.Attributes}"> | |
<ListBox.ItemTemplate> | |
<DataTemplate> | |
<StackPanel Orientation="Horizontal"> | |
<Grid> | |
<Grid.ColumnDefinitions> | |
<ColumnDefinition SharedSizeGroup="Description"/> | |
</Grid.ColumnDefinitions> | |
<TextBlock Text="{Binding DisplayName}" | |
HorizontalAlignment="Left" | |
MinWidth="100"/> | |
</Grid> | |
<Grid> | |
<Grid.ColumnDefinitions> | |
<ColumnDefinition SharedSizeGroup="ModifiedValue"/> | |
</Grid.ColumnDefinitions> | |
<TextBlock Text="{Binding ModifiedValue}" | |
HorizontalAlignment="Right"/> | |
</Grid> | |
</StackPanel> | |
</DataTemplate> | |
</ListBox.ItemTemplate> | |
</ListBox> | |
</Grid> | |
<!-- Gameplay --> | |
<Grid Grid.Row="1" Grid.Column="1" | |
Background="Beige"> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="*"/> | |
<RowDefinition Height="*"/> | |
</Grid.RowDefinitions> | |
<Grid.ColumnDefinitions> | |
<ColumnDefinition Width="3*"/> | |
<ColumnDefinition Width="2*"/> | |
</Grid.ColumnDefinitions> | |
<!-- Game Messages --> | |
<Border Grid.Row="0" Grid.Column="0" | |
Grid.RowSpan="2" | |
BorderBrush="Gainsboro" | |
BorderThickness="1"> | |
<RichTextBox x:Name="GameMessages" | |
Background="Beige" | |
VerticalScrollBarVisibility="Auto"> | |
<RichTextBox.Resources> | |
<Style TargetType="{x:Type Paragraph}"> | |
<Setter Property="Margin" Value="0"/> | |
</Style> | |
</RichTextBox.Resources> | |
</RichTextBox> | |
</Border> | |
<!-- Location information --> | |
<Border Grid.Row="0" Grid.Column="1" | |
BorderBrush="Gainsboro" | |
BorderThickness="1"> | |
<Grid Margin="3"> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="*"/> | |
<RowDefinition Height="Auto"/> | |
</Grid.RowDefinitions> | |
<TextBlock Grid.Row="0" | |
HorizontalAlignment="Center" | |
Text="{Binding CurrentLocation.Name}"/> | |
<Image Grid.Row="1" | |
HorizontalAlignment="Center" | |
VerticalAlignment="Center" | |
Height="125" | |
Width="125" | |
Source="{Binding CurrentLocation.ImageName, | |
Converter={StaticResource FileToBitmapConverter}}"/> | |
<TextBlock Grid.Row="2" | |
HorizontalAlignment="Center" | |
Text="{Binding CurrentLocation.Description}" | |
TextWrapping="Wrap"/> | |
</Grid> | |
</Border> | |
<!-- Monster information --> | |
<Border Grid.Row="1" Grid.Column="1" | |
BorderBrush="Gainsboro" | |
BorderThickness="1"> | |
<Grid> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="Auto" /> | |
<RowDefinition Height="*" /> | |
<RowDefinition Height="Auto" /> | |
</Grid.RowDefinitions> | |
<TextBlock Grid.Row="0" | |
HorizontalAlignment="Center" | |
Height="Auto" | |
Text="{Binding CurrentMonster.Name}" /> | |
<Image Grid.Row="1" | |
HorizontalAlignment="Center" | |
VerticalAlignment="Center" | |
Height="125" | |
Width="125" | |
Source="{Binding CurrentMonster.ImageName, | |
Converter={StaticResource FileToBitmapConverter}}"/> | |
<StackPanel Grid.Row="2" | |
Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}" | |
HorizontalAlignment="Center" | |
Orientation="Horizontal"> | |
<TextBlock>Current Hit Points:</TextBlock> | |
<TextBlock Text="{Binding CurrentMonster.CurrentHitPoints}" /> | |
</StackPanel> | |
</Grid> | |
</Border> | |
</Grid> | |
<!-- Inventory, Quests, and Recipes --> | |
<Grid Grid.Row="2" Grid.Column="0" | |
Background="BurlyWood"> | |
<TabControl x:Name="PlayerDataTabControl"> | |
<TabItem Header="Inventory" | |
x:Name="InventoryTabItem"> | |
<DataGrid ItemsSource="{Binding CurrentPlayer.Inventory.GroupedInventory}" | |
AutoGenerateColumns="False" | |
HeadersVisibility="Column"> | |
<DataGrid.Columns> | |
<DataGridTextColumn Header="Description" | |
Binding="{Binding Item.Name, Mode=OneWay}" | |
Width="*"/> | |
<DataGridTextColumn Header="Qty" | |
IsReadOnly="True" | |
Width="Auto" | |
Binding="{Binding Quantity, Mode=OneWay}"/> | |
<DataGridTextColumn Header="Price" | |
Binding="{Binding Item.Price, Mode=OneWay}" | |
Width="Auto"/> | |
</DataGrid.Columns> | |
</DataGrid> | |
</TabItem> | |
<TabItem Header="Quests" | |
x:Name="QuestsTabItem"> | |
<DataGrid ItemsSource="{Binding CurrentPlayer.Quests}" | |
AutoGenerateColumns="False" | |
HeadersVisibility="Column"> | |
<DataGrid.Columns> | |
<DataGridTextColumn Header="Name" | |
Binding="{Binding PlayerQuest.Name, Mode=OneWay}" | |
Width="*"> | |
<DataGridTextColumn.CellStyle> | |
<Style TargetType="DataGridCell"> | |
<Setter Property="ToolTip" | |
Value="{Binding PlayerQuest.ToolTipContents}"/> | |
</Style> | |
</DataGridTextColumn.CellStyle> | |
</DataGridTextColumn> | |
<DataGridTextColumn Header="Done?" | |
Binding="{Binding IsCompleted, Mode=OneWay}" | |
Width="Auto"/> | |
</DataGrid.Columns> | |
</DataGrid> | |
</TabItem> | |
<TabItem Header="Recipes" | |
x:Name="RecipesTabItem"> | |
<DataGrid ItemsSource="{Binding CurrentPlayer.Recipes}" | |
AutoGenerateColumns="False" | |
HeadersVisibility="Column"> | |
<DataGrid.Columns> | |
<DataGridTextColumn Header="Name" | |
Binding="{Binding Name, Mode=OneWay}" | |
Width="*"> | |
<DataGridTextColumn.CellStyle> | |
<Style TargetType="DataGridCell"> | |
<Setter Property="ToolTip" | |
Value="{Binding ToolTipContents}"/> | |
</Style> | |
</DataGridTextColumn.CellStyle> | |
</DataGridTextColumn> | |
<DataGridTemplateColumn MinWidth="75"> | |
<DataGridTemplateColumn.CellTemplate> | |
<DataTemplate> | |
<Button Click="OnClick_Craft" | |
Width="55" | |
Content="Craft"/> | |
</DataTemplate> | |
</DataGridTemplateColumn.CellTemplate> | |
</DataGridTemplateColumn> | |
</DataGrid.Columns> | |
</DataGrid> | |
</TabItem> | |
</TabControl> | |
</Grid> | |
<!-- Action controls --> | |
<Grid Grid.Row="2" Grid.Column="1" | |
Background="Lavender"> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="*" /> | |
</Grid.RowDefinitions> | |
<Grid.ColumnDefinitions> | |
<ColumnDefinition Width="*" /> | |
<ColumnDefinition Width="255" /> | |
</Grid.ColumnDefinitions> | |
<!-- Combat Controls --> | |
<Grid Grid.Row="0" Grid.Column="0" | |
HorizontalAlignment="Center" | |
VerticalAlignment="Center"> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
</Grid.RowDefinitions> | |
<Grid.ColumnDefinitions> | |
<ColumnDefinition Width="150"/> | |
<ColumnDefinition Width="10"/> | |
<ColumnDefinition Width="50"/> | |
</Grid.ColumnDefinitions> | |
<ComboBox Grid.Row="0" Grid.Column="0" | |
Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}" | |
ItemsSource="{Binding CurrentPlayer.Inventory.Weapons}" | |
SelectedItem="{Binding CurrentPlayer.CurrentWeapon}" | |
DisplayMemberPath="Name"/> | |
<Button Grid.Row="0" Grid.Column="2" | |
Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}" | |
Content="Use" | |
Click="OnClick_AttackMonster"/> | |
<ComboBox Grid.Row="1" Grid.Column="0" | |
Visibility="{Binding CurrentPlayer.Inventory.HasConsumable, Converter={StaticResource BooleanToVisibility}}" | |
ItemsSource="{Binding CurrentPlayer.Inventory.Consumables}" | |
SelectedItem="{Binding CurrentPlayer.CurrentConsumable}" | |
DisplayMemberPath="Name"/> | |
<Button Grid.Row="1" Grid.Column="2" | |
Visibility="{Binding CurrentPlayer.Inventory.HasConsumable, Converter={StaticResource BooleanToVisibility}}" | |
Content="Use" | |
Click="OnClick_UseCurrentConsumable"/> | |
</Grid> | |
<!-- Movement Controls --> | |
<Grid Grid.Row="0" Grid.Column="1"> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="*" /> | |
<RowDefinition Height="*" /> | |
<RowDefinition Height="*" /> | |
</Grid.RowDefinitions> | |
<Grid.ColumnDefinitions> | |
<ColumnDefinition Width="*" /> | |
<ColumnDefinition Width="*" /> | |
<ColumnDefinition Width="*" /> | |
</Grid.ColumnDefinitions> | |
<Button Grid.Row="0" Grid.Column="1" | |
Height="25" Width="65" Margin="10" | |
Click="OnClick_MoveNorth" | |
Visibility="{Binding HasLocationToNorth, Converter={StaticResource BooleanToVisibility}}" | |
Content="North"/> | |
<Button Grid.Row="1" Grid.Column="0" | |
Height="25" Width="65" Margin="10" | |
Click="OnClick_MoveWest" | |
Visibility="{Binding HasLocationToWest, Converter={StaticResource BooleanToVisibility}}" | |
Content="West"/> | |
<Button Grid.Row="1" Grid.Column="1" | |
Height="25" Width="65" Margin="10" | |
Click="OnClick_DisplayTradeScreen" | |
Visibility="{Binding HasTrader, Converter={StaticResource BooleanToVisibility}}" | |
Content="Trade"/> | |
<Button Grid.Row="1" Grid.Column="2" | |
Height="25" Width="65" Margin="10" | |
Click="OnClick_MoveEast" | |
Visibility="{Binding HasLocationToEast, Converter={StaticResource BooleanToVisibility}}" | |
Content="East"/> | |
<Button Grid.Row="2" Grid.Column="1" | |
Height="25" Width="65" Margin="10" | |
Click="OnClick_MoveSouth" | |
Visibility="{Binding HasLocationToSouth, Converter={StaticResource BooleanToVisibility}}" | |
Content="South"/> | |
</Grid> | |
</Grid> | |
</Grid> | |
</Window> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Windows; | |
using System.Windows.Controls; | |
using System.Windows.Documents; | |
using System.Windows.Input; | |
using Engine.EventArgs; | |
using Engine.Models; | |
using Engine.Services; | |
using Engine.ViewModels; | |
using Microsoft.Win32; | |
using WPFUI.Windows; | |
namespace WPFUI | |
{ | |
public partial class MainWindow : Window | |
{ | |
private const string SAVE_GAME_FILE_EXTENSION = "soscsrpg"; | |
private readonly MessageBroker _messageBroker = MessageBroker.GetInstance(); | |
private readonly Dictionary<Key, Action> _userInputActions = | |
new Dictionary<Key, Action>(); | |
private GameSession _gameSession; | |
public MainWindow(Player player) | |
{ | |
InitializeComponent(); | |
InitializeUserInputActions(); | |
SetActiveGameSessionTo(new GameSession(player, 0, 0)); | |
} | |
private void OnClick_MoveNorth(object sender, RoutedEventArgs e) | |
{ | |
_gameSession.MoveNorth(); | |
} | |
private void OnClick_MoveWest(object sender, RoutedEventArgs e) | |
{ | |
_gameSession.MoveWest(); | |
} | |
private void OnClick_MoveEast(object sender, RoutedEventArgs e) | |
{ | |
_gameSession.MoveEast(); | |
} | |
private void OnClick_MoveSouth(object sender, RoutedEventArgs e) | |
{ | |
_gameSession.MoveSouth(); | |
} | |
private void OnClick_AttackMonster(object sender, RoutedEventArgs e) | |
{ | |
_gameSession.AttackCurrentMonster(); | |
} | |
private void OnClick_UseCurrentConsumable(object sender, RoutedEventArgs e) | |
{ | |
_gameSession.UseCurrentConsumable(); | |
} | |
private void OnGameMessageRaised(object sender, GameMessageEventArgs e) | |
{ | |
GameMessages.Document.Blocks.Add(new Paragraph(new Run(e.Message))); | |
GameMessages.ScrollToEnd(); | |
} | |
private void OnClick_DisplayTradeScreen(object sender, RoutedEventArgs e) | |
{ | |
if(_gameSession.CurrentTrader != null) | |
{ | |
TradeScreen tradeScreen = new TradeScreen(); | |
tradeScreen.Owner = this; | |
tradeScreen.DataContext = _gameSession; | |
tradeScreen.ShowDialog(); | |
} | |
} | |
private void OnClick_Craft(object sender, RoutedEventArgs e) | |
{ | |
Recipe recipe = ((FrameworkElement)sender).DataContext as Recipe; | |
_gameSession.CraftItemUsing(recipe); | |
} | |
private void InitializeUserInputActions() | |
{ | |
_userInputActions.Add(Key.W, () => _gameSession.MoveNorth()); | |
_userInputActions.Add(Key.A, () => _gameSession.MoveWest()); | |
_userInputActions.Add(Key.S, () => _gameSession.MoveSouth()); | |
_userInputActions.Add(Key.D, () => _gameSession.MoveEast()); | |
_userInputActions.Add(Key.Z, () => _gameSession.AttackCurrentMonster()); | |
_userInputActions.Add(Key.C, () => _gameSession.UseCurrentConsumable()); | |
_userInputActions.Add(Key.I, () => SetTabFocusTo("InventoryTabItem")); | |
_userInputActions.Add(Key.Q, () => SetTabFocusTo("QuestsTabItem")); | |
_userInputActions.Add(Key.R, () => SetTabFocusTo("RecipesTabItem")); | |
_userInputActions.Add(Key.T, () => OnClick_DisplayTradeScreen(this, new RoutedEventArgs())); | |
} | |
private void MainWindow_OnKeyDown(object sender, KeyEventArgs e) | |
{ | |
if(_userInputActions.ContainsKey(e.Key)) | |
{ | |
_userInputActions[e.Key].Invoke(); | |
} | |
} | |
private void SetTabFocusTo(string tabName) | |
{ | |
foreach(object item in PlayerDataTabControl.Items) | |
{ | |
if (item is TabItem tabItem) | |
{ | |
if (tabItem.Name == tabName) | |
{ | |
tabItem.IsSelected = true; | |
return; | |
} | |
} | |
} | |
} | |
private void SetActiveGameSessionTo(GameSession gameSession) | |
{ | |
// Unsubscribe from OnMessageRaised, or we will get double messages | |
_messageBroker.OnMessageRaised -= OnGameMessageRaised; | |
_gameSession = gameSession; | |
DataContext = _gameSession; | |
// Clear out previous game's messages | |
GameMessages.Document.Blocks.Clear(); | |
_messageBroker.OnMessageRaised += OnGameMessageRaised; | |
} | |
private void StartNewGame_OnClick(object sender, RoutedEventArgs e) | |
{ | |
//SetActiveGameSessionTo(new GameSession()); | |
} | |
private void LoadGame_OnClick(object sender, RoutedEventArgs e) | |
{ | |
//OpenFileDialog openFileDialog = | |
// new OpenFileDialog | |
// { | |
// InitialDirectory = AppDomain.CurrentDomain.BaseDirectory, | |
// Filter = $"Saved games (*.{SAVE_GAME_FILE_EXTENSION})|*.{SAVE_GAME_FILE_EXTENSION}" | |
// }; | |
//if(openFileDialog.ShowDialog() == true) | |
//{ | |
// SetActiveGameSessionTo(SaveGameService.LoadLastSaveOrCreateNew(openFileDialog.FileName)); | |
//} | |
} | |
private void SaveGame_OnClick(object sender, RoutedEventArgs e) | |
{ | |
SaveGame(); | |
} | |
private void Exit_OnClick(object sender, RoutedEventArgs e) | |
{ | |
Close(); | |
} | |
private void MainWindow_OnClosing(object sender, CancelEventArgs e) | |
{ | |
YesNoWindow message = | |
new YesNoWindow("Save Game", "Do you want to save your game?"); | |
message.Owner = GetWindow(this); | |
message.ShowDialog(); | |
if (message.ClickedYes) | |
{ | |
SaveGame(); | |
} | |
} | |
private void SaveGame() | |
{ | |
SaveFileDialog saveFileDialog = | |
new SaveFileDialog | |
{ | |
InitialDirectory = AppDomain.CurrentDomain.BaseDirectory, | |
Filter = $"Saved games (*.{SAVE_GAME_FILE_EXTENSION})|*.{SAVE_GAME_FILE_EXTENSION}" | |
}; | |
if (saveFileDialog.ShowDialog() == true) | |
{ | |
//SaveGameService.Save(_gameSession, saveFileDialog.FileName); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using Engine.Factories; | |
using Engine.Services; | |
namespace Engine.Models | |
{ | |
public class Monster : LivingEntity | |
{ | |
private readonly List<ItemPercentage> _lootTable = | |
new List<ItemPercentage>(); | |
public int ID { get; } | |
public string ImageName { get; } | |
public int RewardExperiencePoints { get; } | |
public Monster(int id, string name, string imageName, | |
int maximumHitPoints, IEnumerable<PlayerAttribute> attributes, | |
GameItem currentWeapon, | |
int rewardExperiencePoints, int gold) : | |
base(name, maximumHitPoints, maximumHitPoints, attributes, gold) | |
{ | |
ID = id; | |
ImageName = imageName; | |
CurrentWeapon = currentWeapon; | |
RewardExperiencePoints = rewardExperiencePoints; | |
} | |
public void AddItemToLootTable(int id, int percentage) | |
{ | |
// Remove the entry from the loot table, | |
// if it already contains an entry with this ID | |
_lootTable.RemoveAll(ip => ip.ID == id); | |
_lootTable.Add(new ItemPercentage(id, percentage)); | |
} | |
public Monster GetNewInstance() | |
{ | |
// "Clone" this monster to a new Monster object | |
Monster newMonster = | |
new Monster(ID, Name, ImageName, MaximumHitPoints, Attributes, | |
CurrentWeapon, RewardExperiencePoints, Gold); | |
foreach(ItemPercentage itemPercentage in _lootTable) | |
{ | |
// Clone the loot table - even though we probably won't need it | |
newMonster.AddItemToLootTable(itemPercentage.ID, itemPercentage.Percentage); | |
// Populate the new monster's inventory, using the loot table | |
if(DiceService.Instance.Roll(100).Value <= itemPercentage.Percentage) | |
{ | |
newMonster.AddItemToInventory(ItemFactory.CreateGameItem(itemPercentage.ID)); | |
} | |
} | |
return newMonster; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Xml; | |
using Engine.Models; | |
using Engine.Services; | |
using Engine.Shared; | |
namespace Engine.Factories | |
{ | |
public static class MonsterFactory | |
{ | |
private const string GAME_DATA_FILENAME = ".\\GameData\\Monsters.xml"; | |
private static readonly GameDetails s_gameDetails; | |
private static readonly List<Monster> _baseMonsters = new List<Monster>(); | |
static MonsterFactory() | |
{ | |
if(File.Exists(GAME_DATA_FILENAME)) | |
{ | |
s_gameDetails = GameDetailsService.ReadGameDetails(); | |
XmlDocument data = new XmlDocument(); | |
data.LoadXml(File.ReadAllText(GAME_DATA_FILENAME)); | |
string rootImagePath = | |
data.SelectSingleNode("/Monsters") | |
.AttributeAsString("RootImagePath"); | |
LoadMonstersFromNodes(data.SelectNodes("/Monsters/Monster"), rootImagePath); | |
} | |
else | |
{ | |
throw new FileNotFoundException($"Missing data file: {GAME_DATA_FILENAME}"); | |
} | |
} | |
private static void LoadMonstersFromNodes(XmlNodeList nodes, string rootImagePath) | |
{ | |
if(nodes == null) | |
{ | |
return; | |
} | |
foreach(XmlNode node in nodes) | |
{ | |
var attributes = s_gameDetails.PlayerAttributes; | |
attributes.First(a => a.Key.Equals("DEX")).BaseValue = | |
Convert.ToInt32(node.SelectSingleNode("./Dexterity").InnerText); | |
attributes.First(a => a.Key.Equals("DEX")).ModifiedValue = | |
Convert.ToInt32(node.SelectSingleNode("./Dexterity").InnerText); | |
Monster monster = | |
new Monster(node.AttributeAsInt("ID"), | |
node.AttributeAsString("Name"), | |
$".{rootImagePath}{node.AttributeAsString("ImageName")}", | |
node.AttributeAsInt("MaximumHitPoints"), | |
attributes, | |
ItemFactory.CreateGameItem(node.AttributeAsInt("WeaponID")), | |
node.AttributeAsInt("RewardXP"), | |
node.AttributeAsInt("Gold")); | |
XmlNodeList lootItemNodes = node.SelectNodes("./LootItems/LootItem"); | |
if(lootItemNodes != null) | |
{ | |
foreach(XmlNode lootItemNode in lootItemNodes) | |
{ | |
monster.AddItemToLootTable(lootItemNode.AttributeAsInt("ID"), | |
lootItemNode.AttributeAsInt("Percentage")); | |
} | |
} | |
_baseMonsters.Add(monster); | |
} | |
} | |
public static Monster GetMonster(int id) | |
{ | |
return _baseMonsters.FirstOrDefault(m => m.ID == id)?.GetNewInstance(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Collections.ObjectModel; | |
using System.Linq; | |
namespace Engine.Models | |
{ | |
public class Player : LivingEntity | |
{ | |
#region Properties | |
private int _experiencePoints; | |
public int ExperiencePoints | |
{ | |
get => _experiencePoints; | |
private set | |
{ | |
_experiencePoints = value; | |
OnPropertyChanged(); | |
SetLevelAndMaximumHitPoints(); | |
} | |
} | |
public ObservableCollection<QuestStatus> Quests { get; } = | |
new ObservableCollection<QuestStatus>(); | |
public ObservableCollection<Recipe> Recipes { get; } = | |
new ObservableCollection<Recipe>(); | |
#endregion | |
public event EventHandler OnLeveledUp; | |
public Player(string name, int experiencePoints, | |
int maximumHitPoints, int currentHitPoints, | |
IEnumerable<PlayerAttribute> attributes, int gold) : | |
base(name, maximumHitPoints, currentHitPoints, attributes, gold) | |
{ | |
ExperiencePoints = experiencePoints; | |
} | |
public void AddExperience(int experiencePoints) | |
{ | |
ExperiencePoints += experiencePoints; | |
} | |
public void LearnRecipe(Recipe recipe) | |
{ | |
if(!Recipes.Any(r => r.ID == recipe.ID)) | |
{ | |
Recipes.Add(recipe); | |
} | |
} | |
private void SetLevelAndMaximumHitPoints() | |
{ | |
int originalLevel = Level; | |
Level = (ExperiencePoints / 100) + 1; | |
if (Level != originalLevel) | |
{ | |
MaximumHitPoints = Level * 10; | |
OnLeveledUp?.Invoke(this, System.EventArgs.Empty); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using Engine.Factories; | |
using Engine.Models; | |
using Engine.ViewModels; | |
using Newtonsoft.Json; | |
using Newtonsoft.Json.Linq; | |
namespace Engine.Services | |
{ | |
public static class SaveGameService | |
{ | |
public static void Save(GameSession gameSession, string fileName) | |
{ | |
File.WriteAllText(fileName, | |
JsonConvert.SerializeObject(gameSession, Formatting.Indented)); | |
} | |
public static GameSession LoadLastSaveOrCreateNew(string fileName) | |
{ | |
if(!File.Exists(fileName)) | |
{ | |
return null; | |
} | |
// Save game file exists, so create the GameSession object from it. | |
JObject data = JObject.Parse(File.ReadAllText(fileName)); | |
// Populate Player object | |
Player player = CreatePlayer(data); | |
int x = (int)data[nameof(GameSession.CurrentLocation)][nameof(Location.XCoordinate)]; | |
int y = (int)data[nameof(GameSession.CurrentLocation)][nameof(Location.YCoordinate)]; | |
// Create GameSession object with saved game data | |
return new GameSession(player, x, y); | |
} | |
private static Player CreatePlayer(JObject data) | |
{ | |
//string fileVersion = FileVersion(data); | |
//Player player; | |
//switch(fileVersion) | |
//{ | |
// case "0.1.000": | |
// player = | |
// new Player((string)data[nameof(GameSession.CurrentPlayer)][nameof(Player.Name)], | |
// (int)data[nameof(GameSession.CurrentPlayer)][nameof(Player.ExperiencePoints)], | |
// (int)data[nameof(GameSession.CurrentPlayer)][nameof(Player.MaximumHitPoints)], | |
// (int)data[nameof(GameSession.CurrentPlayer)][nameof(Player.CurrentHitPoints)], | |
// (int)data[nameof(GameSession.CurrentPlayer)][nameof(Player.Dexterity)], | |
// (int)data[nameof(GameSession.CurrentPlayer)][nameof(Player.Gold)]); | |
// break; | |
// default: | |
// throw new InvalidDataException($"File version '{fileVersion}' not recognized"); | |
//} | |
//PopulatePlayerInventory(data, player); | |
//PopulatePlayerQuests(data, player); | |
//PopulatePlayerRecipes(data, player); | |
//return player; | |
return null; | |
} | |
private static void PopulatePlayerInventory(JObject data, Player player) | |
{ | |
string fileVersion = FileVersion(data); | |
switch(fileVersion) | |
{ | |
case "0.1.000": | |
foreach(JToken itemToken in (JArray)data[nameof(GameSession.CurrentPlayer)] | |
[nameof(Player.Inventory)] | |
[nameof(Inventory.Items)]) | |
{ | |
int itemId = (int)itemToken[nameof(GameItem.ItemTypeID)]; | |
player.AddItemToInventory(ItemFactory.CreateGameItem(itemId)); | |
} | |
break; | |
default: | |
throw new InvalidDataException($"File version '{fileVersion}' not recognized"); | |
} | |
} | |
private static void PopulatePlayerQuests(JObject data, Player player) | |
{ | |
string fileVersion = FileVersion(data); | |
switch(fileVersion) | |
{ | |
case "0.1.000": | |
foreach(JToken questToken in (JArray)data[nameof(GameSession.CurrentPlayer)] | |
[nameof(Player.Quests)]) | |
{ | |
int questId = | |
(int)questToken[nameof(QuestStatus.PlayerQuest)][nameof(QuestStatus.PlayerQuest.ID)]; | |
Quest quest = QuestFactory.GetQuestByID(questId); | |
QuestStatus questStatus = new QuestStatus(quest); | |
questStatus.IsCompleted = (bool)questToken[nameof(QuestStatus.IsCompleted)]; | |
player.Quests.Add(questStatus); | |
} | |
break; | |
default: | |
throw new InvalidDataException($"File version '{fileVersion}' not recognized"); | |
} | |
} | |
private static void PopulatePlayerRecipes(JObject data, Player player) | |
{ | |
string fileVersion = FileVersion(data); | |
switch(fileVersion) | |
{ | |
case "0.1.000": | |
foreach(JToken recipeToken in | |
(JArray)data[nameof(GameSession.CurrentPlayer)][nameof(Player.Recipes)]) | |
{ | |
int recipeId = (int)recipeToken[nameof(Recipe.ID)]; | |
Recipe recipe = RecipeFactory.RecipeByID(recipeId); | |
player.Recipes.Add(recipe); | |
} | |
break; | |
default: | |
throw new InvalidDataException($"File version '{fileVersion}' not recognized"); | |
} | |
} | |
private static string FileVersion(JObject data) | |
{ | |
return (string)data[nameof(GameSession.GameDetails.Version)]; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Engine.Models; | |
using Engine.Services; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace TestEngine.Services | |
{ | |
[TestClass] | |
public class TestCombatService | |
{ | |
[TestMethod] | |
public void Test_FirstAttacker() | |
{ | |
// Player and monster with dexterity 12 | |
//Player player = new Player("", 0, 0, 0, 18, 0); | |
//Monster monster = new Monster(0, "", "", 0, 12, null, 0, 0); | |
//CombatService.Combatant result = CombatService.FirstAttacker(player, monster); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Engine.Models; | |
using Engine.ViewModels; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace TestEngine.ViewModels | |
{ | |
[TestClass] | |
public class TestGameSession | |
{ | |
[TestMethod] | |
public void TestCreateGameSession() | |
{ | |
//Player player = new Player("", 0, 10, 10, 15, 10); | |
//GameSession gameSession = new GameSession(player, 0, 0); | |
//Assert.IsNotNull(gameSession.CurrentPlayer); | |
//Assert.AreEqual("Town Square", gameSession.CurrentLocation.Name); | |
} | |
[TestMethod] | |
public void TestPlayerMovesHomeAndIsCompletelyHealedOnKilled() | |
{ | |
//Player player = new Player("", 0, 10, 10, 15, 10); | |
//GameSession gameSession = new GameSession(player, 0, 0); | |
//gameSession.CurrentPlayer.TakeDamage(999); | |
//Assert.AreEqual("Home", gameSession.CurrentLocation.Name); | |
//Assert.AreEqual(gameSession.CurrentPlayer.Level * 10, gameSession.CurrentPlayer.CurrentHitPoints); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Linq; | |
using Engine.Services; | |
using Engine.ViewModels; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace TestEngine.Services | |
{ | |
[TestClass] | |
public class TestSaveGameService | |
{ | |
[TestMethod] | |
public void Test_Restore_0_1_000() | |
{ | |
//GameSession gameSession = | |
// SaveGameService | |
// .LoadLastSaveOrCreateNew(@".\TestFiles\SavedGames\Game_0_1_000.soscsrpg"); | |
//// Game session data | |
//Assert.AreEqual("0.1.000", gameSession.GameDetails.Version); | |
//Assert.AreEqual(0, gameSession.CurrentLocation.XCoordinate); | |
//Assert.AreEqual(1, gameSession.CurrentLocation.YCoordinate); | |
//// Player data | |
//Assert.AreEqual("Scott", gameSession.CurrentPlayer.Name); | |
//Assert.AreEqual(18, gameSession.CurrentPlayer.Dexterity); | |
//Assert.AreEqual(8, gameSession.CurrentPlayer.CurrentHitPoints); | |
//Assert.AreEqual(10, gameSession.CurrentPlayer.MaximumHitPoints); | |
//Assert.AreEqual(20, gameSession.CurrentPlayer.ExperiencePoints); | |
//Assert.AreEqual(1, gameSession.CurrentPlayer.Level); | |
//Assert.AreEqual(1000000, gameSession.CurrentPlayer.Gold); | |
//// Player quest data | |
//Assert.AreEqual(1, gameSession.CurrentPlayer.Quests.Count); | |
//Assert.AreEqual(1, gameSession.CurrentPlayer.Quests[0].PlayerQuest.ID); | |
//Assert.IsFalse(gameSession.CurrentPlayer.Quests[0].IsCompleted); | |
//// Player recipe data | |
//Assert.AreEqual(1, gameSession.CurrentPlayer.Recipes.Count); | |
//Assert.AreEqual(1, gameSession.CurrentPlayer.Recipes[0].ID); | |
//// Player inventory data | |
//Assert.AreEqual(5, gameSession.CurrentPlayer.Inventory.Items.Count); | |
//Assert.AreEqual(1, gameSession.CurrentPlayer.Inventory.Items.Count(i => i.ItemTypeID.Equals(1001))); | |
//Assert.AreEqual(1, gameSession.CurrentPlayer.Inventory.Items.Count(i => i.ItemTypeID.Equals(2001))); | |
//Assert.AreEqual(1, gameSession.CurrentPlayer.Inventory.Items.Count(i => i.ItemTypeID.Equals(3001))); | |
//Assert.AreEqual(1, gameSession.CurrentPlayer.Inventory.Items.Count(i => i.ItemTypeID.Equals(3002))); | |
//Assert.AreEqual(1, gameSession.CurrentPlayer.Inventory.Items.Count(i => i.ItemTypeID.Equals(3003))); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
namespace Engine.Models | |
{ | |
public class Trader : LivingEntity | |
{ | |
public int ID { get; } | |
public Trader(int id, string name) : base(name, 9999, 9999, new List<PlayerAttribute>(), 9999) | |
{ | |
ID = id; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment