Към съдържанието
wannacode

Бойната система

Накратко: Боят се върти на тикове: всеки добавя бързината си към лента с енергия, който стигне 100, действа и плаща 100. Ударът е точност срещу избягване, щета = оръжие + сила − защита (поне 1), критичен удар удвоява, а видът щета решава какво следва.

Проблем

Вълкът е по-бърз от голема

В мината героят среща вълк кръвник и гоблин едновременно. Кой удря пръв и колко пъти? Първият отговор е по кръгове: героят, после всеки враг по реда им, и отначало. Но вълкът е два пъти по-бърз от рудния голем (бързина 14 срещу 7). Ако и двамата удрят веднъж на кръг, бързината не значи нищо, а играч, който е вложил точки в нея, не получава нищо.

Затова редът е друг, а ударът има свои правила. Ето екран на боя от играта (от минаването, с което се проверява играта): героят е срещу гигантски плъх, дневникът пази последните редове, а ударът на героя е пропуснат.

=== Бой ===
Ива, варварин ур.1
Ж [###########-] 46/48 М [########] 4/4
1) Гигантски плъх [########] 9/9
Бой! Срещу теб: гигантски плъх.
Гигантски плъх драска Ива (1).
Ива пропуска гигантски плъх.
Гигантски плъх драска Ива (1).
1 удар 2 умение 3 предмет 4 бягство

За този кадър трябват пет неща: ред на ходовете, точност срещу избягване, щета, видове щета и думи в дневника. Урокът ги строи едно по едно с малки програми, а накрая ги събира в бой на героя с вълк и гоблин и показва същото в играта. Бойците тук са със готови показатели: как се събират от база, екипировка и ефекти, е в урока за същества.

Идеята

Бойци с готови показатели

Боецът е клас с показатели, живот и една лента с енергия:

class Fighter {
constructor(name, maxHp, strength, agility, defense, speed,
weaponMin, weaponMax, kind, resist, weak) {
this.name = name;
this.maxHp = maxHp;
this.strength = strength;
this.agility = agility;
this.defense = defense;
this.speed = speed;
this.weaponMin = weaponMin;
this.weaponMax = weaponMax;
this.kind = kind;
this.resist = resist;
this.weak = weak;
this.hp = maxHp;
this.energy = 0;
}
accuracy() {
return 75 + this.agility * 2;
}
evasion() {
return this.agility * 2;
}
critChance() {
return 5 + Math.floor(this.agility / 10);
}
isAlive() {
return this.hp > 0;
}
}

От ловкостта идват три числа за боя:

  • точност = 75 + 2 × ловкост: колко добре улучваш;
  • избягване = 2 × ловкост: колко трудно те улучват;
  • шанс за критичен удар = 5 + ловкост / 10 (цяло деление): всеки удар е с поне 5% шанс.

Вълкът е с ловкост 8, героят с 4: точността на героя е 75 + 8 = 83, избягването на вълка е 16. В играта същите три формули стоят в Creature, но показателите идват от слоевете (база + екипировка + ефекти):

// Суровата стойност (може да е отрицателна): база + екипировка + ефекти.
rawStat(stat) {
let value = this.base.get(stat) + this.gearBonus(stat);
for (const effect of this.effects) value += effect.modifier(stat);
return value;
}
stat(stat) {
const value = this.rawStat(stat);
return value < 0 ? 0 : value;
}
maxHp() {
const value = this.stat('maxHp');
return value < 1 ? 1 : value;
}
maxMana() {
return this.stat('maxMana');
}
// Под 25% живот раната е тежка: ударите стават по-неточни.
isHeavilyWounded() {
return this.hp * 4 < this.maxHp();
}
accuracy() {
return 75 + this.stat('agility') * 2 + this.rawStat('accuracy') - (this.isHeavilyWounded() ? 15 : 0);
}
evasion() {
return this.stat('agility') * 2 + this.rawStat('evasion');
}
critChance() {
return 5 + Math.floor(this.stat('agility') / 10);
}
Math.floor(this.agility / 10) е цялото деление: в JavaScript / е винаги дробно.

Идеята

Енергия вместо кръгове

Вместо кръгове всеки боец има лента с енергия. Светът върти тикове: на всеки тик всеки добавя бързината си към лентата. Който стигне 100, може да действа; когато действа, плаща 100. По-бързият стига 100 по-често, затова действа по-често, и никой не чака „кръга“.

nextActor() {
while (true) {
let best = null;
for (const f of this.participants()) {
if (f.energy >= 100 && (best === null || f.energy > best.energy)) best = f;
}
if (best !== null) return best;
for (const f of this.participants()) f.energy += Math.max(1, f.speed);
this.ticks++;
}
}

Ето правилата, наредени както ги чете кодът:

  1. Между живите бойци се търси този с най-много енергия от тези на 100 или повече. При равенство печели по-ранният в списъка, а героят е пръв. (Сравнението е >, не >=: по-късният трябва да е строго по-пълен.)
  2. Ако такъв няма, започва тик: всеки добавя max(1, speed). Долната граница 1 пази цикъла: боец с бързина 0 или по-малка (ефект на забавяне) иначе никога не би стигнал 100 и цикълът щеше да е безкраен.
  3. Победителят не се нулира, а плаща 100. Остатъкът пази предимството му.

Програмата подрежда героя (бързина 10), вълка (14) и гоблина (10) и печата първите седем хода:

const hero = new Fighter("Ива", 48, 7, 4, 3, 10, 4, 7, "slash", "", "");
const wolf = new Fighter("Вълк кръвник", 20, 3, 8, 1, 14, 2, 5, "slash", "", "");
const goblin = new Fighter("Гоблин копиеносец", 16, 2, 5, 1, 10, 2, 5, "pierce", "", "");
const battle = new Battle([hero, wolf, goblin]);
for (let i = 0; i < 7; i++) {
const actor = battle.nextActor();
console.log("Тик " + 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 и номерата на плочките долу:

nextActor() {
while (true) {
let best = null;
for (const f of this.participants()) {
if (f.energy >= 100 && (best === null || f.energy > best.energy)) best = f;
}
if (best !== null) return best;
for (const f of this.participants()) f.energy += Math.max(1, f.speed);
this.ticks++;
}
}
// в цикъла на боя
const actor = battle.nextActor();
actor.energy -= 100;
Стъпка по стъпка, с думи
  1. Трима бойци: Ива, Вълк кръвник и Гоблин копиеносец. Всеки има лента с енергия, която започва от 0, и бързина (10, 14, 10). Който стигне 100, действа.
  2. Няма кой да действа още, затова светът прави тик: всеки добавя бързината си към лентата (поне 1). Лентите растат с различна скорост.
  3. Тик 8: Вълк кръвник е на 112. Той стига 100 пръв и действа. Плаща 100, но не нула: остават му 12.
  4. Тик 10: Ива и Гоблин копиеносец са на 100 едновременно. Равенство: действа по-ранният в списъка, а героят е пръв.
  5. Героят плати, а Гоблин копиеносец още е на 100. Няма нов тик: следващото извикване на nextActor го връща веднага.
  6. Тик 15: Вълк кръвник действа пак, вече на 110. Остатъкът от първия ход му спестява тик: без него щеше да чака до тик 16.
  7. След 22 тика: Вълк кръвник е действал 3 пъти, Ива и Гоблин копиеносец по 2. Бързината е скорост на лентата, не брой ходове: така по-бързият действа по-често и няма „кръгове“.

В играта същата опашка стои в началото на класа Battle, а ходът започва с плащането на 100:

nextActor() {
while (true) {
let best = null;
for (const creature of this.participants()) {
if (creature.energy >= 100 && (best === null || creature.energy > best.energy)) best = creature;
}
if (best !== null) return best;
for (const creature of this.participants()) creature.energy += Math.max(1, creature.stat('speed'));
}
}
// Началото на хода: ефектите действат, презарежданията намаляват. Зашеметеният пропуска.
beginTurn(actor) {
actor.energy -= 100;
const stunned = actor.hasEffect('stun');
tickEffects(actor, this);
actor.tickCooldowns();
this.handleDeaths();
if (!actor.isAlive()) return false;
// След зашеметяване следващият ход е сигурен: иначе трима врагове с тояги заключват героя.
actor.stunProof = stunned;
if (stunned) {
this.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:

function clamp(value, low, high) {
if (value < low) return low;
if (value > high) return high;
return value;
}
function hitChance(accuracy, evasion) {
return clamp(accuracy - evasion, 10, 95);
}

Затягането (clamp) значи, че всеки удар има поне 10% шанс да улучи и поне 5% да пропусне: ловка цел не е недосегаема, а най-точният удар не е сигурен. После боят хвърля зар от 1 до 100 и пропуска, ако хвърленото е над шанса.

Програмата показва четири двойки (точност, избягване) и пет хвърляния със семе 2026 срещу шанса на героя към вълка, 83 − 16 = 67%:

const pairs = [[83, 16], [91, 8], [83, 80], [120, 0]];
for (const [accuracy, evasion] of pairs) {
console.log(accuracy + " - " + evasion + " = " + (accuracy - evasion) + ", шанс " + hitChance(accuracy, evasion) + "%");
}
const chance = hitChance(83, 16);
const dice = new Random(2026);
for (let i = 0; i < 5; i++) {
const roll = dice.range(1, 100);
console.log("хвърляш " + 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): със същото семе падат същите числа на четирите езика. Затова един бой със семе е повторяем и може да се запише, да се възпроизведе и да се провери с тест.

function hitChance(accuracy, evasion) {
  return clamp(accuracy - evasion, , 95);
}

Идеята

Щета и критичен удар

Улучил ли си, щетата е:

оръжие + сила − защита, поне 1.

Оръжието дава число от интервал (ръчната брадва на героя е 4–7), силата на нападателя се добавя, а защитата на целта се вади. Последното ограничение е важно: слаб удар срещу здрава броня не лекува целта и не е нула, а си остава 1.

Критичен удар има шанс 5% + ловкост / 10 (и още 10% за пробождащо оръжие) и удвоява оръжието и силата. Защитата се вади след удвояването, затова бронята отнема същото и от критичния удар:

function damageOf(weapon, strength, defense, crit) {
let damage = weapon + strength;
if (crit) damage *= 2;
damage -= defense;
if (damage < 1) damage = 1;
return damage;
}
function show(label, weapon, strength, defense, crit) {
let sum = weapon + " + " + strength;
if (crit) sum = "(" + sum + ") * 2";
console.log(label + ": " + sum + " - " + defense + " -> " + damageOf(weapon, strength, defense, crit));
}
show("обикновен удар", 6, 7, 1, false);
show("критичен удар", 6, 7, 1, true);
show("слаб удар срещу броня", 2, 1, 6, false);
for (const agility of [4, 8, 25, 40]) {
console.log("критичен удар: ловкост " + agility + " -> " + (5 + Math.floor(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). Второ, какво следва след удара: режещото кърви, тъпото зашеметява, огънят гори, отровата отравя (ефектите са темата на следващия урок).

function resisted(target, kind, damage) {
if (target.resist === kind) return Math.floor(damage / 2);
if (target.weak === kind) return Math.floor((damage * 3) / 2);
return damage;
}

Скелетът е устойчив на режещо и слаб на тъпо. Удар от 12 срещу него дава:

const skeleton = new Fighter("Скелет", 28, 4, 4, 2, 10, 3, 6, "slash", "slash", "blunt");
const kinds = ["slash", "pierce", "blunt", "fire", "poison"];
const names = ["режещо", "пробождащо", "тъпо", "огън", "отрова"];
for (let i = 0; i < kinds.length; i++) {
console.log(names[i] + ": 12 -> " + resisted(skeleton, kinds[i], 12));
}
Изход
режещо: 12 -> 6
пробождащо: 12 -> 12
тъпо: 12 -> 18
огън: 12 -> 12
отрова: 12 -> 12

Тъпото (боздуганът, тоягата) разбива кости, а мечът се плъзга по тях. Затова не всяко оръжие върши работа срещу всеки враг. В таблицата на враговете от данните на играта за това има две колони: resist и weak. След щетата играта проверява вида на удара и слага ефект:

function afterHit(battle, target, type, crit) {
if (!target.isAlive()) return;
const 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)“. Думата за силата на удара зависи не от числото, а от дела от живота на целта, който отнема:

function percent(a, b) {
return Math.floor((a * 100) / b);
}
function damageWord(damage, maxHp) {
const share = percent(damage, maxHp);
if (share < 10) return "драска";
if (share < 25) return "удря";
if (share < 45) return "разсича";
return "смазва";
}
for (const damage of [1, 2, 4, 5, 8, 9, 10, 12]) {
console.log(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 са само разрез. Играчът разбира, че ударът е тежък, без да сравнява числа. Ето функцията в играта (имената на думите са ключове към текстовете на четирите езика):

// Думата за силата на удара зависи от това каква част от живота на целта отнема.
function damageWord(damage, maxHp) {
const 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, казва го с дума и намалява живота:

function strike(battle, dice, attacker, target) {
const hitChance = clamp(attacker.accuracy() - target.evasion(), 10, 95);
if (dice.range(1, 100) > hitChance) {
battle.say(attacker.name + " пропуска " + target.name + ".");
return false;
}
let damage = dice.range(attacker.weaponMin, attacker.weaponMax) + attacker.strength;
let critChance = attacker.critChance();
if (attacker.kind === "pierce") critChance += 10;
const crit = dice.chance(critChance);
if (crit) damage *= 2;
damage -= target.defense;
damage = resisted(target, attacker.kind, damage);
if (damage < 1) damage = 1;
let 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:

const dice = new Random(16);
const hero = new Fighter("Ива", 48, 7, 4, 3, 10, 4, 7, "slash", "", "");
const wolf = new Fighter("Вълк кръвник", 20, 3, 8, 1, 14, 2, 5, "slash", "", "");
const goblin = new Fighter("Гоблин копиеносец", 16, 2, 5, 1, 10, 2, 5, "pierce", "", "");
const battle = new Battle([hero, wolf, goblin]);
battle.say("Бой! Срещу теб: " + wolf.name + ", " + goblin.name + ".");
while (true) {
const actor = battle.nextActor();
actor.energy -= 100;
const 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().length === 1) break;
}
battle.say((hero.isAlive() ? "Победа!" : "Загуба.") + " " + hero.name + ": живот " + hero.hp + "/" + hero.maxHp);
for (const line of battle.log) console.log(line);
Изход
Бой! Срещу теб: Вълк кръвник, Гоблин копиеносец.
Вълк кръвник драска Ива (4).
Ива смазва Вълк кръвник (13).
Гоблин копиеносец пропуска Ива.
Вълк кръвник удря Ива (7). Кръвта плисва!
Ива смазва Вълк кръвник (13).
Вълк кръвник пада и не става.
Гоблин копиеносец пропуска Ива.
Ива смазва Гоблин копиеносец (10).
Гоблин копиеносец драска Ива (2).
Ива смазва Гоблин копиеносец (11).
Гоблин копиеносец пада и не става.
Победа! Ива: живот 35/48

В дневника има 2 пропуска и 1 критичен удар: вълкът удря героя за 7, повече от обикновените 2–5, а в края се казва, че кръвта плисва. Дневникът е списък от редове. Екранът на боя рисува последните от тях:

draw(buffer) {
const battle = this.battle;
const 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');
// Редовете се редят плътно един след друг: героят, враговете, дневникът, командите.
let row = 3;
const heroEffects = effectLabels(hero);
if (heroEffects.length > 0) {
buffer.text(2, row, heroEffects, 'green');
row += 1;
}
const enemies = battle.aliveEnemies();
for (let i = 0; i < enemies.length && i < 4; i++) {
const 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;
}
const logRows = 7;
const first = Math.max(0, battle.log.length - logRows);
for (let i = first; i < battle.log.length; 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, this.note.length > 0 ? this.note : t('battle.commands'), this.note.length > 0 ? 'red' : 'white');
}
}

И самият удар в играта е същият, с две разлики: ударът има сила (power), с която уменията удрят по-силно от обикновения удар, а след щетата идва afterHit, който слага ефекти.

function strike(battle, attacker, target, profile, power) {
const rng = battle.game.rng;
const 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;
}
let damage = rng.range(profile.min, profile.max) + attacker.stat('strength');
damage = Math.floor((damage * power) / 100);
let critChance = attacker.critChance();
if (profile.type === 'pierce') critChance += 10;
const crit = rng.chance(critChance);
if (crit) damage *= 2;
damage -= target.stat('defense');
damage = resisted(target, profile.type, damage);
if (damage < 1) damage = 1;
let line = t('combat.hit', {
attacker: attacker.name, verb: damageWord(damage, target.maxHp()), target: target.name, damage: damage,
});
if (crit) line += t(!target.isHero() && 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. Списъкът е в този ред: рицар, магьосник, стрелец. Какво печата програмата?

const battle = new Battle([
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", "", ""),
]);
const order = [];
for (let i = 0; i < 8; i++) {
const actor = battle.nextActor();
actor.energy -= 100;
order.push(actor.name);
}
console.log(order.join(", "));
Отговор

Пробвай

Намери грешката

Колега реши, че след хода енергията трябва да е нула, и пипна един ред. Ето какво печата програмата сега:

const hero = new Fighter("Ива", 48, 7, 4, 3, 10, 4, 7, "slash", "", "");
const wolf = new Fighter("Вълк кръвник", 20, 3, 8, 1, 14, 2, 5, "slash", "", "");
const goblin = new Fighter("Гоблин копиеносец", 16, 2, 5, 1, 10, 2, 5, "pierce", "", "");
const battle = new Battle([hero, wolf, goblin]);
for (let i = 0; i < 7; i++) {
const actor = battle.nextActor();
console.log("Тик " + 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, както в урока. Кой ред е виновен?

Избери реда с грешката.

По-точно

Какво не е в тази система

Опашката по енергия има малко правила, но зад нея има избори.

Всеки ход струва 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 и преброй победите. Колко от тях имат критичен удар? Това е първата стъпка към симулацията от последния урок.

Тест

Провери се

1. След хода си бойците плащат 100 енергия, а не я нулират. Защо?
2. Точността е 83, а избягването на целта е 80. Какъв е шансът за попадение?
3. Оръжие 6, сила 7, защита на целта 1, критичен удар. Колко е щетата?
4. Скелетът е устойчив на режещо и слаб на тъпо. Колко щета нанася удар от 12 тъпо?
5. От какво зависи думата в дневника („драска“, „удря“, „разсича“, „смазва“)?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·