-
Notifications
You must be signed in to change notification settings - Fork 15
Fenia: Skills and spells
Для всех заклинаний можно перекрыть их логику из фени, объявив один из методов: runVict
, runChar
, runArg
или runRoom
. Выбор метода происходит в коде команды cast
и зависит от настроек заклинания (его поля target
) и того, с каким параметром персонаж применил данное заклинание.
Например, для заклинания bless
в его профайле указано:
<target>char_room obj_inv obj_room</target>
Это означает, что благословить можно как персонажа, так и предмет в своем инвентаре или рядом в комнате. Если выполнена команда c bless barrel
, вызовется метод runObj
, а если выполнить c bless
или c bless grigoriy
, вызовется метод runVict
.
Для заклинаний, у которых есть несколько таких методов run, можно перекрыть как все, так и только некоторые -- в последнем случае будет просто вызван исходный метод из С++.
Для перекрытия логики заклинания достаточно в сценарии выполнить такое присваивание:
.Spell("bless").runObj = function() {
ch.act("Заклинание {g%s{x вызвано для {W%O2{x на уровне {C%d{x.", spell.rname, obj, level);
}
После этого исходный метод из С++ вызываться не будет, даже если внутри runObj
пустота, будьте осторожны.
Самый простой способ перекрыть заклинание из фени -- воспользоваться редактором умений skedit
и его командой fenia runObj
, fenia runVict
. При этом тема сценария будет автоматически установлена в spell/bless/runObj
, что позволит легко искать все такие методы командой cs list <фильтр>
. Сценарии заклинаний автоматически сохраняются на диск в каталоге fenia/spell/...
.
Для краткости записи в методы run* ничего не передается в параметры, вместо этого все необходимые поля и методы доступны изнутри run* (так называемый контекст вызова заклинания, который становится полем this
).
Полный список доступных полей и методов можно найти на сайте во вкладке Умения.
Вот несколько примеров:
-
vict
- цель заклинания для методаrunVict
, в других методах равнаnull
. Это поле можно переприсвоить, тогда поменяется и цель заклинания. Вспомогательные методы наподобиеdamage()
,savesSpell
всегда работают с текущимvict
. -
obj
- цель заклинания для методаrunObj
, в других методах равнаnull
. -
arg
- цель заклинания для методаrunArg
, в других методах равнаnull
. -
room
- цель заклинания для методаrunRoom
, в других методах равнаnull
. -
dam
- текущие повреждения. Устанавливаются как напрямую:dam = dam + 30;
так и через вызов методовcalcDamage()
. МетодcalcDamage
автоматически вызовется для каждого витка цикла у функцииdamageRoom
или в самом начале методаrunVict
.
.Spell("bluefire").runVict = function() {
if (!ch.is_npc() && !ch.neutral) {
vict = ch;
msgChar("Твой {CГолубой огонь{x оборачивается против тебя!");
}
if (.chance(50)) {
savesSpell(.tables.damage_table.fire);
if (vict != ch) {
msgNotVict("Голубой огонь %1$C2 {Rобжигает{x %2$C4!");
msgVict("Голубой огонь %1$C2 {Rобжигает{x тебя!");
msgChar("Твой Голубой огонь {Rобжигает{x %2$C4!");
} else {
msgChar("Твой Голубой огонь {Rобжигает{x тебя!");
msgRoom("%1$^C1 {Rобжигается{x своим голубым огнем.");
}
damage(.tables.damage_table.fire);
effectFire();
} else {
savesSpell(.tables.damage_table.cold);
if (vict != ch) {
msgNotVict("Голубой огонь %1$C2 {Cобмораживает{x %2$C4!");
msgVict("Голубой огонь %1$C2 {CCобмораживает{x тебя!");
msgChar("Твой Голубой огонь {Cобмораживает{x %2$C4!");
} else {
msgChar("Твой Голубой огонь {Cобмораживает{x тебя!");
msgRoom("%1$^C1 {Cобмораживается{x своим голубым огнем.");
}
damage(.tables.damage_table.cold);
effectCold();
}
}
.Spell("earthquake").runRoom = function() {
msgChar("Земля дрожит под твоими ногами!");
msgRoom("%1$^C1 вызывает ураган и землетрясение.");
msgArea("Земля слегка дрожит под твоими ногами.");
damageRoom(function() { // not ch, not safe, 50% chance for mirrors
if (vict.flying)
return;
if (vict.in_room.sector_type == .tables.sector_table.mountain)
dam = dam * 4;
else if (vict.in_room.sector_type == .tables.sector_table.city)
dam = dam * 3;
else if (vict.in_room.sector_type == .tables.sector_table.inside)
dam = dam * 2;
damage();
if (.chance(skill.effective(ch) / 2)) {
vict.wait = 2 * 12;
vict.position = .tables.position_table.rest;
}
});
// Destroy grave belonging to a vampire in owr PK range.
if (graveFind() != null && .chance(skill.effective(ch) / 2)) {
graveDestroy();
}
}
.Spell("scream").runRoom = function() {
if (ch.isAffected("scream")) {
ch.act("Ты сдавленно хрипишь.");
return;
}
msgChar("Ты пронзительно кричишь, сотрясая все вокруг!");
msgRoom("%1$^C1 пронзительно кричит, сотрясая все вокруг!");
msgArea("До тебя доносятся пронзительные вопли.");
dam = .dice(level, 20); // TODO calc damage from caster's hp
effectScream(); // called for the whole room
// Remember original level and damage.
state.level = level;
state.dam = dam;
damageRoom(function() { // called for all unsafe victims in the room
// Reset level and damage back to original for every victim.
level = state.level;
dam = state.dam;
if (!savesSpell())
vict.wait = 2 * 12; // 2 battle ticks
else {
dam = dam / 2; // divide by 2 again to get dam/4.
level = level / 2;
}
effectScream(); // called for the current victim
});
}
.Spell("hunger weapon").runObj = function() {
var chance;
if (obj.item_type != .tables.item_table.weapon) {
msgChar("%2$^O1 не оружие.");
return;
}
if (obj.value4 & .tables.weapon_type2.vampiric) {
msgChar("%2$^O1 уже жажд%2$nет|ят чужой крови.");
return;
}
if (obj.value4 & .tables.weapon_type2.holy
|| obj.extra_flags & .tables.extra_flags.anti_evil
|| obj.extra_flags & .tables.extra_flags.bless)
{
msgChar("Боги Света наказывают тебя за попытку осквернения священного оружия!");
msgRoom("Боги Света наказывают %1$C4 за попытку осквернения священного оружия!");
dam = .min(ch.hit - 1, 1000);
ch.rawdamage(ch, dam, "holy");
return;
}
chance = skill.effective(ch);
if (obj.value4 & .tables.weapon_type2.flaming)
chance = chance / 2;
if (obj.value4 & .tables.weapon_type2.frost)
chance = chance / 2;
if (obj.value4 & .tables.weapon_type2.sharp)
chance = chance / 2;
if (obj.value4 & .tables.weapon_type2.vorpal)
chance = chance / 2;
if (obj.value4 & .tables.weapon_type2.shocking)
chance = chance / 2;
if (obj.value4 & .tables.weapon_type2.fading)
chance = chance / 2;
if (.number_percent() >= chance) {
msgAll("%2$^O1 на миг ярко вспыхива%2$nет|ют багровым... но затем угаса%2$nет|ют.");
return;
}
var af;
af = .Affect();
af.type = skill.name;
af.level = level / 2;
af.duration = level / 4;
af.bitvector("weapon_type2", "vampiric");
obj.affectAdd(af);
af.bitvector("extra_flags", "anti_good anti_neutral");
obj.affectAdd(af);
msgChar("Ты передаешь %2$O3 свою жажду крови.");
msgRoom("%1$^C1 внимательно смотрит на %2$O4, %1$P2 глаза вспыхивают {rкрасным{x.");
}
.Spell("entangle").runObj = function() {
if (obj != graveFind()) {
// Either obj is not a grave at all, or vampire is safe from ch.
msgChar("%2$^O1 неподходящая цель.");
return;
}
// TODO: saves check or chance check
// Success:
vict = graveOwner(); // assign vict field so that damage() and msgVict() work
msgVict("Корни терновника проникают в могилу, тревожа твой покой.");
msgAll("Колючий терновник опутывает могилу, проникая корнями глубоко под землю!");
graveDestroy();
// Vampire is now here, do some damage.
dam = .number_range(level, level * 4);
damage("pierce");
}
.Spell("heat metal").runVict = function() {
dam = 0;
state.success = false; // Track successful heat attempts, false by default.
if (!savesSpell())
damageItems(function () {
if (.number_range(1, 2 * level) <= obj.level)
return;
if (!obj.madeOfMetal())
return;
if (obj.extra_flags & .tables.extra_flags.burn_proof)
return;
if (obj.item_type == .tables.item_table.weapon
&& obj.value4 & .tables.weapon_type2.flaming)
return;
if (savesSpell())
return;
state.success = true;
// Drop from inventory or burn.
if (obj.wear_loc == "none") {
dam = dam + .number_range(1, obj.level) / 2;
if (vict.can_drop_obj(obj)) {
// TODO: do we really need different msgs for weapon and non-weapon
if (obj.item_type == .tables.item_table.weapon) {
vict.act("%1$^O1 раскаля%1$nется|ются, и ты бросаешь %1$P2 на землю.", obj);
vict.recho("%1$^O1 раскаля%1$nется|ются, и %2$C1 бросает %1$P2 на землю.", obj, vict);
} else {
vict.recho("%1$^C1 кричит от боли и бросает %2$O4 на землю!", vict, obj);
vict.act("Ты кричишь от боли и бросаешь %O4 на землю!", obj);
}
obj.obj_to_room(vict.in_room);
return;
}
// Stuck in inventory.
if (obj.item_type == .tables.item_table.weapon)
vict.act("%1$^O1 обжига%1$nет|ют тебя!", obj);
else
vict.act("%1$^O1 обжига%1$nет|ют твою кожу!", obj);
return;
}
// TODO: calculate chance to drop an equipped item.
// if ((obj.weight / 10) > number_range(1, 2 * vict.cur_dex))
// TODO: item weight should affect dam.
// TODO: first should try to remove() and put in inventory, then try to drop.
// Worn or wielded item: remove & drop or burn.
if (vict.can_drop_obj(obj) && obj.remove()) {
dam = dam + .number_range(1, obj.level) / 2;
if (obj.item_type == .tables.item_table.weapon) {
vict.recho("%1$^O1 выпада%1$nет|ют из обожженных рук %2$C2.", obj, vict);
vict.act("Оружие выпадает из твоих обожженных рук!");
} else {
vict.recho("%1$^C1 кричит от боли и бросает %2$O4 на землю!", vict, obj);
vict.act("Ты кричишь от боли и бросаешь %O4 на землю!", obj);
}
obj.obj_to_room(vict.in_room);
return;
}
// Stuck on the body!
dam = dam + .number_range(1, obj.level);
if (obj.item_type == .tables.item_table.weapon)
vict.act("Раскаленное оружие обжигает твои руки!");
else
vict.act("%1$^O1 обжига%1$nет|ют твою кожу!", obj);
});
if (state.success) {
// Inflict accumulated damage. Set 'damtype fire' in skedit.
dam = dam * 2 / 3;
damage();
} else {
// Oops nothing happened.
msgChar("Твоя попытка нагревания закончилась неудачей.");
msgVict("Ты чувствуешь легкое прикосновение тепла.");
}
}
.Spell("deforestation").runRoom = function() {
var af;
if (sect != "forest") {
ch.act("Вокруг тебя нет леса, который можно уничтожить.");
return;
}
af = .Affect();
af.type = skill.name;
af.level = level;
af.duration = level/25 + 1;
ch.affectAdd(af);
af.location = .tables.apply_flags.sector_type;
af.modifier = .tables.sector_table.field;
room.affectJoin(af);
ch.in_room.echo("Деревья вокруг тебя исчезают, и местность превращается в поле.");
}
Многим умениям, таким как 'подножка', 'бросок грязью', 'воровство', соответствуют команды (с таким же или чуть измененным названием, например, 'грязь', 'украсть'). Конфигурация таких команд задается внутри профайлов их умений, в секции <command>
, например:
<command type="SKILL(dirt)">
<cat>class</cat>
<hint>(воинские профессии) ослепить врага грязью</hint>
<name>dirt</name>
<order>fight_only</order>
<position>fight</position>
<russian>
<node>грязь</node>
</russian>
</command>
Команды умений обладают почти теми же полями и свойствами, что и обычные команды. Большинство логики для них на данный момент задано в коде, однако уже есть возможность переопределить их метод run
из фени, и тогда будет вызван именно он. Это удобно делать изнутри редактора skedit
, пользуясь подкомандой fenia run
.
Важным полем для команды умения является argtype
, равное одному из значений таблицы argtype_table
. Установив это поле в obj_here
или char_room
, можно добиться того, что внутрь метода run
попадет уже заранее найденный предмет obj
или персонаж vict
, а если они не найдутся -- игроку выдастся сообщение об ошибке. Если argtype
не задан, аргументы придется анализировать самостоятельно, используя поля argAll
, argOne
и argTwo
.
Для краткости записи в метод run
ничего не передается в параметры, вместо этого все необходимые поля и методы сразу доступны изнутри run
. Полный список доступных полей и методов можно найти на сайте во вкладке Умения. Вот несколько примеров:
-
argAll
- вся строка аргументов, с которыми была вызвана эта команда -
argOne
- первый аргумент, с которым была вызвана команда -
argTwo
- второй аргумент, с которым была вызвана команда -
vict
- персонаж, цель данной команды. Будет определен для команд, у которыхargtype
равенchar_fight
,char_room
и так далее -
obj
- предмет, цель данной команды. Будет определен для команд, у которыхargtype
равенobj_here
,obj_carry
и так далее
Команда без аргументов, вешает аффект на комнату.
.SkillCommand("settraps").run = function() {
if (!skill.usable(ch)) {
ch.act("Ты абсолютно не в курсе, как это делается.");
return;
}
if (ch.in_room.room_flags & .tables.room_flags.law) {
ch.act("Мистическая сила защищает комнату.");
return;
}
if (ch.in_room.isAffected(skill.name)) {
ch.act("В этой комнате уже есть ловушка.");
return;
}
if (ch.isAffected(skill.name)) {
ch.act("Ты не сможешь уследить за двумя ловушками.");
return;
}
ch.wait = skill.beats(ch);
if (.chance(7 * skill.effective(ch) / 10)) {
var af;
af = .Affect();
af.type = skill.name;
af.level = skill.level(ch);
af.duration = af.level / 40;
af.where = .tables.affwhere_flags.room_affects;
af.bitvector = .tables.raffect_flags.thief_trap;
ch.in_room.affectJoin(af);
cooldown(af.duration);
ch.act("Ты устраиваешь ловушку в комнате.");
ch.recho("%^C1 устраивает ловушку в комнате.", ch);
skill.improve(ch, true);
} else {
ch.act("Твоя попытка устроить ловушку провалилась.");
skill.improve(ch, false);
}
}
С помощью Фени можно дать персонажу какое-то временное умение, ранее не доступное для его профессии, как будто оно ему приснилось.
Если это умение уже было доступно (как профессиональное или уже приснилось), метод ничего не сделает и вернет false.
Пример присваивания временного умения в триггере onEquip
:
if (.Skill("dodge").giveTemporary(ch, 75)) {
// Успешно повесилось разученным на 75, пока не снимет вещь.
ch.act("Ты чувствуешь себя ловким и проворным, как лиса.");
}
Или так:
.Skill("dodge").giveTemporary(ch, 100, 3); // повесить разученным на 100, на 3 мировых дня.
Снять такое умение можно в триггере onRemove
. Никаких проверок не нужно, лишнего не снимет.
.Skill("dodge").removeTemporary(ch);
Cправка по всем методам скилла можно вывести так:
eval ptc(.Skill("none").api())
Для этого служит конструктор .FeniaSkill("название")
. Созданное умение существует, пока на него кто-то ссылается (т.е. если просто внутри функции объявить его во временной переменной, то умение исчезнет как только закончится выполняться функция). Поэтому удобно присвоить новое умение куда-то в .tmp, где его никто не разрушит.
Для удобства введем .tmp.skills
как хранилище всех феневых умений, но это не обязательно, это может быть любое поле.
Зарегистрировать новое умение и прописать ему имена:
if (.tmp.skills == null)
.tmp.skills = .Map();
.tmp.skills.cowardice = .FeniaSkill("cowardice");
.tmp.skills.cowardice.nameRus = "трусость";
.tmp.skills.cowardice.dammsg = "трусость";
.tmp.skills.cowardice.dammsg_gender = "f";
Повесить на себя вечный аффект трусости с правильным русским именем:
eval affectAdd(.Affect("cowardice", 100, -1))
Нанести себе 10 очков повреждений, 'Твоя трусость царапает тебя.':
eval damage(this, 10, "cowardice", "none")
Посмотреть все поля:
eval ptc(.tmp.skills.cowardice.api())
Пока это все скилы-пустышки, которые годятся для повесить аффект или нанести повреждения. По мере надобности добавим возможность cоздавать заклинания или боевые умения-команды.
На персонажа можно повесить аффект, улучшающий/ухудшающий знание умения или целой группы умений.
Пример аффекта, влияющего на знания нескольких умений:
var af;
af = .Affect();
af.type = "clumsiness"; // (создать новое умение как описано выше)
af.location = .tables.apply_flags.learned; // на что оказываем влияние (разученность)
af.modifier = -50; // нету у тебя больше парирования
af.where = .tables.affwhere_flags.skills; // что именно меняем - умения или группу умений
af.global = "dodge parry 'enhanced damage'"; // действует на несколько умений сразу
af.duration = 10; // на 10 минут (тиков)
af.level = ch.level;
ch.affectAdd(af);
Пример аффекта, влияющего на знание группы умений:
af.where = .tables.affwhere_flags.skill_groups;
af.modifier = 100;
af.global = "benedictions"; // благословляй не хочу
На персонажа можно повесить аффект, влияющий на уровень произносимых заклинаний. Влиять можно на все заклинания, заклинания одной группы или индивидуальные.
Пример аффекта, влияющего на уровень всех заклинаний:
var af;
af = .Affect();
af.type = "abracadabra";
af.location = .tables.apply_flags.level // на что оказываем влияние (уровень)
af.modifier = 3; // увеличивать уровень на 3
af.duration = 10; // на 10 минут (тиков)
af.level = ch.level;
ch.addAffect(af);
Пример аффекта, влияющего на уровень лечебных заклинаний:
var af;
af = .Affect();
af.type = "abracadabra";
af.location = .tables.apply_flags.level // на что оказываем влияние (уровень)
af.modifier = 1; // увеличивать уровень на 1
af.where = .tables.affwhere_flags.skill_groups;
af.global = "healing";
af.duration = 10;
af.level = ch.level;
ch.addAffect(af);