В этой статье я попытаюсь передать практический опыт скриптового создания точек перехода между уровнями. Файл all.spawn меняться не будет, поэтому такой механизм более удобен для совмещения модов и не требует начала новой игры.
В качестве примера попробуем реализовать режим freeplay — продолжение игры после уничтожения О-Сознания. Для этого нужно создать как минимум две точки — возврат на ЧАЭС-1 со внутреннего дворика (после уничтожения О-Сознания) и переход из ЧАЭС-1 в Припять. К слову сказать, если вернуться на ЧАЭС-1, то там будет продолжать действовать таймер «выброса», который в конце-концов включит «deadzone». Отключить счетчик мне пока не удалось, но зато можно легко отключить сам «выброс» и всласть побегать по уровню.
[править]
Часть 1. Генерация работающего LEVEL_CHANGER
Стандартной функции alife():create(…) недостаточно для создания полноценного level_changer. Собственно методика создания сложных объектов описана в статье Один из методов спавна. Сложность заключалась лишь в порядке полей и свойствах Shape. Неоценимую помощь в этом вопросе оказала утилита ACDC (created by bardak).
Ниже я привожу код функции, которая создает и инициализирует level_changer:
function create_level_changer(
p_story_id, — STORY_ID нового level_changer (понадобится нам позже)
p_position, — вектор, координаты точки, в которой будет располагаться центр нового level_changer
p_lvertex_id, — level_vertext_id — идентифицируют уровень, на котором будет создан level_changer
p_gvertex_id, — game_vertext_id
p_dest_lv, — level_vertex_id — идентифицируют уровень, на который level_changer будет перебрасывать игрока
p_dest_gv, — game_vertex_id
p_dest_pos, — координаты точки, в которой на новом уровне окажется игрок
p_dest_dir, — направрение взгляда игрока
p_dest_level, — название уровня, например «L11_Pripyat»
p_silent — следует задать 1, чтобы подавить вопрос о смене уровня (автоматический переход)
)
local obj = alife():create(«level_changer», p_position, p_lvertex_id, p_gvertex_id)
level.map_add_object_spot(obj.id, «level_changer», «»)
local packet = net_packet()
obj:STATE_Write(packet)
— свойства cse_alife_object
local game_vertex_id = packet:r_u16()
local cse_alife_object__unk1_f32 = packet:r_float()
local cse_alife_object__unk2_u32 = packet:r_u32()
local level_vertex_id = packet:r_u32()
local object_flags = packet:r_u32()
local custom_data = packet:r_stringZ()
local story_id = packet:r_u32()
local spawn_story_id = packet:r_u32()
— свойства cse_shape
local shape_count = packet:r_u8()
for i=1,shape_count do
local shape_type = packet:r_u8()
if shape_type == 0 then
— sphere
local center = packet:r_vec3()
local radius = packet:r_float()
else
— box
local axis_x_x = packet:r_float()
local axis_x_y = packet:r_float()
local axis_x_z = packet:r_float()
local axis_y_x = packet:r_float()
local axis_y_y = packet:r_float()
local axis_y_z = packet:r_float()
local axis_z_x = packet:r_float()
local axis_z_y = packet:r_float()
local axis_z_z = packet:r_float()
local offset_x = packet:r_float()
local offset_y = packet:r_float()
local offset_z = packet:r_float()
end
end
— свойства cse_alife_space_restrictor
local restrictor_type = packet:r_u8()
— свойства cse_level_changer
local dest_game_vertex_id = packet:r_u16()
local dest_level_vertex_id = packet:r_u32()
local dest_position = packet:r_vec3()
local dest_direction = packet:r_vec3()
local dest_level_name = packet:r_stringZ()
local dest_graph_point = packet:r_stringZ()
local silent_mode = packet:r_u8()
packet:w_begin(game_vertex_id) — game_vertex_id
packet:w_float(cse_alife_object__unk1_f32)
packet:w_u32(cse_alife_object__unk2_u32)
packet:w_u32(level_vertex_id) — level_vertex_id
packet:w_u32( bit_not(193) ) — object_flags = -193 = 0xFFFFFF3E
packet:w_stringZ(custom_data)
packet:w_u32(p_story_id) — story_id
packet:w_u32(spawn_story_id)
packet:w_u8(1) — количество фигур
— packet:w_u8(0) — тип фигуры: сфера
— packet:w_vec3(vector():set(0, 0, 0)) — sphere_center
— packet:w_float(3.0)
packet:w_u8(1) — тип фигуры: box
packet:w_float(2) — axis_x_x
packet:w_float(0) — axis_x_y
packet:w_float(0) — axis_x_z
packet:w_float(0) — axis_y_x
packet:w_float(4) — axis_y_y
packet:w_float(0) — axis_y_z
packet:w_float(0) — axis_z_x
packet:w_float(0) — axis_z_y
packet:w_float(4) — axis_z_z
packet:w_float(0) — offset_x
packet:w_float(0) — offset_y
packet:w_float(0) — offset_z
packet:w_u8(3) — restrictor_type
packet:w_u16(p_dest_gv) — destination game_vertex_id
packet:w_u32(p_dest_lv) — destination level_vertex_id
packet:w_vec3(p_dest_pos) — destination position
packet:w_vec3(p_dest_dir) — destination direction (направление взгляда)
packet:w_stringZ(p_dest_level) — destination level name
packet:w_stringZ(«start_actor_99») — some string, always const
packet:w_u8(p_silent) — 1 for silent level changing
packet:r_seek(0)
obj:STATE_Read(packet, packet:w_tell())
— news_manager.send_tip(db.actor, «LC creation finished», nil, nil, 30000)
end
Для shape типа «box» загрузка координат методом packet:r_matrix() окончилась неудачей. Я подозреваю, что не был прочитан вектор «offset», но точной уверенности нет, поэтому пока остановился на покомпонентной выборке и сохранении координат.
[править]
Часть 2. Создание точек перехода
Теперь следует написать функции создания нужных точек перехода и подключить их к игре. Сами функции просты:
function exit_monolit()
if (not has_alife_info(«freeplay_activated1»)) then
create_level_changer(11410, vector():set(-13.26, 47.71, 46.57), 200, 2417,
162109,
2384,
vector():set( 375.615, 0.224, 27.737 ),
vector():set( 0.0, 0.0 , 0.0 ),
«L12_Stancia»,
1)
db.actor:give_info_portion(«freeplay_activated1»)
end
— создается переход из ЧАЭС в Припять
create_chaes2pripyat_exit()
— актер перебрасывается в level_changer, возвращающий его на ЧАЭС, ко входу в бункер
db.actor:set_actor_position( vector():set(-13.26, 47.71, 46.57) )
end
function refuze_o_sozn()
if (not has_alife_info(«freeplay_activated2»)) then
create_level_changer(21410, vector():set(946.872, 6.0, 167.66), 240852, 2637,
472710,
2280,
vector():set( 1062.15, -0.0982, -3.512 ),
vector():set( 0.0 , 0.0 , -1.0 ),
«L12_Stancia»,
1)
db.actor:give_info_portion(«freeplay_activated2»)
end
— создается переход из ЧАЭС в Припять
create_chaes2pripyat_exit()
— актер перебрасывается в level_changer, возвращающий его к правым воротам ЧАЭС
db.actor:set_actor_position( vector():set(946.872, 6.0, 167.66) )
end
function create_chaes2pripyat_exit()
— создается переход из ЧАЭС в Припять
if (not has_alife_info( «exit_chaes2pripyat_created» )) then
create_level_changer(31410, vector():set( 917.35, 0.419, -316.35 ), 403866, 2401,
73868,
2117,
vector():set( 31.3, 3.0, 240.0 ),
vector():set( 0.0, 0.0, -1.0 ),
«L11_Pripyat»,
0)
db.actor:give_info_portion(«exit_chaes2pripyat_created»)
end
end
Функцию exit_monolit я создал исключительно для тестирования, но решил оставить и тут. Вдруг кто-то захочет реализовать более сложный возврат: Меченого грузят в «грузовик смерти» и он снова приходит в себя на кордоне…
Функция exit_monolit создает «тихий» переход на уровень ЧАЭС-1 и обычный — в начале уровня ЧАЭС-1 для возврата в Припять, после чего перебрасывает актера прямо внутрь созданного перехода. Функция refuze_o_sozn делает тоже самое, только игрок появляется перед воротами в правом верхем углу карты (мне кажется, что так более логично). Все телепорты защищаются уникальными info_portions, дабы избежать их повторного создания, ведь игрок может захотеть «закончить» игру несколько раз.
Теперь подключение. Во-первых надо добавить новые info-portions. Я решил не изменять оригинальные файлы игры, а сделал для них (ну и для других тоже) отдельный файл
config\gameplay\_info_sa.xml
следующего вида:
<game_information_portions>
<info_portion id=»freeplay_activated1″></info_portion>
<info_portion id=»freeplay_activated2″></info_portion>
<info_portion id=»exit_chaes2pripyat_created»></info_portion>
</game_information_portions>
Теперь в этот файл можно будет добавлять новые info_portion, которые вы будете использовать в своих сюжетах. Подключается этот файл в system.ltx в секции «info_portions»:
[info_portions]
;список xml файлов, содержащих info_portions
files = _info_sa, info_portions, ………………..
Кстати, именно так я рекомендую добавлять новые диалоги и новых персонажей. Это упростит процессы совмещения модов и аддонов.
Следующий шаг — подключение наших скриптов к игре. Для этого откройте файл
config\ui\ui_movies.xml
найдите элементы «mov_desire_» (их пять, по количеству ложных концовок игры — все варианты общения с Монолитом). Внутри каждого элемента есть дочерние элементы «function_on_stop», которые задают функцию, запускающуюся сразу после ролика. Стандартное содержимое:
<function_on_stop>xr_effects.game_credits</function_on_stop>
Функция xr_effects.game_credits запускает финальные титры. Именно ее и нужно заменить на вызов нашей функции _freeplay_sa.exit_monolit. Вот что должно получиться:
<function_on_stop>_freeplay_sa.exit_monolit</function_on_stop>
<!— original: <function_on_stop>xr_effects.game_credits</function_on_stop> —>
Старый элемент я советую не удалять, а закомментировать.
Чуть ниже «mov_desire_5» находится тэг ролика для концовки «Присоединение к О-Сознанию». Его мы трогать не будем — Меченый станет медузой. А вот после него — тэг для ролика «Отказ от О-Сознания»: «mov_refuse_osoznanie». Функцию завершения в нем заменим следующим образом:
<function_on_stop>_freeplay_sa.refuze_o_sozn</function_on_stop>
<!— original: <function_on_stop>xr_effects.game_credits</function_on_stop> —>
Можно запускать и, если есть сохранения перед монолитом и/или о-сознанием, тестировать. Первые переходы срабатывают «тихо» — запрос на смену уровня не выдается (в параметре p_silent задана 1). До перехода в Припять можно успеть добежать, пока не сработал выброс… Но это как-то неправильно, выброс надо остановить (он же произошел, пока мы были внутри станции). «По-честному» (скриптом, без модификации all.spawn) таймер выброса отключить не получится, но зато выброс можно подавить небольшим «хаком». Найдите файл xr_logic.script, а в нем — функцию switch_to_section. Ее нужно модифицировать следующим образом:
— Выполняет переключение на указанную секцию, если задана.
— Если section == nil, остается работать старая секция.
function switch_to_section(npc, st, section)
if section == nil or section == «» then
return false
end
— 15.03.2008 by SA:
— отключает «смертельные зоны» на ЧАЭС после запуска режима FREEPLAY
— таймер не отключается, но сам «выброс» не происходит
if (section == «sr_aes_deadzone») then
if (has_alife_info(«freeplay_activated1») or has_alife_info(«freeplay_activated2»)) then
return false
end
end
… далее без изменений …
end
Данный способ оставляет висящий на нулях таймер и все эффекты начала выброса, но сам «выброс» отключается.
И последнее: переход ЧАЭС1-Припять необходимо отметить на карте. Для этого существует вполне «легальный» механизм. Находим файл level_tasks.script и в конце функции add_lchanger_location дописываем следующее:
— aes (++16.03.2008 by SA):
obj = sim:story_object(31410)
if obj then
level.map_add_object_spot(obj.id, «level_changer», «To Pripyat»)
end
Здесь 31410 — story_id нашего level_changer, который создается нашей функцией create_chaes2pripyat_exit.
На этом пока все (мелкие огрехи типа направления взгляда игрока после смены уровня исправлю позже). Прошу тестировать и дополнять.
С уважением, sarthur.
———————AMK————————————————————————
— Получение параметров лч.
function get_lc_data(obj)
local packet = net_packet()
obj:STATE_Write(packet)
local size=packet:w_tell()
packet:r_seek(0)
local t={}
t.game_vertex_id = packet:r_u16()
t.distance = packet:r_float()
t.direct_control = packet:r_s32()
t.level_vertex_id = packet:r_s32()
t.object_flags = packet:r_s32()
t.custom_data = packet:r_stringZ()
t.story_id = packet:r_s32()
t.spawn_story_id = packet:r_s32()
local shape_count = packet:r_u8()
t.shapes={}
for i=1,shape_count do
local shape_type = packet:r_u8()
t.shapes<I>={}
t.shapes[i].shtype=shape_type
if shape_type == 0 then
t.shapes[i].center = packet:r_vec3()
t.shapes[i].radius = packet:r_float()
else
t.shapes[i].v1 = packet:r_vec3()
t.shapes[i].v2 = packet:r_vec3()
t.shapes[i].v3 = packet:r_vec3()
t.shapes[i].offset = packet:r_vec3()
end
end
t.restrictor_type = packet:r_u8()
t.dest_game_vertex_id = packet:r_u16()
t.dest_level_vertex_id = packet:r_s32()
t.dest_position = packet:r_vec3()
t.dest_direction = packet:r_vec3()
t.dest_level_name = packet:r_stringZ()
t.dest_graph_point = packet:r_stringZ()
t.silent_mode = packet:r_u8()
if packet:r_elapsed() ~= 0 then
abort(«left=%d», packet:r_elapsed())
end
return t
end
— Запись параметров лч.
function set_lc_data(t,obj)
local packet = net_packet()
packet:w_begin(t.game_vertex_id)
packet:w_float(t.distance)
packet:w_s32(t.direct_control)
packet:w_s32(t.level_vertex_id)
packet:w_s32(t.object_flags)
packet:w_stringZ(t.custom_data)
packet:w_s32(t.story_id)
packet:w_s32(t.spawn_story_id)
packet:w_u8(table.getn(t.shapes))
for i=1,table.getn(t.shapes) do
packet:w_u8(t.shapes[i].shtype)
if t.shapes[i].shtype == 0 then
packet:w_vec3(t.shapes[i].center)
packet:w_float(t.shapes[i].radius)
else
packet:w_vec3(t.shapes[i].v1)
packet:w_vec3(t.shapes[i].v2)
packet:w_vec3(t.shapes[i].v3)
packet:w_vec3(t.shapes[i].offset)
end
end
packet:w_u8(t.restrictor_type)
packet:w_u16(t.dest_game_vertex_id)
packet:w_s32(t.dest_level_vertex_id)
packet:w_vec3(t.dest_position)
packet:w_vec3(t.dest_direction)
packet:w_stringZ(t.dest_level_name)
packet:w_stringZ(t.dest_graph_point)
packet:w_u8(t.silent_mode)
packet:r_seek(0)
obj:STATE_Read(packet, packet:w_tell())
end
— Создание лч и перемещение ГГ(Автор Xiani).
function start_jump_to(x,y,z,lv, gv, dx, dy, dz, level_name)
amk.mylog(«jump:create_lc»)
local lc = alife():create(«level_changer», db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id())
amk.mylog(«jump:get_lc_data»)
local tbl = get_lc_data(lc)
local shapes={}
shapes[1]={}
shapes[1].shtype=0
shapes[1].radius = 10
shapes[1].center = vector():set(0,0,0)
tbl.shapes = shapes
tbl.dest_level_name = level_name
tbl.dest_position = vector():set(x,y,z)
tbl.dest_direction = vector():set(dx,dy,dz)
tbl.dest_game_vertex_id = gv
tbl.dest_level_vertex_id = lv
tbl.silent_mode = 1
tbl.story_id = 8657
amk.mylog(«jump:set_lc_data»)
set_lc_data(tbl,lc)
db.actor:set_actor_position(lc.position)
amk.mylog(«jump:finished»)
end
А использовать просто:
function road_to_yantar(actor, npc)
dialogs.relocate_money(npc, 3500, «out»)
start_jump_to(29.146209716797,-11.688985824585,-279.86639404297,54978,1480, 0,0,0,»L08_yantar»)
end — попадёшь в бункер к Сахарову.