-
Notifications
You must be signed in to change notification settings - Fork 15
Fenia: Examples
В доме живет кот, и нам хочется, чтобы кот периодически мяукал и драл мебель.
В списке триггеров для мобов мы видим два триггера, onSpec
и onArea
, которые выполняются с какой-то периодичносью. Для нашей задачи больше подойдет onSpec
, т.к. в этом случае в действиях кота будет какой-то элемент неожиданности. onArea
, вызываемый раз в минуту, будет совпадать с другими действиями - сменой дня и ночи, спаданием аффектов, и смотреться не так красиво.
Триггер onSpec
мы повесим на прототип котенка с внумом 3090 - он есть и в локальной версии.
Все экземпляры котенка, созданные, например, с помощью команды load mob 3090
, также будут иметь этот триггер. Все триггеры, висящие на прототипах, всегда первым параметром имеют конкретный экземпляр моба. В нашем примере этот параметр называется mob
и содержит, соответственно, котенка, от которого сейчас вызвался тригер onSpec
.
С помощью встроенного редактора мобов medit
можно присвоить отдельный триггер для прототипа моба.
medit 3090
fenia onSpec
По этой команде откроется веб-редактор сценариев с уже готовым шаблоном для onSpec. Останется только дописать туда наш код и нажать на кнопку Run:
.get_mob_index(3090).onSpec = function(mob) {
mob.interpret("эмоц громко и протяжно мяукает.");
}
load mob 3090
см
Черный Квадрат
Ты стоишь на черном игровом квадрате. Ты видишь заброшенные ворота
недалеко на юге.
[Выходы: север юг запад]
Маленький верный котенок (kitten) здесь.
Котенок громко и протяжно мяукает.
Однако быстро становится понятно, что этот котенок будет не переставая мяукать каждые 4 секунды, что невыносимо. Сделаем это мяуканье более случайным. Для работы со случайными числами есть несколько функций в корневом объекте. Посмотрим по ним подсказку:
eval ptc(.api())
...
chanceOneOf: (x) true если .number_range(1, x) == 1
chance: (x) true если x < .number_percent()
number_percent: произвольное число от 1 до 100
number_range: (x, y) произвольное число в промежутке от x до y
dice: (x, y) x раз кинуть кубик с y гранями
Нам удобен метод .chance
: так, .chance(50)
вернет true
в 50ти случаях из ста, т.е. с вероятностью 50%, .chance(1)
вернет true
с вероятностью 1% и так далее.
Перепишем сценарий:
.get_mob_index(3090).onSpec = function(mob) {
if (.chance(20))
mob.interpret("эмоц громко и протяжно мяукает.");
}
Теперь он мяукает не так часто, но достаточно часто для отладки. В окончательной версии шанс можно сделать равным 1.
Предположим, мы хотим добавить ему еще какие-то действия с равной вероятностью. Для этого пригодится метод .chanceOneOf
. Вот как будет выглядеть новый сценарий с тремя случайными действиями котенка:
.get_mob_index(3090).onSpec = function(mob) {
if (.chance(20)) {
if (.chanceOneOf(3))
mob.interpret("эмоц громко и протяжно мяукает.");
else if (.chanceOneOf(2))
mob.interpret("эмоц зевает и грациозно потягивается.");
else
mob.interpret("эмоц гоняется за собственным хвостом.");
}
}
Следующий шаг - сделать, чтобы котенок драл мебель. Для начала выясним, есть ли рядом с ним в комнате мебель,
т.е. предмет, лежащий на полу, и имеющий тип furniture
. Все типы предметов описаны в таблице .tables.item_table
, содержимое таблицы можно вывести на экран:
eval ptc(.tables.item_table.api())
Список всех доступных таблиц с флагами:
eval ptc(.tables.api())
Первая колонка содержит символическое название типа предмета, оно же используется в зонах и редакторе OLC. Для мебели это поле .tables.item_table.furniture
.
Теперь надо найти в комнате предмет-мебель, используя метод комнаты get_obj_type
. Вот его справка в API комнаты:
eval ptc(in_room.api())
...
get_obj_type: (type) поиск объекта в комнате по его типу, item type
Для отладки создадим рядом с котенком какую-то мебель (на локальном мире можно поискать мебель командой vnum type furniture
).
load obj 9822
Ты создаешь диван!
А затем найдем эту мебель рядом с нами в комнате и отпечатаем ее имя:
eval sofa=in_room.get_obj_type(.tables.item_table.furniture)
eval ptc(sofa.name)
sofa диван
Для того, чтобы вывести сообщение о действии, выполняемом с предметом, воспользуемся методом персонажа recho
- вывести строку всем в комнате. Подробнее о формате сообщений смотреть в этой статье. Статья описывает функцию fmt
, используемую в коде, однако в фене формат сообщений точно такой же.
Вот как теперь выглядит сценарий. Дабы избежать вытягивания кода в "спагетти", сделаны некоторые изменения:
.get_mob_index(3090).onSpec = function(mob) {
if (!.chance(1))
return;
if (.chanceOneOf(4))
mob.interpret("эмоц громко и протяжно мяукает.");
else if (.chanceOneOf(3))
mob.interpret("эмоц зевает и грациозно потягивается.");
else if (.chanceOneOf(2))
mob.interpret("эмоц гоняется за собственным хвостом.");
else {
var furniture;
furniture = mob.in_room.get_obj_type(.tables.item_table.furniture);
if (furniture == null)
return;
mob.recho("%^C1 дерет когтями %O4.", mob, furniture);
}
}
В зоне есть несколько комнат "зловещей рощи", в дневное время там обычные выходы, а ночью в роще можно заблудиться - идешь в одном направлении, а попадаешь совсем в другое.
Время суток содержится в поле корневого объекта .sunlight
. Можно отпечатать список всех полей корневого объекта и посмотреть по нему справку:
eval ptc(.api())
...
sunlight: время суток: 0=ночь, 1=рассвет, 2=день, 3=закат
...
Поле .hour
содержит конкретный час, однако в разное время года темнота наступает в разное время, поэтому поле .sunlight
для нашей задачи удобнее.
Список полей и методов комнаты можно посмотреть командой
eval ptc(in_room.api())
где in_room
- комната, где находится this
, т.е. персонаж выполняющий команду eval
.
Нам понадобятся методы exits
- список всех видимых персонажу выходов и getRoom
- получить комнату, в которую ведет какая-то дверь.
Метод exits
создает и возвращает список. Также новый список можно создать с помощью метода корневого объекта .List()
. Поэтому методы списка можно отпечатать себе такими способами:
eval ptc(in_room.exits(this).api())
eval ptc(.List().api())
Нам пригодится метод random
- получить случайный элемент списка.
Комбинируя все это вместе, получаем такой код для поиска случайной двери для персонажа ch, затем поиска комнаты, в которую эта дверь ведет и, наконец, внума (номера) этой комнаты:
var mydoors, randomDoor, randomRoom, randomDoorVnum;
mydoors = exits(ch);
randomDoor = mydoors.random();
randomRoom = getRoom(randomDoor);
randomDoorVnum = randomRoom.vnum;
return randomDoorVnum;
Избавляясь от промежуточных переменных:
return getRoom(exits(ch).random()).vnum;
Oбычно комнаты со случайными выходами задаются в редакторе зон, с помощью особого вида reset-a, 'randomize exits'. Для нашей задачи мы можем перемешивать выходы при попытке персонажа выйти или зайти в эту комнату. Для этого есть такие триггера:
onEntryLocation(walker,sourceRoom,door)
onExitLocation(walker,targetRoom,door)
Они подробно описаны в списке триггеров комнат.
Таким образом, чтобы в ночное время возвращать для персонажа случайную целевую комнату, понадобится такой код:
.get_room_index(4000).onExitLocation = function(walker, targetRoom, door) {
if (.sunlight == 0)
return getRoom(exits(walker).random()).vnum;
};
Примечание: в общем случае метод exits
может вернуть пустой список (например, в комнате без выходов), тогда метод random
вернет null
и при попытке вызова getRoom
с таким аргументом произойдет исключение. Однако в нашем случае мы уверены, что хотя бы один выход из комнаты есть: выход под номером door
, ведущий в targetRoom
- именно в него бы попал персонаж, если бы мы не вмешались.
В примере выше триггер onExitLocation присвоится комнате с номером 4000. Но что если таких комнат несколько, например, наша "зловещая роща" состоит из 4х комнат: 4208, 4216, 4207, 4215. Ленивый способ - присвоить триггер какой-то переменной и затем сделать четыре присваивания подряд:
var myfunc;
myfunc = function(walker, targetRoom, door) {
...
};
.get_room_index(4208).onExitLocation = myfunc;
.get_room_index(4216).onExitLocation = myfunc;
...
Более продвинутый способ - организовать все номера этих комнат в список и выполнить присваивание всем элементам списка, используя метод forEach
. Этот метод принимает в параметры функцию, которую надо выполнить для каждого элемента списка. Переменная this
внутри функции будет указывать на каждый элемент по очереди:
.List().add(4208, 4216, 4207, 4215).forEach(
function() {
.get_room_index(this).onExitLocation = function(walker, targetRoom, door) {
...
};
}
};
Подробнее о работе со сценариями читайте в этой статье.
.apply(function() {
.List().add(4208, 4216, 4207, 4215).forEach(function() {
.get_room_index(this).onExitLocation = function(walker,targetRoom,door) {
if (.sunlight == 0)
return getRoom(exits(walker).random()).vnum;
};
});
})
Посмотреть, успешно ли присвоился триггер какой-то комнате, можно так:
eval ptc(.get_room_index(4215).rtapi())
Runtime fields:
onExitLocation
Библиотекарша (внум 28800) реагирует на фразу 'кофе пожалуйста', наливает и дает тебе кофе. Это должно происходить реалистично, с паузами между ее действиями.
Перед началом работы просмотрите эту статью о том, как работать со сценариями из веб-клиента и не только.
Сперва сделаем простую реакцию на эту фразу и все действия выполним подряд без задержек. В списке триггеров для мобов находим триггер onSpeech
. Внутри него нужно проверить, то ли сказали, что нам нужно, и если да - выполнить набор каких-то действий.
Так как триггер onSpeech
вешается на прототип моба, первым параметром ему передастся конкретный экземпляр библиотекарши, которая услышала реплику. В нашем примере это переменная ch
. Параметр vict
- тот, кто произнес реплику.
Вот как может начинаться такой сценарий, который можно создать, используя редактор мобов medit:
medit 28800
fenia onSpeech
Откроется редактор сценариев с уже готовым шаблоном для onSpeech:
.get_mob_index(28800).onSpeech = function (ch, vict, msg) {
if (msg != "кофе пожалуйста")
return;
ch.interpret("emote снимает с печки чайник.");
ch.interpret("emote наливает чашку {Dкофе{x.");
}
Теперь нужно, собственно, создать и вручить говорящему чашку кофе. Для создания экземпляров предметов служит метод create
на прототипе предмета. Чашка кофе имеет внум 3101. Свежесозданный предмет надо тут же вручить кому-то в руки или положить в комнату, иначе он зависнет в 'лимбо'.
var coffee;
coffee = .get_obj_index(3101).create();
coffee.obj_to_char(ch);
Избавившись от промежуточной переменной, получим одну строку для того, чтобы создать экземпляр чашки и положить его библиотекарше в инвентарь:
.get_obj_index(3101).create().obj_to_char(ch);
Вот полный сценарий без задержек между действиями:
.get_mob_index(28800).onSpeech = function (ch, vict, msg) {
if (msg != "кофе пожалуйста")
return;
ch.interpret("emote снимает с печки чайник.");
ch.interpret("emote наливает чашку {Dкофе{x.");
.get_obj_index(3101).create().obj_to_char(ch);
ch.interpret("дать чашка " + vict.getName());
ch.interpret("эмоц тепло улыбается.");
}
Конечно, тут не все идеально: библиотекарша может не видеть говорящего, у того может быть слишком много всего в инвентаре и дать чашку не получится. Сценарий можно усложнить проверкой, оказалась ли чашка в итоге в инвентаре у говорящего, но мы для простоты примера этого делать не будем. Проверка, например, может выглядеть так:
// запомнить чашку в переменной coffee, как в примере выше
if (coffee.carried_by != vict) {
...
}
Все триггера, начинающиеся с on
, выполняются сразу, прямо изнутри обработчика той или иной команды (сказать
, дать
). Поэтому все действия, которые моб производит внутри такого триггера, выполнятся единым блоком.
Если внутри триггера хочется выполнять действия с задержками, необходимо использовать набор триггеров, начинающийся с post
: postSpeech
, postGive
и так далее.
Такой триггер выполнится не сразу, но почти мгновенно после того, как завершится вызвавшая его команда.
И так как результатов выполнения триггера уже никто не ждет, можно провести в нем сколько угодно времени.
С технической точки зрения, каждый такой post
-триггер будет выполняться внутри отдельного феневого потока.
В этом примере библиотекарша отреагирует на фразу не мгновенно, а в следующий "пульс" (четверть секунды), и это будет заметно:
.get_mob_index(28800).postSpeech = function (ch, vict, msg) {
if (msg != "кофе пожалуйста")
return;
ch.interpret("emote снимает с печки чайник.");
}
Не забудем обнулить триггер onSpeech
, иначе они выполнятся оба:
eval .get_mob_index(28800).onSpeech = null;
Однако такой задержки в четверть секунды может оказаться мало. К тому же, нам хочется делать паузу и между остальными ее действиями. Поэтому воспользуемся методом .scheduler.sleep
, который прерывает выполнение текущего потока (триггера) на указанное число пульсов. В одной секунде - 4 пульса.
Теперь можно вызвать задержки перед каждым действием библиотекарши, добившись более-менее реалистичной реакции:
.get_mob_index(28800).postSpeech = function (ch, vict, msg) {
if (msg != "кофе пожалуйста")
return;
.scheduler.sleep(2);
ch.interpret("эмоц снимает с печки чайник.");
.scheduler.sleep(4);
ch.interpret("эмоц наливает чашку {Dкофе{x.");
.scheduler.sleep(4);
.get_obj_index(3101).create().obj_to_char(ch);
ch.interpret("дать чашка " + vict.getName());
.scheduler.sleep(4);
ch.interpret("эмоц тепло улыбается.");
}
Библиотекарше должно быть все равно, произнесли рядом с ней фразу кофе пожалуйста
или кофе, пожалуйста
, или даже пожалуйста, кофе мне
. Однако наш код позволяет ей реагировать на одну жестко заданную фразу. Для более гибкой реакции воспользуемся регулярными выражениями и методом строки match
.
По сути нас интересует наличе во фразе слова кофе
и волшебного слова пожалуйста
, пусть даже написанного с ошибками:
if (!msg.match("кофе"))
return;
if (!msg.match("пожалуй*ста"))
return;
Для первой проверки, конечно, можно было воспользоваться и поиском подстроки (метод contains
), однако если мы захотим научить библиотекаршу понимать и по-английски, это удобнее делать с помощью регулярных выражений, перечисляя варианты через |
:
if (!msg.match("кофе|coffee"))
return;
if (!msg.match("пожалуй*ста|please|pls"))
return;
Это просто пример, и улучшать тут можно до бесконечности. Список всех методов у строки можно посмотреть так:
eval ptc("".api())
.get_mob_index(28800).postSpeech = function (ch, vict, msg) {
if (!msg.match("кофе|coffee"))
return;
if (!msg.match("пожалуй*ста|please|pls"))
return;
.scheduler.sleep(2);
ch.interpret("эмоц снимает с печки чайник.");
.scheduler.sleep(4);
ch.interpret("эмоц наливает чашку {Dкофе{x.");
.scheduler.sleep(4);
.get_obj_index(3101).create().obj_to_char(ch);
ch.interpret("дать чашка " + vict.getName());
.scheduler.sleep(4);
ch.interpret("эмоц тепло улыбается.");
}
Свалка примеров, как выполнить ту или иную задачу на Фене.
Для проверки этого или любого другого битового флага используется метод .isset_bit
:
if (.isset_bit(ch.affected_by, .tables.affect_flags.sanctuary)) {
}
Также можно воспользоваться битовым оператором и
, &
:
// Типичный набор сопротивляемостей у сатира:
eval ptc(.tables.res_flags.names(res_flags))
disease wood
// Проверяем что битовая маска не нулевая
eval ptc(res_flags & .tables.res_flags.wood)
8388608
// А в этом случае - нулевая.
eval ptc(res_flags & .tables.res_flags.holy)
0
// Поэтому в сценарии можно писать:
if (ch.res_flags & .tables.res_flags.wood) {
ch.act("Палки тебе не страшны!");
}
Предмет в 99.9% случаев будет в одном из трех мест: внутри другого предмета, в руках у персонажа или на полу в комнате.
if (item.in_obj == container) {
item.getRoom().echo("%^O1 находится внутри %O2.", item, container);
} else if (item.carried_by != null) {
item.getRoom().echo("%^O4 несёт %C1.", item, item.carried_by);
} else if (item.in_room != null) {
item.getRoom().echo("%^O1 просто лежит на полу в комнате '%s'.", item, item.in_room.name);
}
Поле комнаты ppl
хранит список (List) всех персонажей в этой комнате. Поэтому для работы с ним доступны все методы списка, в том числе random
:
eval ptc(in_room.ppl.api()) - вывести API для списка
eval ptc(in_room.ppl.random().name) - вывести имя случайного персонажа в комнате
Если необходим любой случайный персонаж, кроме ch, этот элемент можно вычесть из списка методом sub
и продолжить работать так же:
var vict;
vict = ch.in_room.ppl.sub(ch).random();
if (vict != null) {
}
Поле комнаты ppl
содержит список персонажей в комнате, и для работы с ним удобно использовать синтаксис for (element in list)
. Предварительно из этого списка можно вычесть ch, чтобы не делать эту проверку внутри самого цикла.
// Переменную mob не надо предварительно объявлять с помощью var.
for (mob in ch.in_room.ppl.sub(ch)) {
if (.chance(10)) {
mob.interpret("эмоц глупо хихикает.");
}
}
var group;
group = 0;
for (gch in ch.in_room.ppl) {
if (ch.is_same_group(gch))
group = group + 1;
}
ch.act("Рядом с тобой %d членов твоей группы, включая тебя.", group);
Пусть у нас есть персонаж ch, и ему надо вывести сводку о погоде, если он снаружи помещения.
if (.isset_bit(ch.in_room.room_flags, .tables.room_flags.indoors))
ch.act("Сперва выйди на улицу.");
else if (.sky == 0)
ch.act("Над тобой ясное синее небо.");
else if (.sky == 1)
ch.act("Небо затянуто тучами.");
else if (.sky == 2)
ch.act("Идёт дождь.");
else
ch.act("Гремит гром и сверкают молнии, лучше поищи укрытие!");
С полем комнаты sector_type
можно работать как численно, так и превратив его в строку для наглядности.
if (ch.in_room.sector_type == .tables.sector_table.forest)
ch.act("Наконец-то ты в лесу!");
var s;
s = .tables.sector_table.name(
ch.in_room.sector_type);
if (s == "forest")
ch.act("Вокруг тебя лес.");
else if (s == "field");
ch.act("Степь да степь кругом.");
else if (s == "city")
ch.act("Ты в городской местности.");
if (ch.get_eq_char("wield") != null) {
// ...
}
Для этого найдем предмет в локации wield
, проверим, что это оружие, и проанализируем его параметры.
var w, damtype, ave;
w = ch.get_eq_char("wield");
if (w != null && w.item_type == .tables.item_table.weapon) {
ch.act("%^O1 имеет тип удара %s и среднее повреждение %d.",
w,
.tables.weapon_flags.name(w.value3),
w.ave);
if (w.value3 == .tables.weapon_flags.pierce
|| w.value3 == .tables.weapon_flags.stab)
{
ch.act("Это оружие отлично подходит для удара в спину.");
}
}
Этот аффект сделает оружие обмораживающим на 10 тиков.
var af;
af = .Affect();
af.type = "winters touch";
af.where = .tables.affwhere_flags.weapon;
af.level = 50;
af.duration = 10;
af.bitvector = .tables.weapon_type2.frost;
obj.affectAdd(af);
Предварительно неплохо бы проверить, не обладает ли оружие уже какими-то необычными свойствами:
if (obj.hasWeaponFlag("flaming shocking frost"))
return;
Этот аффект ничего не изменяет (не задействованы поля location
, modifier
и т.д.), но на него есть проверка из умений blackjack
и ему подобных.
var af;
af = .Affect();
af.type = "backguard";
af.level = ch.modifyLevel;
af.duration = 2;
ch.affectAdd(af);
Более компактная запись:
// Создать и повесить на персонажа аффект на 2 тика.
ch.affectAdd(
.Affect("backguard", ch.modifyLevel, 2));
var dam, vict;
vict = ch.fighting;
ch.act("Ты бьешь по %C3 со страшной силой.", vict);
ch.rvecho(vict, "%^C1 бьет по %C3 со страшной силой.", ch, vict);
vict.act("%^C1 бьет по тебе со страшной силой.", ch);
dam = .dice(ch.modifyLevel, 18);
// Повреждение с сообщением "Твое землятрясение" и типом повреждений 'none'.
ch.damage(vict, dam, "earthquake", "none");
Пусть надо реагировать на реплики вида "можешь дать золото", "можешь подарить лошадь", "можешь отдать" и так далее, а в ответ отвечать "я и не собирался подарить лошадь", т.е. используя те же слова, которые персонаж употребил в своей фразе. Для этого удобно пользоваться методом строки matchAndReplace
:
.get_mob_index(3333).onSpeech = function(mob, speaker, message) {
var pattern, reply;
// Регулярное выражение, которому должна соответствовать реплика персонажа:
pattern = "можешь (дать|вернуть|отдать|подарить) ([^ ]+)";
// Шаблон ответа моба. $1 будет заменено на первое совпадение из скобок, $2 - на второе и так далее.
reply = "я и не собирался тебе $1 $2";
if (message.match(pattern))
mob.say(reply.matchAndReplace(pattern, message));
};
Поле religion
у персонажа ссылается на структуру Religion, которую также можно получить через конструктор .Religion("god name")
.
Вывод всех полей и методов религии:
eval ptc(.Religion("ares").api())
Проверка доступности религии персонажу this
:
eval ptc(.Religion("enki").available(this))
Пример onGreet
тригера, где моб по-разному приветствует входящего в зависимости от религии:
.get_mob_index(xxxx).onGreet = function(mob, ch) {
if (ch.is_npc())
return;
if (ch.religion.name == "none")
mob.say("Приветствую тебя, безбожн%Gик|ик|ица.", ch);
else
mob.say("Приветствую тебя, идущ%Gее|ий|ая по пути %N2.", ch, ch.religion.nameRus);
};
Предположим, мы хотим отравить противника, но не хотим зря тратить "заряды" - если у него иммунитет к яду или заклинаниям в целом, то пытаться бесполезно. Вот как выглядит эта проверка:
if (!victim.isImmune(.tables.damage_table.poison, .tables.damage_flags.magic)) {
if (!victim.isAffected("poison")) {
ch.spell("poison", ch.modifyLevel, victim);
return;
}
}
// Альтернативная запись, через строки.
if (!victim.isImmune("disease", "magic")) {
if (!victim.isAffected("plague")) {
ch.spell("plague", ch.modifyLevel, victim);
return;
}
}
Для этоо используйте триггер onExtraDescr
. Он вызывается каждый раз, как персонаж смотрит на какое-то ключевое слово рядом с предметом или на сам предмет. Из триггера нужно вернуть текст или null. В примере ниже при выполнении команды look telescope
(см телескоп
) персонаж увидит то или иное описание, в зависимости от поля state
телескопа (это поле устанавливается в другом триггере).
telescope.onExtraDescr = function(obj, char, keyword) {
if (keyword.is_name("telescope телескоп")) {
if (obj.state == "galaxy")
return "Краем глаза ты замечаешь в окуляре дивное изображение таинственной галактики.";
else if (obj.state == "arcadia")
return "Мельком взглянув в окуляр ты видишь цветущие луга, притягивающие и манящие.";
return null;
}
};
Из фени легко получить значение аттрибутов, которые представляют из себя строку или число.
var warcry;
warcry = ch.attribute("warcry");
if (warcry != null)
ch.interpret_raw("yell", warcry);
else {
ch.pecho("Ты издаешь боевой клич и чувствуешь, что теперь тебе все по плечу!");
ch.recho("%^C1 издает боевой клич!", ch);
}
Также доступны некоторые более сложные аттрибуты, представляющие из себя ассоциативный массив.
var restrings;
restrings = ch.attribute("create food");
if (restrings != null && restrings["default"] != null) {
ch.act("Созданная тобой еда получит имя %s и шорт %s.", restrings["default"].name, restrings["default"].shortDescr);
}