Бойната система
Накратко: Боят се върти на тикове: всеки добавя бързината си към лента с енергия, който стигне 100, действа и плаща 100. Ударът е точност срещу избягване, щета = оръжие + сила − защита (поне 1), критичен удар удвоява, а видът щета решава какво следва.
Проблем
Вълкът е по-бърз от голема
В мината героят среща вълк кръвник и гоблин едновременно. Кой удря пръв и колко пъти? Първият отговор е по кръгове: героят, после всеки враг по реда им, и отначало. Но вълкът е два пъти по-бърз от рудния голем (бързина 14 срещу 7). Ако и двамата удрят веднъж на кръг, бързината не значи нищо, а играч, който е вложил точки в нея, не получава нищо.
Затова редът е друг, а ударът има свои правила. Ето екран на боя от играта (от минаването, с което се проверява играта): героят е срещу гигантски плъх, дневникът пази последните редове, а ударът на героя е пропуснат.
=== Бой ===Ива, варварин ур.1 Ж [###########-] 46/48 М [########] 4/41) Гигантски плъх [########] 9/9 Бой! Срещу теб: гигантски плъх. Гигантски плъх драска Ива (1). Ива пропуска гигантски плъх. Гигантски плъх драска Ива (1).1 удар 2 умение 3 предмет 4 бягствоЗа този кадър трябват пет неща: ред на ходовете, точност срещу избягване, щета, видове щета и думи в дневника. Урокът ги строи едно по едно с малки програми, а накрая ги събира в бой на героя с вълк и гоблин и показва същото в играта. Бойците тук са със готови показатели: как се събират от база, екипировка и ефекти, е в урока за същества.
Идеята
Бойци с готови показатели
Боецът е клас с показатели, живот и една лента с енергия:
class Fighter{ public string Name; public int MaxHp; public int Strength; public int Agility; public int Defense; public int Speed; public int WeaponMin; public int WeaponMax; public string Kind; public string Resist; public string Weak; public int Hp; public int Energy;
public Fighter(string name, int maxHp, int strength, int agility, int defense, int speed, int weaponMin, int weaponMax, string kind, string resist, string weak) { Name = name; MaxHp = maxHp; Strength = strength; Agility = agility; Defense = defense; Speed = speed; WeaponMin = weaponMin; WeaponMax = weaponMax; Kind = kind; Resist = resist; Weak = weak; Hp = maxHp; }
public int Accuracy() { return 75 + Agility * 2; }
public int Evasion() { return Agility * 2; }
public int CritChance() { return 5 + Agility / 10; }
public bool IsAlive() { return Hp > 0; }}От ловкостта идват три числа за боя:
- точност = 75 + 2 × ловкост: колко добре улучваш;
- избягване = 2 × ловкост: колко трудно те улучват;
- шанс за критичен удар = 5 + ловкост / 10 (цяло деление): всеки удар е с поне 5% шанс.
Вълкът е с ловкост 8, героят с 4: точността на героя е 75 + 8 = 83, избягването на вълка е 16. В играта същите три формули стоят в Creature, но показателите идват от слоевете (база + екипировка + ефекти):
// Суровата стойност (може да е отрицателна): база + екипировка + ефекти.public int RawStat(string stat){ int value = Base[stat] + GearBonus(stat); foreach (Effect effect in Effects) value += effect.Modifier(stat); return value;}
public int Stat(string stat){ int value = RawStat(stat); return value < 0 ? 0 : value;}
public int MaxHp(){ int value = Stat("maxHp"); return value < 1 ? 1 : value;}
public int MaxMana(){ return Stat("maxMana");}
// Под 25% живот раната е тежка: ударите стават по-неточни.public bool IsHeavilyWounded(){ return Hp * 4 < MaxHp();}
public int Accuracy(){ return 75 + Stat("agility") * 2 + RawStat("accuracy") - (IsHeavilyWounded() ? 15 : 0);}
public int Evasion(){ return Stat("agility") * 2 + RawStat("evasion");}
public int CritChance(){ return 5 + Stat("agility") / 10;}Идеята
Енергия вместо кръгове
Вместо кръгове всеки боец има лента с енергия. Светът върти тикове: на всеки тик всеки добавя бързината си към лентата. Който стигне 100, може да действа; когато действа, плаща 100. По-бързият стига 100 по-често, затова действа по-често, и никой не чака „кръга“.
public Fighter NextActor(){ while (true) { Fighter? best = null; foreach (Fighter f in Participants()) { if (f.Energy >= 100 && (best == null || f.Energy > best.Energy)) best = f; } if (best != null) return best; foreach (Fighter f in Participants()) f.Energy += Math.Max(1, f.Speed); Ticks++; }}Ето правилата, наредени както ги чете кодът:
- Между живите бойци се търси този с най-много енергия от тези на 100 или повече. При равенство печели по-ранният в списъка, а героят е пръв. (Сравнението е
>, не>=: по-късният трябва да е строго по-пълен.) - Ако такъв няма, започва тик: всеки добавя
max(1, speed). Долната граница 1 пази цикъла: боец с бързина 0 или по-малка (ефект на забавяне) иначе никога не би стигнал 100 и цикълът щеше да е безкраен. - Победителят не се нулира, а плаща 100. Остатъкът пази предимството му.
Програмата подрежда героя (бързина 10), вълка (14) и гоблина (10) и печата първите седем хода:
var hero = new Fighter("Ива", 48, 7, 4, 3, 10, 4, 7, "slash", "", "");var wolf = new Fighter("Вълк кръвник", 20, 3, 8, 1, 14, 2, 5, "slash", "", "");var goblin = new Fighter("Гоблин копиеносец", 16, 2, 5, 1, 10, 2, 5, "pierce", "", "");var battle = new Battle(new List<Fighter> { hero, wolf, goblin });for (int i = 0; i < 7; i++){ Fighter actor = battle.NextActor(); Console.WriteLine("Тик " + battle.Ticks + ": " + actor.Name + " действа (енергия " + actor.Energy + ")."); actor.Energy -= 100;}Тик 8: Вълк кръвник действа (енергия 112).Тик 10: Ива действа (енергия 100).Тик 10: Гоблин копиеносец действа (енергия 100).Тик 15: Вълк кръвник действа (енергия 110).Тик 20: Ива действа (енергия 100).Тик 20: Гоблин копиеносец действа (енергия 100).Тик 22: Вълк кръвник действа (енергия 108).Вълкът е пръв на тик 8 (14 × 8 = 112). На тик 10 героят и гоблинът са на 100 едновременно: героят е по-ранен в списъка, а гоблинът действа веднага след него, без нов тик. Вълкът пак действа на тик 15, не на 16: от първия ход му остават 12.
Виж го
Опашката в движение
Трите ленти растат тик по тик. Следи чертата на 100 и номерата на плочките долу:
public Fighter NextActor(){ while (true) { Fighter? best = null; foreach (Fighter f in Participants()) { if (f.Energy >= 100 && (best == null || f.Energy > best.Energy)) best = f; } if (best != null) return best; foreach (Fighter f in Participants()) f.Energy += Math.Max(1, f.Speed); Ticks++; }}
// в цикъла на бояFighter actor = battle.NextActor();actor.Energy -= 100;Стъпка по стъпка, с думи
- Трима бойци: Ива, Вълк кръвник и Гоблин копиеносец. Всеки има лента с енергия, която започва от 0, и бързина (10, 14, 10). Който стигне 100, действа.
- Няма кой да действа още, затова светът прави тик: всеки добавя бързината си към лентата (поне 1). Лентите растат с различна скорост.
- Тик 8: Вълк кръвник е на 112. Той стига 100 пръв и действа. Плаща 100, но не нула: остават му 12.
- Тик 10: Ива и Гоблин копиеносец са на 100 едновременно. Равенство: действа по-ранният в списъка, а героят е пръв.
- Героят плати, а Гоблин копиеносец още е на 100. Няма нов тик: следващото извикване на nextActor го връща веднага.
- Тик 15: Вълк кръвник действа пак, вече на 110. Остатъкът от първия ход му спестява тик: без него щеше да чака до тик 16.
- След 22 тика: Вълк кръвник е действал 3 пъти, Ива и Гоблин копиеносец по 2. Бързината е скорост на лентата, не брой ходове: така по-бързият действа по-често и няма „кръгове“.
В играта същата опашка стои в началото на класа Battle, а ходът започва с плащането на 100:
public Creature NextActor(){ while (true) { Creature? best = null; foreach (Creature creature in Participants()) { if (creature.Energy >= 100 && (best == null || creature.Energy > best.Energy)) best = creature; } if (best != null) return best; foreach (Creature creature in Participants()) creature.Energy += Math.Max(1, creature.Stat("speed")); }}// Началото на хода: ефектите действат, презарежданията намаляват. Зашеметеният пропуска.public bool BeginTurn(Creature actor){ actor.Energy -= 100; bool stunned = actor.HasEffect("stun"); TickEffects(actor, this); actor.TickCooldowns(); HandleDeaths(); if (!actor.IsAlive()) return false; // След зашеметяване следващият ход е сигурен: иначе трима врагове с тояги заключват героя. actor.StunProof = stunned; if (stunned) { Say(T("combat.stunned", ("name", actor.Name))); return false; } return true;}Първият ред на beginTurn е actor.energy -= 100. После идват ефектите (кървене, отрова), презарежданията и пропускането на хода при зашеметяване: това са темите на следващите уроци (кръв, рани и ефекти). Тук важното е, че редът на ходовете е отделен от самия удар: nextActor казва кой действа, а какво прави, е друг въпрос.
По-точно, за любопитните
Тази опашка е опашка с приоритет: следващият е този с най-висок приоритет (най-много енергия). Тук се реализира с линейно обхождане на всички живи бойци при всяко извикване. В боя са най-много четирима, затова това са няколко сравнения. При стотици участници (битка на армии) се ползва двоична купчина (heap): следващият се взима за O(log n), не за O(n).
Тиковете също могат да се прескочат: вместо да се добавя по една бързина, се смята за всеки ⌈(100 − енергия) / бързина⌉ тика до готовността, взема се най-малкото и всички се напредват с него наведнъж. Резултатът е същият, но без цикъл. Канонът пази простия цикъл: той се чете с един поглед и стига за четирима.
Ограничението в играта е до 200 хода на героя на бой (heroTurns): ако нещо в правилата заседне, боят свършва като загуба, а не виси вечно.
Идеята
Улучи или пропусни
Ударът не винаги улучва. Шансът е точността на нападателя минус избягването на целта, затегнат между 10 и 95:
static int Clamp(int value, int low, int high){ if (value < low) return low; if (value > high) return high; return value;}
static int HitChance(int accuracy, int evasion){ return Clamp(accuracy - evasion, 10, 95);}Затягането (clamp) значи, че всеки удар има поне 10% шанс да улучи и поне 5% да пропусне: ловка цел не е недосегаема, а най-точният удар не е сигурен. После боят хвърля зар от 1 до 100 и пропуска, ако хвърленото е над шанса.
Програмата показва четири двойки (точност, избягване) и пет хвърляния със семе 2026 срещу шанса на героя към вълка, 83 − 16 = 67%:
(int, int)[] pairs = { (83, 16), (91, 8), (83, 80), (120, 0) };foreach (var (accuracy, evasion) in pairs){ Console.WriteLine($"{accuracy} - {evasion} = {accuracy - evasion}, шанс {HitChance(accuracy, evasion)}%");}
int chance = HitChance(83, 16);var dice = new Random(2026);for (int i = 0; i < 5; i++){ int roll = dice.Range(1, 100); Console.WriteLine("хвърляш " + roll + (roll > chance ? " — пропуск" : " — удар"));}83 - 16 = 67, шанс 67%91 - 8 = 83, шанс 83%83 - 80 = 3, шанс 10%120 - 0 = 120, шанс 95%хвърляш 76 — пропускхвърляш 59 — удархвърляш 23 — удархвърляш 37 — удархвърляш 58 — ударЗаровете са генераторът от канона (Random, mulberry32): със същото семе падат същите числа на четирите езика. Затова един бой със семе е повторяем и може да се запише, да се възпроизведе и да се провери с тест.
static int HitChance(int accuracy, int evasion)
{
return Clamp(accuracy - evasion, , 95);
}Долната граница е 10: дори срещу най-ловкия враг (83 − 80 = 3) ударът улучва в един от десет случая. Горната е 95.
Идеята
Щета и критичен удар
Улучил ли си, щетата е:
оръжие + сила − защита, поне 1.
Оръжието дава число от интервал (ръчната брадва на героя е 4–7), силата на нападателя се добавя, а защитата на целта се вади. Последното ограничение е важно: слаб удар срещу здрава броня не лекува целта и не е нула, а си остава 1.
Критичен удар има шанс 5% + ловкост / 10 (и още 10% за пробождащо оръжие) и удвоява оръжието и силата. Защитата се вади след удвояването, затова бронята отнема същото и от критичния удар:
static int DamageOf(int weapon, int strength, int defense, bool crit){ int damage = weapon + strength; if (crit) damage *= 2; damage -= defense; if (damage < 1) damage = 1; return damage;}
static void Show(string label, int weapon, int strength, int defense, bool crit){ string sum = weapon + " + " + strength; if (crit) sum = "(" + sum + ") * 2"; Console.WriteLine(label + ": " + sum + " - " + defense + " -> " + DamageOf(weapon, strength, defense, crit));}Show("обикновен удар", 6, 7, 1, false);Show("критичен удар", 6, 7, 1, true);Show("слаб удар срещу броня", 2, 1, 6, false);
foreach (int agility in new[] { 4, 8, 25, 40 }){ Console.WriteLine("критичен удар: ловкост " + agility + " -> " + (5 + agility / 10) + "%");}обикновен удар: 6 + 7 - 1 -> 12критичен удар: (6 + 7) * 2 - 1 -> 25слаб удар срещу броня: 2 + 1 - 6 -> 1критичен удар: ловкост 4 -> 5%критичен удар: ловкост 8 -> 5%критичен удар: ловкост 25 -> 7%критичен удар: ловкост 40 -> 9%Първият ред е ударът на героя срещу вълк (защита 1). Вторият е същият удар, но критичен: 25, повече от два пъти. Третият е слаб удар срещу броня 6: изваждането дава −3, а играта връща 1. Последните четири реда са шансът за критичен удар при различна ловкост: расте бавно, с 1% на десет точки.
Идеята
Видове щета
Ударът има вид: режещо, пробождащо, тъпо, огън, отрова. Видът казва две неща. Първо, как реагира целта: някои същества са устойчиви (щетата се дели на 2), други са слаби (умножава се по 3/2). Второ, какво следва след удара: режещото кърви, тъпото зашеметява, огънят гори, отровата отравя (ефектите са темата на следващия урок).
static int Resisted(Fighter target, string kind, int damage){ if (target.Resist == kind) return damage / 2; if (target.Weak == kind) return damage * 3 / 2; return damage;}Скелетът е устойчив на режещо и слаб на тъпо. Удар от 12 срещу него дава:
var skeleton = new Fighter("Скелет", 28, 4, 4, 2, 10, 3, 6, "slash", "slash", "blunt");string[] kinds = { "slash", "pierce", "blunt", "fire", "poison" };string[] names = { "режещо", "пробождащо", "тъпо", "огън", "отрова" };for (int i = 0; i < kinds.Length; i++){ Console.WriteLine(names[i] + ": 12 -> " + Resisted(skeleton, kinds[i], 12));}режещо: 12 -> 6пробождащо: 12 -> 12тъпо: 12 -> 18огън: 12 -> 12отрова: 12 -> 12Тъпото (боздуганът, тоягата) разбива кости, а мечът се плъзга по тях. Затова не всяко оръжие върши работа срещу всеки враг. В таблицата на враговете от данните на играта за това има две колони: resist и weak. След щетата играта проверява вида на удара и слага ефект:
public static void AfterHit(Battle battle, Creature target, string type, bool crit){ if (!target.IsAlive()) return; Random rng = battle.Game.Rng; if (type == "slash" && (crit || rng.Chance(35))) ApplyEffect(target, MakeEffect("bleed", 3, 1), battle); else if (type == "blunt" && rng.Chance(10)) ApplyEffect(target, MakeEffect("stun", 1, 0), battle); else if (type == "fire" && rng.Chance(30)) ApplyEffect(target, MakeEffect("burn", 3, 2), battle); else if (type == "poison") ApplyEffect(target, MakeEffect("poison", 3, 2), battle);}В канона resist и weak на врага са списъци от видове (скелетът е устойчив на режещо, големът на пробождащо и огън), а не по един вид. Тук е по един низ, за да е кратък кодът.
Идеята
Думата вместо числото
Дневникът на боя не пише „щета 12“. Пише „Ива смазва вълка (12)“. Думата за силата на удара зависи не от числото, а от дела от живота на целта, който отнема:
static int Percent(int a, int b){ return a * 100 / b;}
static string DamageWord(int damage, int maxHp){ int share = Percent(damage, maxHp); if (share < 10) return "драска"; if (share < 25) return "удря"; if (share < 45) return "разсича"; return "смазва";}foreach (int damage in new[] { 1, 2, 4, 5, 8, 9, 10, 12 }){ Console.WriteLine(damage + " от 20: " + Percent(damage, 20) + "%, " + DamageWord(damage, 20));}1 от 20: 5%, драска2 от 20: 10%, удря4 от 20: 20%, удря5 от 20: 25%, разсича8 от 20: 40%, разсича9 от 20: 45%, смазва10 от 20: 50%, смазва12 от 20: 60%, смазваПод 10% е драскотина, под 25% удар, под 45% разрез, а над това смазване. Така 12 щета са смазване за вълк с 20 живот, а за героя с 48 са само разрез. Играчът разбира, че ударът е тежък, без да сравнява числа. Ето функцията в играта (имената на думите са ключове към текстовете на четирите езика):
// Думата за силата на удара зависи от това каква част от живота на целта отнема.public static string DamageWord(int damage, int maxHp){ int share = Percent(damage, maxHp); if (share < 10) return T("verb.scratch"); if (share < 25) return T("verb.hit"); if (share < 45) return T("verb.cut"); return T("verb.crush");}Виж го
Целият бой
Сега всичко се събира. Ударът проверява точността, хвърля щета и критичен, вади защитата, прилага устойчивостта, не позволява под 1, казва го с дума и намалява живота:
static bool Strike(Battle battle, Random dice, Fighter attacker, Fighter target){ int hitChance = Clamp(attacker.Accuracy() - target.Evasion(), 10, 95); if (dice.Range(1, 100) > hitChance) { battle.Say(attacker.Name + " пропуска " + target.Name + "."); return false; } int damage = dice.Range(attacker.WeaponMin, attacker.WeaponMax) + attacker.Strength; int critChance = attacker.CritChance(); if (attacker.Kind == "pierce") critChance += 10; bool crit = dice.Chance(critChance); if (crit) damage *= 2; damage -= target.Defense; damage = Resisted(target, attacker.Kind, damage); if (damage < 1) damage = 1; string line = attacker.Name + " " + DamageWord(damage, target.MaxHp) + " " + target.Name + " (" + damage + ")."; if (crit) line += " Кръвта плисва!"; battle.Say(line); target.Hp = Math.Max(0, target.Hp - damage); return true;}Битката е цикъл: nextActor, плащане на 100, ударът на героя към първия жив враг или на врага към героя, дума за падането, край, когато единият отбор не е жив. Героят (Ива) срещу вълк и гоблин, със семе 16:
var dice = new Random(16);var hero = new Fighter("Ива", 48, 7, 4, 3, 10, 4, 7, "slash", "", "");var wolf = new Fighter("Вълк кръвник", 20, 3, 8, 1, 14, 2, 5, "slash", "", "");var goblin = new Fighter("Гоблин копиеносец", 16, 2, 5, 1, 10, 2, 5, "pierce", "", "");var battle = new Battle(new List<Fighter> { hero, wolf, goblin });battle.Say("Бой! Срещу теб: " + wolf.Name + ", " + goblin.Name + ".");while (true){ Fighter actor = battle.NextActor(); actor.Energy -= 100; Fighter target = actor == hero ? battle.Participants()[1] : hero; Strike(battle, dice, actor, target); if (!target.IsAlive()) battle.Say(target.Name + (target == hero ? " пада. Мината поглъща светлината." : " пада и не става.")); if (!hero.IsAlive() || battle.Participants().Count == 1) break;}battle.Say((hero.IsAlive() ? "Победа!" : "Загуба.") + " " + hero.Name + ": живот " + hero.Hp + "/" + hero.MaxHp);foreach (string line in battle.Log) Console.WriteLine(line);Бой! Срещу теб: Вълк кръвник, Гоблин копиеносец.Вълк кръвник драска Ива (4).Ива смазва Вълк кръвник (13).Гоблин копиеносец пропуска Ива.Вълк кръвник удря Ива (7). Кръвта плисва!Ива смазва Вълк кръвник (13).Вълк кръвник пада и не става.Гоблин копиеносец пропуска Ива.Ива смазва Гоблин копиеносец (10).Гоблин копиеносец драска Ива (2).Ива смазва Гоблин копиеносец (11).Гоблин копиеносец пада и не става.Победа! Ива: живот 35/48В дневника има 2 пропуска и 1 критичен удар: вълкът удря героя за 7, повече от обикновените 2–5, а в края се казва, че кръвта плисва. Дневникът е списък от редове. Екранът на боя рисува последните от тях:
public override void Draw(CellBuffer buffer){ Battle battle = Battle; Hero hero = battle.Hero; buffer.Text(0, 0, "=== " + T("battle.title") + " ===", "yellow"); buffer.Text(0, 1, hero.Name + ", " + Nm(hero.HeroClass()) + " " + T("hud.level", ("level", hero.Level)), "white"); buffer.Text(2, 2, T("hud.hp"), "red"); buffer.Bar(4, 2, 12, hero.Hp, hero.MaxHp(), "red"); buffer.Text(19, 2, hero.Hp + "/" + hero.MaxHp(), "default"); buffer.Text(27, 2, T("hud.mana"), "blue"); buffer.Bar(29, 2, 8, hero.Mana, hero.MaxMana(), "blue"); buffer.Text(40, 2, hero.Mana + "/" + hero.MaxMana(), "default"); // Редовете се редят плътно един след друг: героят, враговете, дневникът, командите. int row = 3; string heroEffects = EffectLabels(hero); if (heroEffects.Length > 0) { buffer.Text(2, row, heroEffects, "green"); row += 1; } List<Enemy> enemies = battle.AliveEnemies(); for (int i = 0; i < enemies.Count && i < 4; i++) { Enemy enemy = enemies[i]; buffer.Text(0, row, (i + 1) + ") ", "default"); buffer.Text(3, row, Pad(Cap(enemy.Name), 22), enemy.Kind.Color); buffer.Bar(25, row, 8, enemy.Hp, enemy.MaxHp(), "red"); buffer.Text(36, row, enemy.Hp + "/" + enemy.MaxHp(), "default"); buffer.Text(44, row, EffectLabels(enemy), "green"); row += 1; } int logRows = 7; int first = Math.Max(0, battle.Log.Count - logRows); for (int i = first; i < battle.Log.Count; i++) { buffer.Text(1, row, battle.Log[i], LogColor(battle.Log[i])); row += 1; } if (battle.Over) { buffer.Text(0, row, T(battle.Outcome == "win" ? "battle.won" : battle.Outcome == "fled" ? "battle.away" : "battle.lost"), "yellow"); } else { buffer.Text(0, row, Note.Length > 0 ? Note : T("battle.commands"), Note.Length > 0 ? "red" : "white"); }}И самият удар в играта е същият, с две разлики: ударът има сила (power), с която уменията удрят по-силно от обикновения удар, а след щетата идва afterHit, който слага ефекти.
public static bool Strike(Battle battle, Creature attacker, Creature target, DamageProfile profile, int power){ Random rng = battle.Game.Rng; int hitChance = Clamp(attacker.Accuracy() - target.Evasion(), 10, 95); if (rng.Range(1, 100) > hitChance) { battle.Say(T("combat.miss", ("attacker", attacker.Name), ("target", target.Name))); return false; } int damage = rng.Range(profile.Min, profile.Max) + attacker.Stat("strength"); damage = (damage * power) / 100; int critChance = attacker.CritChance(); if (profile.Type == "pierce") critChance += 10; bool crit = rng.Chance(critChance); if (crit) damage *= 2; damage -= target.Stat("defense"); damage = Resisted(target, profile.Type, damage); if (damage < 1) damage = 1; string line = T("combat.hit", ("attacker", attacker.Name), ("verb", DamageWord(damage, target.MaxHp())), ("target", target.Name), ("damage", damage)); if (crit) line += T(!target.IsHero() && ((Enemy)target).IsBloodless() ? "combat.critBone" : "combat.critBlood"); battle.Say(line); DealDamage(battle, target, damage); AfterHit(battle, target, profile.Type, crit); return true;}По-точно, за любопитните
Критичният удар е ×2, но боецът с копие (пробождащо) има още 10% шанс за него: в канона пробождащото е „оръжието на критичните удари“. Тук това е един ред: critChance += 10.
Заклинанията в играта винаги улучват и защитата не ги спира (spellHit): те се броят само с бонус на заклинателя и вид щета. Затова вещица с огнена топка и варварин с брадва са различен избор, а не по-слабата и по-силната версия на един и същ воин.
Пробвай
Предвиди изхода
Рицар и магьосник имат бързина 10, стрелецът 15. Списъкът е в този ред: рицар, магьосник, стрелец. Какво печата програмата?
var battle = new Battle(new List<Fighter>{ new Fighter("Рицар", 30, 5, 5, 1, 10, 3, 6, "slash", "", ""), new Fighter("Магьосник", 30, 5, 5, 1, 10, 3, 6, "slash", "", ""), new Fighter("Стрелец", 30, 5, 5, 1, 15, 3, 6, "slash", "", "")});string order = "";for (int i = 0; i < 8; i++){ Fighter actor = battle.NextActor(); actor.Energy -= 100; order += (i > 0 ? ", " : "") + actor.Name;}Console.WriteLine(order);Стрелецът (бързина 15) стига 100 пръв, на тик 7 (105), и действа. Рицарят и магьосникът (10) стигат 100 заедно на тик 10: при равенство действа по-ранният в списъка, затова рицарят е преди магьосника. После стрелецът на тик 14 (5 + 15 × 7 = 110), а на тик 20 и тримата са на 100 и действат по реда на списъка. Втората възможност е с обратна подредба при равенство (магьосникът преди рицаря: сравнение >= вместо >). Третата е по кръгове, както в началото на урока, без да се гледа бързината.
Пробвай
Намери грешката
Колега реши, че след хода енергията трябва да е нула, и пипна един ред. Ето какво печата програмата сега:
var hero = new Fighter("Ива", 48, 7, 4, 3, 10, 4, 7, "slash", "", "");var wolf = new Fighter("Вълк кръвник", 20, 3, 8, 1, 14, 2, 5, "slash", "", "");var goblin = new Fighter("Гоблин копиеносец", 16, 2, 5, 1, 10, 2, 5, "pierce", "", "");var battle = new Battle(new List<Fighter> { hero, wolf, goblin });for (int i = 0; i < 7; i++){ Fighter actor = battle.NextActor(); Console.WriteLine("Тик " + battle.Ticks + ": " + actor.Name + " действа (енергия " + actor.Energy + ")."); actor.Energy = 0;}Тик 8: Вълк кръвник действа (енергия 112).Тик 10: Ива действа (енергия 100).Тик 10: Гоблин копиеносец действа (енергия 100).Тик 16: Вълк кръвник действа (енергия 112).Тик 20: Ива действа (енергия 100).Тик 20: Гоблин копиеносец действа (енергия 100).Тик 24: Вълк кръвник действа (енергия 112).Вълкът действа на тик 8 и после на тик 16, не на 15, както в урока. Кой ред е виновен?
Избери реда с грешката.
actor.energy = 0 изхвърля всичко над 100. Вълкът е на 112 и остатъкът 12 се губи, затова всеки ход му струва цели 8 тика и бързината 14 е същата като 13. Правилното е actor.energy -= 100: плаща се точно 100, а останалото се пази.
По-точно
Какво не е в тази система
Опашката по енергия има малко правила, но зад нея има избори.
Всеки ход струва 100. Бързо действие (глътка от отвара) би могло да струва 50, а тежко (силно умение) 150. Тогава цената е свойство на действието и ходът плаща energy -= cost. Така балансът се настройва по-фино, но играчът трябва да вижда последствията, а за това му трябва опашката на екрана.
Опашката не се вижда. Екранът на боя в канона не показва кой е следващият. Показването следва от същото правило: симулираш nextActor върху копие на бойците и пишеш имената. Не го правим, защото тогава играчът планира по-дълбоко и боят става по-бавен за игра.
Затягането на шанса е решение за играта. С 10–95 всеки бой има случайност. По-тясно, например 30–70, е по-шумен бой, по-широко, например 1–99, е по-предвидим. Числата не са „правилни“: настройват се със симулация (последният урок на модула).
Критичният удар не бива да расте бързо. Двойната щета е силна, затова шансът е 5% + ловкост / 10, а не ловкост. При растеж с 1% на десет точки ловкостта не превръща боя в лотария.
Заровете са отделен вход. strike не търси случайно число сам, а получава генератора. Затова боят със семе е повторяем: с него се пише тест, който проверява цял бой байт по байт, и се прави запис на боя (урокът за действията).
Упражнение
Опашка за запис
Реши задачата в редактора си.
Опашка за запис. Програмата чете от входа на първия ред броя ходове N, а после по един боец на ред във вида име бързина. Бойците са в реда, в който са записани (това е редът при равенство). Програмата печата първите N хода като в урока: тик, име и енергията на бойеца, преди да плати 100.
С вход 10 | Голем 7 | Магьосник 10 | Стрелец 15 (по един на ред) изходът е:
Тик 7: Стрелец действа (енергия 105).Тик 10: Магьосник действа (енергия 100).Тик 14: Стрелец действа (енергия 110).Тик 15: Голем действа (енергия 105).Тик 20: Магьосник действа (енергия 100).Тик 20: Стрелец действа (енергия 100).Тик 27: Стрелец действа (енергия 105).Тик 29: Голем действа (енергия 103).Тик 30: Магьосник действа (енергия 100).Тик 34: Стрелец действа (енергия 110).Внимавай на тик 20: магьосникът и стрелецът са на 100 заедно и действа по-ранният в списъка. Внимавай и на тик 14: стрелецът действа пак с 110, защото остатъкът от първия му ход се пренася.
Още задачи
- Нулева бързина. Подай на боец бързина 0 и −3. Какво ще стане без
max(1, speed)? А със затягането? - Опашката на екрана. Напиши
preview(battle, count), която връща имената на следващитеcountучастници, без да променя бойците (работи върху копие). - Колко боя печели героят? Пусни боя от урока със семена от 1 до 1000 и преброй победите. Колко от тях имат критичен удар? Това е първата стъпка към симулацията от последния урок.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Конструктори и this · Методи на обекта · static: общото за всички · Списъци
- Задачи по темата: Хрониките на Арканум