Пошаговая боевая система C# — обзор кода и лучшие практики?C#

Место общения программистов C#
Anonymous
Пошаговая боевая система C# — обзор кода и лучшие практики?

Сообщение Anonymous »

Я работаю над небольшим консольным пошаговым файтингом 1 на 1 на C#, и мне бы очень хотелось получить общий обзор кода, ориентированный на качество кода и лучшие практики.
Идея проста:
  • У каждого персонажа есть характеристики: здоровье (0–100), атака (0–50), защита (0–50), скорость (0–25), удача (0–1)
  • Бой пошаговый:

    Атакующий рассчитывает базовый урон (

    Код: Выделить всё

    AttackNormal
    )
  • Защитник определяет результат () → полная блокировка/частичная блокировка/сбой

Вот моя текущая реализация:
//character.cs

Код: Выделить всё

namespace FightingGame;

public enum DefenseResultType
{
FullBlock,
PartialBlock,
Failed
}
public class DefenseResult(DefenseResultType resultType, int finalDamage)
{
public DefenseResultType ResultType { get; set; } = resultType;
public int FinalDamage { get; set; } = finalDamage;
}
public class Character(string name, int health, int attack, int defense, int speed, Ability? ability, float luck)
{
private string _name = name;
private int _health = health;
private int _attack = attack;
private int _defense = defense;
private int _speed = speed;
private Ability? _ability = ability;
private float _luck = luck;

public string GetName()
{
return _name;
}
public int GetHealth()
{
return _health;
}
public int GetAttack()
{
return _attack;
}
public int GetDefense()
{
return _defense;
}
public int GetSpeed()
{
return _speed;
}
public Ability GetAbility()
{
return _ability;
}
public float GetLuck()
{
return _luck;
}

public void SetName(string name)
{
if(!string.IsNullOrWhiteSpace(name) && name.Length > 1) _name = name;
}
public void SetHealth(int health)
{
if(health >= 0 && health  0 && attack  0 && defense  0 && speed = 0 && luck   in future multiple dices can be implemented
int dice = 6;

// dice roll * 5 + (defense + speed/2)
int defenseRoll = Random.Shared.Next(1, dice + 1) * 10 + (int)(defense + Math.Floor((float)speed / 2));
// (attack + speed/2) * luck
int attackPressure = (int)(attackerAttack + Math.Floor((float)attackerSpeed / 2) * attackerLuck);

// full block
if (defenseRoll >= attackPressure + 30)
{
return (DefenseResultType.FullBlock, 0);
}
// partial block
if(defenseRoll >= attackPressure + 15)
{
return (DefenseResultType.PartialBlock, incomingDamage / 3);
}
// failed block
return  (DefenseResultType.Failed, incomingDamage);
}
public void TakeDamage(int damage)
{
if (damage > 0)
{
_health = Math.Clamp(_health - damage, 0, 100);
}
}
}
Я сделал несколько быстрых тестовых настроек, чтобы увидеть, как работает баланс, и все в порядке, не стесняйтесь судить сами

//program.cs:

Код: Выделить всё

namespace FightingGame;

class Program
{
public static void Main(string[] args)
{
Character knight = new Character("Knight", 100, 28, 36, 10, null, 0.20f);
Character assassin = new Character("Assassin", 100, 32, 14, 24, null, 0.42f);
Character guardian = new Character("Guardian", 100, 22, 44, 8, null, 0.10f);
Character berserker = new Character("Berserker", 100, 41, 12, 14, null, 0.25f);
Character duelist = new Character("Duelist", 100, 30, 24, 18, null, 0.30f);
Character scout = new Character("Scout", 100, 24, 16, 25, null, 0.38f);

Console.WriteLine("=== CHARACTER STATS ===");
PrintStats(knight);
PrintStats(assassin);
PrintStats(guardian);
PrintStats(berserker);
PrintStats(duelist);
PrintStats(scout);

Console.WriteLine("\n=== FIGHTS ===\n");

SimulateFight(knight, assassin);
SimulateFight(guardian, berserker);
SimulateFight(duelist, scout);
SimulateFight(knight, berserker);
}

private static void PrintStats(Character character)
{
Console.WriteLine(
$"{character.GetName(),-10} | HP: {character.GetHealth(),3} | ATK: {character.GetAttack(),2} | DEF: {character.GetDefense(),2} | SPD: {character.GetSpeed(),2} | LUCK: {character.GetLuck():0.00}"
);
}

private static void SimulateFight(Character firstTemplate, Character secondTemplate)
{
Character fighterA = CloneCharacter(firstTemplate);
Character fighterB = CloneCharacter(secondTemplate);

Console.WriteLine("==================================================");
Console.WriteLine($"FIGHT: {fighterA.GetName()} vs {fighterB.GetName()}");
Console.WriteLine("==================================================");

PrintStats(fighterA);
PrintStats(fighterB);
Console.WriteLine();

Character attacker;
Character defender;

if (fighterA.GetSpeed() > fighterB.GetSpeed())
{
attacker = fighterA;
defender = fighterB;
}
else if (fighterB.GetSpeed() > fighterA.GetSpeed())
{
attacker = fighterB;
defender = fighterA;
}
else
{
attacker = Random.Shared.Next(0, 2) == 0 ? fighterA : fighterB;
defender = attacker == fighterA ? fighterB : fighterA;
}

Console.WriteLine($"Attacks first: {attacker.GetName()}");
Console.WriteLine();

int round = 1;
int maxRounds = 30;

while (fighterA.GetHealth() > 0 && fighterB.GetHealth() > 0 &&  round  fighterB.GetHealth())
{
Console.WriteLine($"WINNER: {fighterA.GetName()}");
}
else if (fighterB.GetHealth() > fighterA.GetHealth())
{
Console.WriteLine($"WINNER: {fighterB.GetName()}");
}
else
{
Console.WriteLine("DRAW");
}

Console.WriteLine();
}

private static void ExecuteTurn(Character attacker, Character defender)
{
Console.WriteLine($"{attacker.GetName()} attacks {defender.GetName()}");

int baseDamage = attacker.AttackNormal(defender);
var (defenseResult, finalDamage) = defender.Defend(attacker, baseDamage);

Console.WriteLine($"Base damage: {baseDamage}");

switch (defenseResult)
{
case DefenseResultType.FullBlock:
Console.WriteLine($"{defender.GetName()} does FULL BLOCK.");
break;

case DefenseResultType.PartialBlock:
Console.WriteLine($"{defender.GetName()} does PARTIAL BLOCK.");
break;

case DefenseResultType.Failed:
Console.WriteLine($"{defender.GetName()} didn't defend.");
break;
}

Console.WriteLine($"Final damage: {finalDamage}");

defender.TakeDamage(finalDamage);

Console.WriteLine(
$"HP after round -> {attacker.GetName()}: {attacker.GetHealth()} | {defender.GetName()}: {defender.GetHealth()}"
);
Console.WriteLine();
}

private static Character CloneCharacter(Character original)
{
return new Character(
original.GetName(),
original.GetHealth(),
original.GetAttack(),
original.GetDefense(),
original.GetSpeed(),
original.GetAbility(),
original.GetLuck()
);
}
}
Что я в основном ищу:
  • Правильно ли я структурирую обязанности между Атакой и Защитой?
  • Уместна ли такая логика внутри класса Символ?
  • Есть ли нарушения лучших практик C#? Последний раз я использовал C# некоторое время назад и не знаю, все ли правильно помню.
  • Предложения по тому, как сделать код более удобным в сопровождении или расширяемым - я хочу добавить к персонажам способности, например какую-то специальную атаку, какой подход будет лучшим?
  • Общие мысли о читабельности и дизайне (не только математика/баланс)
Это всего лишь учебный проект, поэтому меня особенно интересуют отзывы с точки зрения чистого кода и архитектуры, а не просто настройки игрового процесса, но я тоже буду признателен за эти советы.
Заранее спасибо!

Вернуться в «C#»