В этой теме будет выкладываться подробный разбор реализации тех или иных решений по созданию сюжетного мода на конкретных примерах из модификации "Тайные Тропы 2".
Вот небольшое руководство по некоторым скриптовым моментам: http://rghost.ru/4767095 По всем остальным моментам - задавайте вопросы в теме "Вопросы-ответы" - ответим. И всё что знаем - разъясним обязательно и подробно. В этой теме будут собираться и архивироваться наиболее важные и интересные решения по моддингу для ТЧ.
Спавн аномалий скриптовым методом без использования all.spawn, а соответственно без необходимости начинать новую игру:
1) Создаём в папке \gamedata\scripts скриптовый файл с любым именем, напрмер, anomaliya_spawn.script и заполняем этот файл следующим содержанием:
function get_story_id_not_replay() local ST ST = xr_logic.pstor_retrieve(db.actor, "ScriptAnomId", 0) if type(ST) ~= "number" or ST == nil then ST = 9000 xr_logic.pstor_store(db.actor, "ScriptAnomId", ST) end if type(ST) == "number" or ST ~= nil then ST = ST + 1 xr_logic.pstor_store(db.actor, "ScriptAnomId", ST) end return tonumber(ST) end
function create_anom(name_anom, posit_anom, lvid_anom, gvid_anom, radius_anom, power_anom) local obj = alife():create(name_anom, posit_anom, lvid_anom, gvid_anom)
-----------------------write---------------------- local packet = net_packet() obj:STATE_Write(packet)
-----------------------load----------------------- -- cse_alife_object local game_vertex_id = packet:r_u16() local distance = packet:r_float() local direct_control = 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 box = packet:r_matrix() end end
-- cse_alife_space_restrictor local restrictor_type = packet:r_u8()
-- cse_alife_custom_zone local max_power = packet:r_float() local owner_id = packet:r_u32() local enabled_time = packet:r_u32() local disabled_time = packet:r_u32() local start_time_shift = packet:r_u32()
-- cse_alife_anomalous_zone local offline_interactive_radius = packet:r_float() local artefact_spawn_count = packet:r_u16() local artefact_position_offset = packet:r_u32()
-- se_zone_anom local last_spawn_time_present = packet:r_u8()
if (string.find (name_anom, 'zone_zharka_static') ~= nil or string.find (name_anom, 'zone_witches_galantine') ~= nil or string.find (name_anom, 'zone_mosquito_bald') ~= nil or string.find (name_anom, 'zone_mincer') ~= nil or string.find (name_anom, 'zone_gravi_zone') ~= nil or string.find (name_anom, 'zone_buzz') ~= nil ) and packet:r_elapsed() ~= 0 then abort("left=%d", left) end
-- cse_shape packet:w_u8(1) -- количество фигур packet:w_u8(0) -- тип фигуры: сфера packet:w_vec3(vector():set(0, 0, 0)) if radius_anom ~= nil then packet:w_float(radius_anom) else packet:w_float(1.0) end
-- cse_alife_custom_zone if power_anom ~= nil then packet:w_float(power_anom) else packet:w_float(max_power) end packet:w_u32(owner_id) packet:w_u32(enabled_time) packet:w_u32(disabled_time) packet:w_u32(start_time_shift)
function spawn_anom1() this.create_anom('zone_zharka_static', vector():set(-30.974,0.002,693.734),178968,2618, 3, 0.7) end
function spawn_anom2() this.create_anom('zone_witches_galantine_strong', vector():set(-20.851,0.000,693.588),178968,2618, 5, 0.8) end
function spawn_anom3() this.create_anom('zone_mincer_average', vector():set(-26.064,0.000,698.637),178968,2618, 4, 1) end
--Последние две цифры после координат - это радиус действия аномалии и её сила соответственно.
2) Всё, теперь остаётся вызвать функции спавна из диалога или из инфопоршня и три наши аномалии заспавнятся на заданных координатах в нужный игровой момент.
Спавн и удаление переходов между локациями скриптовым методом.
1) Спавн переходов: Создаём в папке \gamedata\scripts скриптовый файл с любым именем, например, y_level.script и вписываем в него следующее:
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)
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_u16(p_dest_gv) -- destination game_vertex_id packet:w_s32(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_02") -- 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, "Новый путь", nil, nil, 20000) end
2) Далее, в самый конец этого файла вписываем собственно функцию спавна перехода, которую потом надо будет вызвать либо из диалога либо из инфопоршня либо по какому-нибудь условию. Например, создадим двухсторонний переход в тайник Стрелка с Агропрома и обратно на Агропром:
function create_agro_taynik() -- создается переход c АГРОПРОМА В ТАЙНИК СТРЕЛКА if (not has_alife_info( "teleport_to_taynik" )) then y_level.create_level_changer(20015, vector():set( -45.089,-0.637,-34.386 ),194126,654, 3053, 717, vector():set( -75.975,-6.726,-74.848 ), vector():set( 0.0,1.5,0.0 ),"l03u_agr_underground",0) db.actor:give_info_portion("teleport_to_taynik") end -- создается переход ИЗ ТАЙНИКА СТРЕЛКА НА АГРОПРОМ if (not has_alife_info( "teleport_from_taynik" )) then y_level.create_level_changer(20016, vector():set( -81.394,-4.946,-70.915 ),2709,717, 194118, 695, vector():set( -47.207,-0.364,-40.833 ), vector():set( 0.0,3.0,0.0 ),"l03_agroprom",0) db.actor:give_info_portion("teleport_from_taynik") end end
--Обратите внимание, что название фашего скриптового файла необходимо ставить в функцию спавна. В представленном варианте - это y_level, у Вас может быть другое имя. --Обратите внимание, что каждомому создаваемому переходу необходимо присвоить свой уникальный story_id и зарегистрировать его в файле: game_story_ids.ltx (путь: \gamedata\config ). В примере заданы для каждого из двух новых переходов стори_айди 20015 и 20016 соответственно. Важно, чтобы создаваемые стори_айди не повторяли уже существующие, иначе будет вылет. Лучше брать значения больше 20000, а лучше внимательно просмотреть существующие номера стори_айди в файле: game_story_ids.ltx 3) Кроме этого, чтобы переход отображался на карте и у него было название, необходимо прописать его в файл: level_tasks.script (путь: \gamedata\scripts) по аналогии с остальными и дать новому переходу название. Вот так: obj = sim:story_object(20015) if obj then level.map_add_object_spot(obj.id, "level_changer", "В Тайник Стрелка") end obj = sim:story_object(20016) if obj then level.map_add_object_spot(obj.id, "level_changer", "На Агропром") end Следите, чтобы в конце функции, в которую добавляете новые регистрации переходов (особенно, если добавляете в самый конец функции), обязательно было, как и изначально, три end 4) В функции спавна переходов выдаются инфопоршни. По одному на каждый создаваемый переход. Эти инфопоршни необходимо прописать в вашем файле с инфопоршнями. В примере это два инфопоршня: teleport_to_taynik и teleport_from_taynik 5) Чтобы заспавнить переход только в один конец, то действуем по аналогии, но сама функция спавна будет немного другой. Например, тайная тропа с Ростока на Армейские Склады:
function tropa_rostok() -- создается переход c РОСТОКА НА АРМЕЙСКИЕ СКЛАДЫ if (not has_alife_info( "info_tropa_rostok" )) then y_level.create_level_changer(20017, vector():set( -289.735,0.099,192.842 ),295,1340, 13812, 1847, vector():set( -408.979,-13.798,400.699 ), vector():set( 0.0,2.0,0.0 ),"l07_military",0) db.actor:give_info_portion("info_tropa_rostok") end end
6) В функции спавна переходов можно так же задать направление взгяда актёра, когда он оказывается в точке после перехода. Это задаётся в строке: vector():set( 0.0,2.0,0.0 ),"l07_military",0) Имеет значение только вторая координата. Она задаёт как раз ориентацию на местности. Показатели очень просты: 0.0 (по умолчанию) это смотреть на север. -1.5 - это 45 градусов вправо (на восток). 3.0 - это 90 градусов (смотреть будет строго на юг). 1.5 - это 45 градусов влево (на запад). 7) Если нужно удалить какой-нибудь переход, то в том же файле вписывем функцию, где указываем стори_айди перехода, который нужно удалить. Пример:
function delete_radar_mg() local sim = alife() local se_obj = sim:story_object(20044) if se_obj then sim:release(se_obj, true) end local actor = db.actor end
Всё. Вызываем данную функцию из удобного для Вас места (диалог или инфопоршень) и переход исчезнет.
Скриптовый метод спавна спейс_рестриктора (space_restrictor) и работа с ним.
При создании сюжетного мода, обойтись без использования спейс_рестрикторов, практически невозможно. Масса сюжетных действий актёра и НПС происходят именно при помощи рестрикторов. раньше, создание рестрикторов было возможно только в аллспавне. А это, как известно, всегда новая игра и возможность гибкого написания сюжета весьма затруднена этим. Теперь есть возможность скриптовым методом создавать спейс_рестрикторы в любой удобный момент. 1) Создаём в папке \gamedata\scripts скриптовый файл с любым именем, например, my_restrictor.script и вписываем в него следующее:
---------- -- чтение формы из нет-пакета ---------- function r_shape(packet) local s local st = {} st.count = packet:r_u8() st.shapes = {} for i=1, st.count do s = {} s.type = packet:r_u8() if s.type == 0 then s.center = packet:r_vec3() s.radius = packet:r_float() else s.axis_x = packet:r_vec3() s.axis_y = packet:r_vec3() s.axis_z = packet:r_vec3() s.offset = packet:r_vec3() end st.shapes[i] = s end return st end
---------- -- запись формы в нет-пакет ---------- function w_shape(packet, st) local s packet:w_u8(st.count) for i=1, st.count do s = st.shapes[i] packet:w_u8(s.type) if s.type == 0 then packet:w_vec3(s.center) packet:w_float(s.radius) else packet:w_vec3(s.axis_x) packet:w_vec3(s.axis_y) packet:w_v ec3(s.axis_z) packet:w_vec3(s.offset) end end end
---------- -- перепаковка нет-пакета созданного скриптом рестрикта ---------- function rewrite_restrictor(se_obj, custom, radius) local packet = net_packet() se_obj:STATE_Write(packet) local game_vertex_id = packet:r_u16() local distance = packet:r_float() local direct_control = packet:r_s32() local level_vertex_id = packet:r_s32() local object_flags = packet:r_s32() local custom_data = packet:r_stringZ() local story_id = packet:r_s32() local spawn_story_id = packet:r_s32() local shape = r_shape(packet) local restrictor_type = packet:r_u8()
2) Далее, в этом же файле (в самом конце файла) будем создавать функции спавна нужных нам спейс_рестрикторов, с последующим вызовом этих функций откуда угодно. Пример номер один. Спавним рестриктор, в котором, при попадании в него актёра, должно произойти какое-то событие. Для примера взят рестриктор, созданный в Х-16, при попадании в который, спавнятся пауки.
function create_x16_restr() local se_obj = alife():create("space_restrictor",vector():set(-66.073,-19.088,-158.844),15672,1490) local custom = "[logic]\ncfg = scripts\\new\\restr\\x16_restr.ltx" --путь к логике рестриктора rewrite_restrictor(se_obj, custom, 5.0) --5.0 - это радиус действия нашего рестриктора end
Функция готова. Вызываем её в нужный нам момент, прописав в диалог или по какому-либо условию. Далее, создаём ltx файл логики рестриктора, с указанным именем и по указанному в функции пути и пишем туда следующее:
В логике, при наличии инфопоршня potapov_first_dialog_start, вызывается функция спавна паука из скриптового файла: esc_graf.script При установленном nil, инфопоршень сработает только один раз. Точно так же, по аналогии, вместо активации функции, можно выдать просто какой-угодно инфопоршень для нужных вам целей. В этом случае в логике рестриктора будет немного другая строка:
В данном примере логики рестриктора, при попадании в него актёра выдаётся два инфопоршня, звучит музыкальный фрагмент и включается следующая схема работы рестриктора, в которой: 1) Если актёр внутри рестриктора и находится там 5 секунд и есть нужный инфопоршень, то выдаётся ещё один нужный инфопоршень и активируется какая-нибудь нужная функция. 2) Если актёр до истечения 5 секунд покинул рестриктор, то удаляется ранее выданный инфопоршень. Данный пример - это работа спейс_рестриктора в ТТ2 в саомом конце, когда формируются две разные концовки: замешкался - одна концовка, если вышел из рестриктора, то другая концовка ждёт актёра.
Далее. Другой вариант спавна спейс_рестриктора. В спейс_рестриктор должен попасть какой-нибудь нужный нам НПС. Функция спавна рестриктора в этом случае будет выглядеть несколько иначе:
function create_lesnik_restr() local sr = alife():create("space_restrictor",vector():set(136.250,1.228,-123.958),475965,3085) local custom = [[[logic] active = sr_idle [sr_idle] on_npc_in_zone = 30159|]]..sr:name()..[[|nil %+lesnik_mesto%]] rewrite_restrictor(sr, custom, 1.0) end
--Обратите внимание, что здесь в рестриктор вписывается стори_айди нашего НПС. В данном случае это Лесник с номером story_id = 30159. И при попадании в рестриктор с радиусом = 1 метр, выдаётся инфопоршень lesnik_mesto. Далее всё по аналогии. Можно активировать функции, вставлять музыкальные фрагменты и т.д. Создавать отдельный файл с логикой рестриктора в этом случае не надо. Всё прописывается в самой функции спавна рестриктора.