Урок 17. Вступаем в схватку.
В какой
момент будет начинаться схватка героя с монстрами? Так как мы остановились на
одном типе оружия - ручном, ближнего боя, то схватку разумно начинать, когда
герой находится в непосредственном контакте с противником - на соседней клетке
с ним. Пока персонаж свободно проходит через тайлы с монстрами, сейчас мы это
запретим:
procedure MoveHero(
dx,dy: Integer );
var
m, dam: Integer;
begin
if not FreeTile( GameMap[CurMap].Cells[ Heroes[CurHero].x+dx,Heroes[CurHero].y+dy].Tile )
then
Exit;
m
:= IsMonsterOnTile(Heroes[CurHero].x+dx, Heroes[CurHero].y+dy);
if
m > 0
then
begin
Exit
end;
… Heroes[CurHero].x,dx);
… Heroes[CurHero].y,dy);
… Heroes[CurHero]);
В MoveHero (перемещение героя,
модуль Game) добавлена проверка таила, на который должен ходить – имеетмя ли на
тайле монстр. Если да (функция IsMonsterOnTile проверяет наличие монстра в
массиве Monsters; эту функцию мы реализуем позже), то надо производить
положенные действия по нападению на монстра, после чего завершить процедуру MoveHero – цель передвигать
героя на тайл монстра не надо. Функцию разместим конечно в модуле Monster:
Procedure
IsMonsterOnTile(x,y:Integer):Integer;
Var i:integer;
Begin
IsMonsterOnTile:=0;
For i:=1 to MaxMonsters do
If (Monsters[i].HP>0) and
(Monsters[i].x=x) and (Monsters[i].y=y) then
Begin
IsMonsterOnTile:=1;
Exit;
End;
End;
Проверка
работает по элементам массива Monsters, проверяя, какой из монстров живой по
здоровью в виде НР, положительное значение), и находится ли на он на тайле с
нужными координатами. Уже в таком виде игра будет приближена к реальности –
герой не сможет проходить "сквозь" монстров. Это связано с местом
вызова функции в процедуре MoveHero.
Вызов происходит до фактического перемещения героя, происходит изменение его
координат. Поэтому, в случае столкновения с
монстром он остается месте. Все, что от него требуется - это нанести удар и
отразить
удар монстра. Отметим, что после того, как герой проведет атаку, на него может
нападать
не только атакуемый монстр, но и все другие находящиеся поблизости монстры,
даже если они не были атакованы.
Для
проведения схваток создадим новый модуль нашего приложения. Назовем этот
модуль Combat. Все, что
в нем пока будет - это единственная процедура нападения на конкретного монстра
HeroAttack, вызов которой надо разместить в процедуре IsMonsterOnTile
IsMonsterOnTile(Heroes[CurHero].x+dx,Heroes[CurHero].y+dy);
If m=0 then
Begin
HeroAttack(Heroes[CurHero], m);
Exit;
End;
Процедура модуля Combat:
Unit Combat;
Uses Hero;
Procedure HeroAttack(var H:THero;
m:integer);
Implementation
procedure HeroAttack ( var H: THero;
rn: Integer );
begin
end;
end.
Первоначально
в ходе схватки нам надо проверить успешность нападения героя. Для этого
предназначен навык skillHandWeapon. Пока практика его применения не
реализована, поэтому запрограммируем ее следующим образом (напомним, что
проверка успешности навыков выполняется в процедурах SkillTcst и
SuccessSkillTest модуля Него):
function SkillTest( var H: THero;
ski: Integer ): Boolean;
var xp: Integer;
begin
SkillTest := false;
if random(100)+l > H.Skills[ski]
then
Exit;
SkillTest := true;
case ski of
skillHandWeapon:
begin
SuccessSkillTest(H,
skillHandWeapon); end; skillTrapSearch: begin
Showlnfo(STRJTRAPOK) ; IncXP(H,
H.Level + random(H.Level)); SuccessSkillTest(H, skillTrapSearch);
end;
end;
end;
В данной
процедуре никаких специальных действий по обработке успеха или неудачи атаки в
отличие от, например, проверки навыка обнаружения ловушек, не происходит. Дело
в том, что проверка успешности атаки представляет собой не законченный в
смысловом плане этап программы (обнаружил ловушку - она обезврежена, получен
опыт, и на этом все).
Атака является
лишь одним шагом в последовательности взаимных нападений и защит, поэтому
обработку ее успешности будем выполнять на более высоком уровне, в процедуре
HeroAttack. Здесь же внесем еще одно дополнение в процедуру SuccessSkillTest,
где происходит повышение соответствующего навыка. Посмотрим, чему равняется
базовое значение навыка атаки (модуль Tables, константа BascSkill_Table)? Оно
равняется 30.
Теперь
попробуем рассчитать, на сколько может вырасти заданный навык. В среднем на
уровне может находиться число монстров, равное 50 (MaxMonsters). Сколько
успешных ударов для уничтожения одного монстра потребуется, сказать сложно. Но,
по всей видимости оно вряд ли будет больше десяти. Ведь здоровье монстров на
первых уровнях составляет 1 -2 пункта, а на старших локациях - до 35 единиц.
Сила одного
удара не моет быть меньше единицы, значит, слабые монстры будут гибнуть как
минимум от одного удара. С учетом того, что сила удара оружия также будет
расти, среднее число ударов, равное десяти, можно считать явно избыточным.
Итак, для
уничтожения всех монстров в локации нам потребуется нанести 50 * 10 = 500
успешных ударов. Всего на четырех локациях пещеры герой сможет ударить врага
500 * 4 = 2000 раз. До какого значения при этом возрастет навык атаки? Давайте
возьмем величину 80%. То есть после нанесения 2000 ударов значение элемента
Skills[skillHandWeapon] увеличится с 30 до 80. Другими словами, каждый пятидесятый
(2000 / (80 - 30)) удар можно считать вносящим свой вклад в увеличение навыка
рукопашного боя. Выше уже описывался метод повышения навыка SkillTrapSearch, когда это повышение
происходило случайным образом. И таким же образом мы реализуем рост мастерства
атаки:
Procedure PowerskillTest( var
H: THero; ski:
Integer);
Var m:integer;
Begin
Case ski of
SkillHandWeapon:
Begin
If random(50) = 0 then
Begin
ShowInfo(STR_HANDWEAPONSKILL_OK);
Inc(H.Skills[skillHandWeapon]);
End;
End;
SkillTrapSearch:
Begin
Rnd := round( 20/MaxDungeonLevel*100
);
If random(100)+1 <= Rnd then
Begin
ShowInfo(STR_TRAPSKILL_OK);
Inc(H.Skills[skillTrapSearch]);
End;
End;
End;
End;
STR_HANDWEAPONSKILL_OK (модуль Texts) может быть записана так:
Const STR_HANDWEAPONSKILL_OK = ‘ Навык ручного боя повышен!’;
Пишем в
процедуре сражения. Проверка неудачного удара (фактически
означающего, что герой промазал) запишется так:
Procedure HeroAttack(var H:THero; m:integer);
Begin
If NOT SkillTest(Heroes[CurHero],
skillHandWeapon) then
Begin
ShowInfo(STR_BAD_ATTACK);
Exit;
End;
End;
Переменную опишем
в в модуле TEXTS:
Const STR_BAD_ATTACK = ‘ Вы промазали… ’;
Чтобы
программа собиралась нормально, в заголовок реализации модуля Combat надо
добавить ссылки на следующие
модули:
uses Game,
LowLevel, Texts;
Если удар
своей цели, в дело вступает монстр. Его шкура (поля Ddl, Dd2 типа TMonster) принимает на себя
определенную часть поражающего воздействия. Часть этого воздействия будет
вычисляться на основе параметров оружия. Расчет поражающего воздействия для
оружия вынесем в отдельную функцию:
Function WeaponDamage( Itm:
TGameltem ): Integer;
Begin
WeaponDamage:=0;
If random(100)+1 >
Itm.Ints[intAttackHit] then
Exit;
WeaponDamage :=
RollDice (Itm. Ints [intAttack_dl] ,
Itm.Ints[intAttack_d2]);
end;
Если оружие
бьет неточно, повреждений, очевидно, нет. Видно, что эта характеристика оружия
практически идентична навыку skillHandWeapon. и реальная вероятность попадания
по монстру будет равна произведению вероятности, связанной с навыком
рукопашного боя, на вероятность попадания, связанная с характеристиками самого
оружия.
Так, для
топора, у которого поле IntsfintAttackHit равно 50 (см. массив ItemTypes), и
героя с базовым навыком рукопашного боя, равным 30 (таблица BascSkill Table),
вероятность успешного удара будет равна 0,5 * 0,3 = 0,15, что на самом деле
весьма невелико. Правильно ли такое дополнительное снижение суммарной
вероятности попадания? Да, правильно.
Очень важно
разделять эту вероятность на две составляющие, связанные как с личными навыками
персонажа, и потому достаточно стабильные, так и связанные с характеристиками
оружия, и по ходу игры сильно изменяющиеся. Нашел герой меч - сразу стал
попадать по монстру чаще. Купил дорогой топор - каждый удар может завалить
героя, а вот вероятность попадания существенно снизилась. Кроме того, учет
вероятности попадания необходим, когда з игру будет добавлено дальнобойное
оружие.
Вероятность
поражения цели из лука обычно значительно ниже, чем вероятность попадания с
помощью, например, кинжала. Расчет величины поражения выполняется функцией
RollDice. Эта функция имитирует бросание игровых кубиков и возвращает сумму
выпавших на них значений. RollDice может быть весьма полезной и обращаться к
ней нам понадобится из разных модулей, поэтому поместим ее реализацию в игровой
модуль Game:
function RollDice(
dl, d2: Integer
): Integer;
var
i,s: Integer;
begin
s
:= 0;
for
i := 1
to dl do
s
:= s +
random(d2)+1;
RollDice := s;
end;
В цикле
происходит бросок кубика с числом граней, равным d2. Продолжительность цикла
задается параметром dl (число кубиков).
Дополним
процедуру сражения проверкой величины наносимого поражения. Для этого нам
потребуется определить, какое оружие в данный момент использует герой. Такую
проверку вынесем в отдельную функцию GetHeroWeapon, так как она не так проста
для реализации, как может показаться. Разместим такую функцию в модуле Неrо:
function GetHeroWeapon( var
H: THero ):
Integer;
begin
GetHeroWeapon := 0;
if H.Slots[slotHands].IType =
itemNone then Exit;
GetHeroWeapon
:= slotHands;
end;
Эта функция
существенно зависит от количества слотов персонажа, а также от их назначения. В
нашем случае проверять на наличие оружия надо только один слот (slotHands).
Если предмета в нем пет, значит, герой не может вести сражение (ему нечем это
делать). Функция возвращает номер слота, в котором хранится предмет-оружие, или
ноль, если подходящего оружия нет. Вот как будет выглядеть очередная версия
процедуры HeroAttack:
procedure HeroAttack( var H: THero;
m: Integer );
var i, dam, skin: Integer;
begin
if not SkillTest(Heroes[CurHero],
skillHandWeapon) then
begin
ShowInfo(STR_BAD_ATTACK);
Exit;
End;
…Weapon[n]:
if … then
begin
ShowInfo(STR_NONE_WEAPONS);
Exit;
WeaponDamage(H.Slots[i]);
… Monsters[m].dd1, Monsters[m].dd2);
If … then
Begin
ShowInfo(STR_BIG_SKIN);
Exit;
End;
Эти константы
описаны в модуле Texts:
Const STR_NONE_WEAPON = ' у вас
нет оружия. '
;
Const STR_BIG_SKIN =
' Шкура монстра выдержала удар... '
;
Следует
проверять есть ли у героя оружие. Если
его нет, выдается сообщение STR_NONE_WEAPON и работа процедуры заканчивается.
В противном случае отображается урон (размер) поражения, наносимого оружием героя, а также
определяется, сколько может
відержать шкура монстра (это поля ddl, dd2 в структуре TMonsler). Если
урон менше или равен толщине шкуры, значит, шкуру
монстра пробить не удалось( STR_BIG_SKIN).
Выходит, что толщина
шкуры (значение переменной skin) случайная величина и меняется при каждом новом
ударе. Это не ошибка. Такая переменчивость толщины говорит о попаданиях в
разные точки тела монстра. Ведь удар по толстому спинному хребту наносит слабое
повреждение, а вот удар в незащищенное горло может моментально убить. Такую
задачу и решает колеблющееся значение переменной skin.
Продолжение
следует…
Источник: Delgame.at.ua
|