Здравствуйте, гость ( Вход | Регистрация )

> UO Inferno
nullptr
сообщение 24.3.2025, 19:30
Сообщение #1


***

Novice
Сообщений: 68
Регистрация: 13.3.2021
Группа: Пользователи
Наличность: 68
Пользователь №: 19.916



Привет всем фанатам старой школы Ultima Online! Этот сервер - попытка воссоздать атмосферу моего детства в 2000-х. Помните разделение на "Сферу" с магическим PvP и "Полы" (Inferno, Dragon World, Utopia и др.) с классическим? Сейчас это хардкорный T2A без рас и классов, построенный на последнем OSI эмуляторе. Добавлены OSI управление, аукцион и другие фишки последних клиентов.

Сайт https://playuo.ru/

Аукцион
(IMG:https://i124.fastpic.org/big/2025/0324/2e/67c2da2bcec6c59ef61d6316b22db02e.png)

Гильдии
(IMG:https://i124.fastpic.org/big/2025/0324/ae/71fc1085db30d199a36ec443b88153ae.png)


Запись ежедневных замесов
[+]


Последние 4 года я провёл над разработкой сервера, и мне удалось создать рабочее ядро T2A, а также написать несколько автоматических турниров. Однако со временем меня стало сильно удручать отсутствие контекста за пределами стандартной "песочницы" UO: добычи ресурсов, крафта и прочего. Мне захотелось оживить PvP-сервер, добавив квесты в духе старой школы, наподобие DxD и других классических проектов.

Последние 4 месяца я посвятил изучению геймдева, сосредоточившись на инструментах для создания диалогов в играх с особым упором на искусственный интеллект. Ведь, создавая сервер в одиночку, это, по сути, единственный реалистичный способ наполнить мир контентом. К моему удивлению, в таких популярных движках, как Unity и других, абсолютно отсутствуют готовые инструменты для создания разветвленных диалогов и скриптов, управляемых ИИ.

Поэтому мне пришлось "костылить" своё собственное решение. Основной упор я сделал на генерацию диалогов в читаемом формате, а также на то, чтобы ветвление и логика квестов могли работать в режиме реального времени, без необходимости перекомпиляции основного сервера. И сейчас, кажется, мне удалось создать удобный формат, который может генерировать практически любой современный ИИ. Благодаря тому, что вся информация передается в контексте, это кратно снижает вероятность ошибок со стороны ИИ при создании диалогов, скриптов и прочего контента.

Example диалог.
(IMG:https://i124.fastpic.org/big/2025/0324/9b/3eb64748dcc469054dfd3017048af89b.png)

Работает это всё на Lua в одном инстансе, который хранит ссылки на виртуальные методы для обработки логики прямо из диалога. Также опционально можно использовать LINQ для выполнения более сложных выборок данных. Поскольку сервер работает в однопоточном режиме, у меня не возникло желания писать дополнительную синхронизацию для квестового потока.

Как только появится немного свободного времени, я планирую написать небольшое обучающее задание, которое познакомит новых игроков с особенностями игры на сервере
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
 
Ответить в эту темуОткрыть новую тему
Ответов
olduo.com
сообщение 24.11.2025, 14:19
Сообщение #2


******

Expert
Сообщений: 520
Регистрация: 31.7.2016
Группа: Пользователи
Наличность: 293
Пользователь №: 18.060



в целом мне на всё ответили, в гильду вроде взяли. между гильдиями какаято движуха говорят часто есть.
про ДС впервые слышу чтобы людей было больше в телеге так как везде где бываю большинство используют zapret и продолжают пользоватся ДС

если ИП меняется, тыб DNS запись типа А сделал бы и всё
у людей типа uo.playuo.ru а ИП если что там меняеш в DNS записи


--------------------
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
nullptr
сообщение 25.11.2025, 22:55
Сообщение #3


***

Novice
Сообщений: 68
Регистрация: 13.3.2021
Группа: Пользователи
Наличность: 68
Пользователь №: 19.916



Цитата(olduo.com @ 24.11.2025, 16:19) *

в целом мне на всё ответили, в гильду вроде взяли. между гильдиями какаято движуха говорят часто есть.
про ДС впервые слышу чтобы людей было больше в телеге так как везде где бываю большинство используют zapret и продолжают пользоватся ДС

если ИП меняется, тыб DNS запись типа А сделал бы и всё
у людей типа uo.playuo.ru а ИП если что там меняеш в DNS записи
Спасибо, полезу сделаю.

Цитата(Legnes @ 25.11.2025, 19:05) *

Маркетплейс удобно сделан.
А Time в маркетплейсе показывает сколько лежит предмет? Или это время сколько он будет продаваться?
Сколько времени осталось продаваться.
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
nullptr
сообщение 5.3.2026, 21:58
Сообщение #4


***

Novice
Сообщений: 68
Регистрация: 13.3.2021
Группа: Пользователи
Наличность: 68
Пользователь №: 19.916



(IMG:https://i127.fastpic.org/big/2026/0305/07/2a97f848a935bfd5d5eb9618c30db707.jpg)

Запись турнира.
https://rutube.ru/video/b99e4ff589efe8d3cbae3996b07bf6c8/
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
nullptr
сообщение 2.9.2026, 15:28
Сообщение #5


***

Novice
Сообщений: 68
Регистрация: 13.3.2021
Группа: Пользователи
Наличность: 68
Пользователь №: 19.916



Очень сильно надоело пилить контент на стороне сервере.

Встроил в ядро Lua с горячей перезагрузкой. Теперь можно буквально генерировать через GPT практически любые активности и квесты.


(IMG:https://i128.fastpic.org/big/2026/0902/6b/d4a62619c9ff373dc3827a5093ac336b.png)
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
olduo.com
сообщение 2.9.2026, 17:27
Сообщение #6


******

Expert
Сообщений: 520
Регистрация: 31.7.2016
Группа: Пользователи
Наличность: 293
Пользователь №: 18.060



Цитата(nullptr @ 2.9.2026, 15:28) *

Очень сильно надоело пилить контент на стороне сервере.

Встроил в ядро Lua с горячей перезагрузкой. Теперь можно буквально генерировать через GPT практически любые активности и квесты.
(IMG:https://i128.fastpic.org/big/2026/0902/6b/d4a62619c9ff373dc3827a5093ac336b.png)

А можно попадробнее? Рисуется какаято фантасмагория. Чатжпт сам кодит на луа чтоле и вываливает баги в прадакшен прямо на лайф? Картинка какогото рейтинга турнира только увеличивает вопросы.

И что за движок такой без горячей перезагрузки скрпатов?


--------------------
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
nullptr
сообщение 2.9.2026, 18:39
Сообщение #7


***

Novice
Сообщений: 68
Регистрация: 13.3.2021
Группа: Пользователи
Наличность: 68
Пользователь №: 19.916



Цитата(olduo.com @ 2.9.2026, 19:27) *

А можно попадробнее? Рисуется какаято фантасмагория. Чатжпт сам кодит на луа чтоле и вываливает баги в прадакшен прямо на лайф? Картинка какогото рейтинга турнира только увеличивает вопросы.

И что за движок такой без горячей перезагрузки скрпатов?


Человек через GPT буквально пишет квесты, турниры и другие активности.

Из последнего.
Систама PvP-рейтинга, голосование за турниры, около восьми массовых турниров и какой-то огромный детективный квест, где нужно искать преступников.

ИИ начинает лажать, когда ему дают слишком большой контекст. Поэтому решил ограничить его YAML-описанием с набором событий и Lua-вызовов — по сути, YAML задаёт рамки и не даёт GPT слишком сильно косячить.

Внутри YAML можно делать вставки Lua-кода, которые затем проверяются через серверный gump. Оттуда же можно отдельно вызвать нужный метод или событие и посмотреть результат.

Алгоритм простой: человек кидает в GPT пример квеста и описание API, получает готовый код, загружает его через FTP, проверяет через серверный gump скриптов — и так поэтапно собирает всю логику.

И как минимум скриптер, не имеет доступа к исходникам сервереа.

Вот пример как это выглядит.
Код
Handlers:
  OnStart: |
    function()
      BR_Serial = 0x400AF1ED

      BR_TZOffsetMin = 300
      BR_Weekday     = 7
      BR_Hour        = 22
      BR_Minute      = 45
      BR_GatherSeconds = 1200
      BR_CatchUpMinutes = 5
      BR_MinPlayers    = 6

      BR_Map = "Felucca"

      BR_EntryTeleporter = { 0x4007A755, 0x40072289 }
      BR_ExitTeleporter = { 0x4007FDEC }
      BR_ExitGate = { 0x4007FDEE }
      BR_ExitHue = 1150
      BR_ExitName = "Выход"
      BR_ExitGhost = false
      BR_GateItems = { 0x4007A3B3, 0x400AE045 }
      BR_GateHue = 1300
      BR_GateName = "Королевская битва"
      BR_Doors = {}

      BR_LobbyRect = { x1 = 6086, y1 = 1131, x2 = 6103, y2 = 1137, z = 0 }
      BR_ArenaRect = { x1 = 6085, y1 = 1107, x2 = 6103, y2 = 1125, z = 0 }

      BR_OutsideX = 6083
      BR_OutsideY = 1129
      BR_OutsideZ = 0

      BR_HeraldX = 6094; BR_HeraldY = 1130; BR_HeraldZ = 10
      BR_RefX    = 6094; BR_RefY    = 1116; BR_RefZ    = 0

      BR_BriefingSeconds  = 25
      BR_CountdownSeconds = 60
      BR_CountdownTail    = 10
      BR_ArmDelaySeconds  = 5
      BR_LootSeconds      = 300
      BR_AwardSeconds     = 15
      BR_CloseSeconds     = 60

      BR_AwolGraceSeconds = 45

      BR_MonsterSwapSeconds = 20
      BR_MonsterAnnounce    = false
      BR_MonsterSpawnGap    = 2

      BR_SpawnerSerial = 0x400CA182

      BR_Monsters = {
        { Type = "OrcishLord",            Props = "Pack CrypthonOrc AI AI_HighPredator Name \"a Crypthon Orc\" Hue 1184" },
        { Type = "BloodElemental",        Props = "Pack CrypthonElemental AI AI_HighPredator Name \"a Crypthon Elemental\" Hue 1184" },
        { Type = "Daemon",                Props = "Pack CrypthonDaemon AI AI_HighPredator Hue 1184 Name \"a Crypthon Daemon\"" },
        { Type = "LichLord",              Props = "Pack CrypthonLich AI AI_HighPredator Name \"a Crypthon Lich\" Hue 1184" },
        { Type = "DreadSpider",           Props = "Pack CrypthonBeetle Body 0xF4 BaseSoundID 1255 AI AI_HighPredator Hue 1184 Name \"a Crypthon Beetle\"" },
        { Type = "Dragon",                Props = "Pack CrypthonDragon AI AI_HighPredator Name \"a Crypthon Dragon\" Hue 1184" },
        { Type = "Ogre",                  Props = "Pack CrypthonOgre AI AI_HighPredator Name \"a Crypthon Ogre\" Hue 1184" },
        { Type = "Balron",                Props = "Pack Balron AI AI_HighPredator" },
        { Type = "AncientWyrm",           Props = "Pack InfernoWyrm AI AI_LowBoss Name \"a Inferno Wyrm\" Hue 2180" },
        { Type = "Moloch",                Props = "Pack InfernoMoloch Hue 2180 Name \"a Inferno Moloch\" AI AI_LowBoss" },
      }

      BR_MonsterProps = "FightMode Closest RangePerception 24 SeeksHome True Tamable False"

      BR_CombatantProp   = "Combatant"
      BR_CombatantHex    = true

      BR_MonsterAggroProps = "ReacquireOnMovement True"

      BR_ScatterAttempts = 40
      BR_ScatterZSearch  = 0
      BR_MinSpawnGap     = 6

      BR_FreezeOn   = "Frozen True"
      BR_FreezeOff  = "Frozen False"
      BR_HideOn     = "Hidden True"
      BR_HideOff    = "Hidden False"
      BR_SquelchOn  = "Squelched True"
      BR_SquelchOff = "Squelched False"

      BR_BcHue = 0x0035
      BR_PreAnnounceMin    = 30
      BR_GatherAnnounceMin = 5

      BR_RegionSerial = 0x4003BC07
      BR_RegionOn     = "Active True"
      BR_RegionOff    = "Active False"
      BR_RegionRules = {
        "AttackAllowed True",
        "MurdersAllowed False",
        "Guarded False",
        "DisableLoot False",
        "DisableDrop False",
        "DisableTrade True",
        "MountsAllowed False",
        "TravelFrom False",
        "TravelTo False",
        "CanMark False",
        "CastAllowed True",
        "CorpseDecay False",
        "ResurrectionAllowed False",
        "LogoutAllowed False",
        "HouseAllowed False",
        "ItemsBreak False",
        "PoisonLevelAllowed 4",
        "Priority 75",
        "ShowRegion False",
        "Name \"Battle Royale\"",
        "ExcludeSpell \"Mark, Recall, Gate Travel, Sacred Journey\"",
        "EnterMessage \"Арена не выпускает живых. Приготовьтесь к сражению!\"",
      }

      BR_RulesLines = {
        { Text = "Приветствую на Королевской битве!",                                      Pause = 2500 },
        { Text = "Сейчас всех вас разбросает по арене - врозь, скрытыми и недвижимыми.",   Pause = 4000 },
        { Text = "Как только отсчёт кончится, чары спадут и на арену выйдет зверь.",       Pause = 4000 },
        { Text = "Зверь меняется каждые двадцать секунд и не щадит никого.",               Pause = 3500 },
        { Text = "Бьёте друг друга, уворачиваетесь от зверя - выжить должен один.",        Pause = 3500 },
        { Text = "Последний выживший - чемпион, ему пять минут на трупы и редкая роба.",   Pause = 4000 },
        { Text = "Роба привязана к нему и служит неделю. Удачи, бойцы!",                   Pause = 3000 },
      }

      BR_Robe = { Class = "Robe", ItemId = 0, Hue = 1300 }
      BR_RobeDays = 7
      BR_RobeTitle = "Королевская битва"
      BR_RobeTemplate = "{Winner} - {Title} {Date}"
      BR_ChampReturn = { x = 0, y = 0, z = 0 }
      BR_RobeDropDX  = 0
      BR_RobeDropDY  = 1

      BR_ConsolationItems = {
        { Class = "SilverCrystal", Props = "" },
        { Class = "BloodCrystal",  Props = "" },
        { Class = "ArmorCrystal",  Props = "" },
      }

      BR_IgnoredSerials = {
        0x00002298,
        0x00001899,
        0x000067BD,
        0x00002D7F,
        0x000002C7,
        0x00000001,
      }

      BR_DefGatherSeconds      = BR_GatherSeconds
      BR_DefCountdownSeconds   = BR_CountdownSeconds
      BR_DefLootSeconds        = BR_LootSeconds
      BR_DefMinPlayers         = BR_MinPlayers
      BR_DefMonsterSwapSeconds = BR_MonsterSwapSeconds

      BR_DescEn = ""
      BR_Phase    = "IDLE"
      BR_Deadline = ""
      BR_ArmAt    = ""
      BR_LastRunDate = ""
      BR_Roster   = {}
      BR_Elim     = {}
      BR_Champion = 0
      BR_Monster  = 0
      BR_MonsterIdx = 0
      BR_MonsterSwapAt = ""
      BR_MonsterTarget = 0
      BR_CountdownSpoken = false
      BR_AwolSince = {}
      BR_PreAnnDate = ""
      BR_GatherAnnLast = -1
      BR_TickCount = 0
      BR_LastError = ""

      BattleRoyale_Main.ComputeZones()
      BattleRoyale_Main.LoadState()
    end

  OnTimer: |
    function()
      BR_TickCount = (BR_TickCount or 0) + 1
      local ok, err = pcall(BattleRoyale_Main.Tick)
      if not ok then BR_LastError = tostring(err) end
    end

Methods:

  RulesTail: |
    function()
      if not BR_DescEn or BR_DescEn == "" then return "" end
      return " Rules: " .. BR_DescEn .. "."
    end

  Go: |
    function(player)
      BattleRoyale_Main.ResetState()
      BR_GatherSeconds      = 60
      BR_BriefingSeconds    = 10
      BR_CountdownSeconds   = 20
      BR_LootSeconds        = 60
      BR_MinPlayers         = 2
      BR_MonsterSwapSeconds = 20
      local now = Tournament_Utils.ParseDT(CreateDT(0, BR_TZOffsetMin))
      local today = string.format("%02d.%02d.%04d", now.day, now.month, now.year)
      BattleRoyale_Main.OpenGather(today)
      Say(player, "BattleRoyale: fast mode, gather open 60s. Stand in the LOBBY.")
    end

  Stat: |
    function(player)
      Say(player, "BattleRoyale: phase=" .. BR_Phase
        .. " roster=" .. #BR_Roster .. " elim=" .. #BR_Elim
        .. " ticks=" .. tostring(BR_TickCount))
      Say(player, "  champion=" .. tostring(BR_Champion)
        .. " monster=" .. tostring(BR_Monster)
        .. " (" .. tostring(BR_MonsterIdx > 0 and BR_Monsters[BR_MonsterIdx].Type or "-") .. ")"
        .. " swapIn=" .. tostring(BattleRoyale_Main.LeftSec(BR_MonsterSwapAt))
        .. " target=" .. tostring(BR_MonsterTarget))
      Say(player, "  " .. BattleRoyale_Main.SchedText())
      if BR_LastError ~= "" then
        Say(player, "BattleRoyale: LAST TICK ERROR: " .. BR_LastError)
      end
    end

  SchedText: |
    function()
      local raw = CreateDT(0, BR_TZOffsetMin)
      local now = Tournament_Utils.ParseDT(raw)
      if not now then
        return "now=" .. tostring(raw) .. " PARSE FAILED (check CreateDT/date format)"
      end
      local last = tostring(BR_LastRunDate)
      if last == "" then last = "(none)" end
      return "now=" .. tostring(raw)
        .. " weekday=" .. Tournament_Utils.Weekday(now.year, now.month, now.day)
        .. " | need weekday=" .. BR_Weekday
        .. " at " .. string.format("%02d:%02d", BR_Hour, BR_Minute)
        .. " | today=" .. string.format("%02d.%02d.%04d", now.day, now.month, now.year)
        .. " lastRun=" .. last
    end

  Arm: |
    function(player)
      Say(player, "BattleRoyale: was phase=" .. BR_Phase .. " | " .. BattleRoyale_Main.SchedText())
      BattleRoyale_Main.ResetState()
      Say(player, "BattleRoyale: ARMED - phase IDLE, lastRun cleared, waiting for schedule.")
    end

  Skip: |
    function(player)
      BR_Deadline = 1
      BR_ArmAt    = 1
      Say(player, "BattleRoyale: phase " .. BR_Phase .. " forced to end.")
    end

  Beast: |
    function(player)
      BattleRoyale_Main.SwapMonster()
      Say(player, "BattleRoyale: monster=" .. tostring(BR_Monster)
        .. " type=" .. tostring(BR_MonsterIdx > 0 and BR_Monsters[BR_MonsterIdx].Type or "-"))
    end

  ComputeZones: |
    function()
      BR_Lobby = Tournament_Utils.ZoneFromRect(BR_LobbyRect)
      BR_Arena = Tournament_Utils.ZoneFromRect(BR_ArenaRect)
      if BR_RefX == 0 and BR_RefY == 0 then
        BR_RefX = BR_Arena.cx; BR_RefY = BR_Arena.cy; BR_RefZ = BR_Arena.z
      end
    end

  SpeechSeconds: |
    function(lines)
      local ms = 0
      for i, l in ipairs(lines) do ms = ms + (l.Pause or 0) end
      return math.ceil(ms / 1000)
    end

  LeftSec: |
    function(deadline)
      local d = tonumber(deadline)
      if not d then return "-" end
      local left = d - Tournament_Utils.NowSec(BR_TZOffsetMin)
      if left < 0 then left = 0 end
      return left
    end

  SerialOf: |
    function(x)
      if type(x) == "table" then return tonumber(x.Serial) end
      return tonumber(x)
    end

  IsIgnored: |
    function(serial)
      for i, s in ipairs(BR_IgnoredSerials) do
        if s == serial then return true end
      end
      return false
    end

  InRoster: |
    function(serial)
      for i, s in ipairs(BR_Roster) do
        if s == serial then return true end
      end
      return false
    end

  RemoveFromRoster: |
    function(serial)
      for i = #BR_Roster, 1, -1 do
        if BR_Roster[i] == serial then table.remove(BR_Roster, i) end
      end
    end

  PlayerName: |
    function(serial)
      if not serial or serial == 0 then return "Unknown" end
      local res, val = GetProps(serial, "Name")
      local n = tostring(val or "")
      n = n:gsub('^"', ''):gsub('"$', '')
      if n == "" then n = "Unknown" end
      return n
    end

  TodayStr: |
    function()
      local now = Tournament_Utils.ParseDT(CreateDT(0, BR_TZOffsetMin))
      if not now then return "??.??.????" end
      return string.format("%02d.%02d.%04d", now.day, now.month, now.year)
    end

  Freeze: |
    function(serial)
      if serial and serial ~= 0 and BR_FreezeOn ~= "" then SetProps(serial, BR_FreezeOn) end
    end

  Unfreeze: |
    function(serial)
      if serial and serial ~= 0 and BR_FreezeOff ~= "" then SetProps(serial, BR_FreezeOff) end
    end

  Hide: |
    function(serial)
      if serial and serial ~= 0 and BR_HideOn ~= "" then SetProps(serial, BR_HideOn) end
    end

  Unhide: |
    function(serial)
      if serial and serial ~= 0 and BR_HideOff ~= "" then SetProps(serial, BR_HideOff) end
    end

  Squelch: |
    function(serial)
      if serial and serial ~= 0 and BR_SquelchOn ~= "" then SetProps(serial, BR_SquelchOn) end
    end

  Unsquelch: |
    function(serial)
      if serial and serial ~= 0 and BR_SquelchOff ~= "" then SetProps(serial, BR_SquelchOff) end
    end

  Spellbind: |
    function(serial)
      BattleRoyale_Main.Hide(serial)
      BattleRoyale_Main.Squelch(serial)
      BattleRoyale_Main.Freeze(serial)
    end

  Release: |
    function(serial)
      BattleRoyale_Main.Unfreeze(serial)
      BattleRoyale_Main.Unsquelch(serial)
      BattleRoyale_Main.Unhide(serial)
    end

  SaveState: |
    function()
      local head = table.concat({
        BR_Phase,
        Tournament_Utils.Pack(BR_Deadline),
        Tournament_Utils.Pack(BR_ArmAt),
        Tournament_Utils.Pack(BR_LastRunDate),
        tostring(BR_Champion),
        tostring(BR_Monster),
        tostring(BR_MonsterIdx),
        Tournament_Utils.Pack(BR_MonsterSwapAt),
      }, "^")
      local body = table.concat({
        Tournament_Utils.JoinSerials(BR_Roster),
        Tournament_Utils.JoinSerials(BR_Elim),
      }, "~~~")
      if BR_Serial and BR_Serial ~= 0 then
        local data = "|" .. head .. "~~~" .. body
        .. "~~~B=" .. tostring(BR_TickCount or 0)
        .. "~~~T=" .. tostring(BR_MinPlayers or 0) .. "|"
      SetProps(BR_Serial, "Data " .. data)
      BR_DataSeen = data
      end
    end

  LoadState: |
    function()
      if not BR_Serial or BR_Serial == 0 then return end
      local res, val = GetProps(BR_Serial, "Data")
      if not val or tostring(val) == "" then return end
      local raw = tostring(val):match("^|(.*)|$") or tostring(val)
      local parts = {}
      for part in (raw .. "~~~"):gmatch("(.-)~~~") do table.insert(parts, part) end
      BR_DataSeen = tostring(val)
      for i, pt in ipairs(parts) do
        if string.sub(pt, 1, 2) == "T=" then
          local mn = tonumber(string.match(pt, "T=(%d+)"))
          if mn and mn > 0 then BR_MinPlayers = mn end
        end
      end
      if #parts < 3 then return end
      local h = {}
      for f in (parts[1] .. "^"):gmatch("(.-)%^") do table.insert(h, f) end
      if #h < 8 then return end
      BR_Phase      = h[1]
      BR_Deadline   = h[2]
      BR_ArmAt      = h[3]
      BR_LastRunDate = h[4]
      BR_Champion   = tonumber(h[5]) or 0
      BR_Monster    = tonumber(h[6]) or 0
      BR_MonsterIdx = tonumber(h[7]) or 0
      BR_MonsterSwapAt = h[8]
      BR_Roster = Tournament_Utils.SplitSerials(parts[2])
      BR_Elim   = Tournament_Utils.SplitSerials(parts[3])
    end

  ExitOpen: |
    function(on)
      if on then
        pcall(function()
          Tournament_Utils.GateSet(BR_ExitTeleporter,
            BR_OutsideX, BR_OutsideY, BR_OutsideZ,
            BR_ExitHue, BR_ExitName, BR_ExitGate)
        end)
      end
      pcall(function()
        Tournament_Utils.GhostMode(BR_ExitTeleporter, on and BR_ExitGhost)
      end)
      pcall(function()
        Tournament_Utils.SetTeleporter(BR_ExitTeleporter, on and true or false)
      end)
    end

  ResetState: |
    function()
      BattleRoyale_Main.ExitOpen(false)
      pcall(function() Tournament_Utils.LadderMode(BR_EntryTeleporter, false) end)
      Tournament_Utils.SetTeleporter(BR_EntryTeleporter, false)
      Tournament_Utils.SetDoors(BR_Doors, false)
      BattleRoyale_Main.SetRegion(false)
      BattleRoyale_Main.RemoveMonster()
      for i, s in ipairs(BR_Roster) do
        BattleRoyale_Main.Release(s)
      end
      BR_Phase = "IDLE"
      BR_Deadline = ""; BR_ArmAt = ""; BR_LastRunDate = ""
      BR_Roster = {}; BR_Elim = {}
      BR_Champion = 0
      BR_MonsterIdx = 0; BR_MonsterSwapAt = ""
      BR_CountdownSpoken = false
      BR_AwolSince = {}
      BR_PreAnnDate = ""; BR_GatherAnnLast = -1
      BR_GatherSeconds      = BR_DefGatherSeconds
      BR_CountdownSeconds   = BR_DefCountdownSeconds
      BR_LootSeconds        = BR_DefLootSeconds
      BR_MinPlayers         = BR_DefMinPlayers
      BR_MonsterSwapSeconds = BR_DefMonsterSwapSeconds
      BattleRoyale_Main.SaveState()
    end

  Tick: |
    function()
      if     BR_Phase == "IDLE"      then BattleRoyale_Main.TickIdle()
      elseif BR_Phase == "GATHER"    then BattleRoyale_Main.TickGather()
      elseif BR_Phase == "BRIEFING"  then BattleRoyale_Main.TickBriefing()
      elseif BR_Phase == "COUNTDOWN" then BattleRoyale_Main.TickCountdown()
      elseif BR_Phase == "RUNNING"   then BattleRoyale_Main.TickRunning()
      elseif BR_Phase == "LOOT"      then BattleRoyale_Main.TickLoot()
      elseif BR_Phase == "AWARD"     then BattleRoyale_Main.TickAward()
      elseif BR_Phase == "CLOSING"   then BattleRoyale_Main.TickClosing()
      end
    end

  TickIdle: |
    function()
      local now = Tournament_Utils.ParseDT(CreateDT(0, BR_TZOffsetMin))
      if not now then return end
      if Tournament_Utils.Weekday(now.year, now.month, now.day) ~= BR_Weekday then return end
      local today = string.format("%02d.%02d.%04d", now.day, now.month, now.year)
      local mod    = now.hour * 60 + now.min
      local target = BR_Hour * 60 + (BR_Minute or 0)

      if mod >= target - BR_PreAnnounceMin and mod < target then
        if BR_PreAnnDate ~= today and BR_LastRunDate ~= today then
          BR_PreAnnDate = today
          BroadcastMessage("Battle Royale starts in " .. (target - mod) .. " minutes!" .. BattleRoyale_Main.RulesTail(), BR_BcHue)
        end
        return
      end

      if mod < target or mod >= target + (BR_CatchUpMinutes or 5) then return end
      if BR_LastRunDate == today then return end
      BattleRoyale_Main.OpenGather(today)
    end

  OpenGather: |
    function(today)
      BattleRoyale_Main.ExitOpen(false)
      pcall(function() Tournament_Utils.LadderMode(BR_EntryTeleporter, false) end)
      pcall(function() Tournament_Utils.SetVendors(true, BR_Map) end)
      pcall(function() Tournament_Utils.ArenaFor("ffa") end)
      pcall(function() Tournament_Utils.GateSet(BR_EntryTeleporter, 6093, 1133, 0,
          BR_GateHue, BR_GateName, BR_GateItems) end)
      BR_LastRunDate = today
      BR_Phase    = "GATHER"
      BR_Deadline = Tournament_Utils.FutureSec(BR_TZOffsetMin, BR_GatherSeconds)
      BR_Roster = {}; BR_Elim = {}
      BR_Champion = 0
      BR_ArmAt = ""
      BR_CountdownSpoken = false
      BR_AwolSince = {}
      BR_GatherAnnLast = math.floor(BR_GatherSeconds / (BR_GatherAnnounceMin * 60))
      Tournament_Utils.SetDoors(BR_Doors, true)
      Tournament_Utils.SetTeleporter(BR_EntryTeleporter, true)
      BroadcastMessage("Battle Royale is OPEN! " .. math.ceil(BR_GatherSeconds / 60)
        .. " min to join through the gate." .. BattleRoyale_Main.RulesTail(), BR_BcHue)
      BattleRoyale_Main.SaveState()
    end

  TickGather: |
    function()
      BattleRoyale_Main.CollectWaiting()

      local left = tonumber(BR_Deadline) - Tournament_Utils.NowSec(BR_TZOffsetMin)
      if left > 0 then
        local bucket = math.floor(left / (BR_GatherAnnounceMin * 60))
        if bucket < BR_GatherAnnLast then
          BR_GatherAnnLast = bucket
          BroadcastMessage("Battle Royale: " .. math.ceil(left / 60) .. " min left to join!", BR_BcHue)
        end
      end

      if not Tournament_Utils.Expired(BR_TZOffsetMin, BR_Deadline) then return end

      Tournament_Utils.SetTeleporter(BR_EntryTeleporter, false)
      Tournament_Utils.SetDoors(BR_Doors, false)

      local present = {}
      local ppl = Tournament_Utils.PlayersIn(BR_Map, BR_Lobby)
      for i, p in ipairs(ppl) do
        if p.Serial and BattleRoyale_Main.InRoster(p.Serial) then
          table.insert(present, p.Serial)
        end
      end
      BR_Roster = present

      if #BR_Roster < BR_MinPlayers then
        BroadcastMessage("Battle Royale cancelled: only " .. #BR_Roster
          .. " fighters, need " .. BR_MinPlayers .. ".", BR_BcHue)
        BattleRoyale_Main.StartClosing()
        return
      end

      BattleRoyale_Main.GrantVoteRights()

      Tournament_Utils.MasterSay(BR_HeraldX, BR_HeraldY, BR_HeraldZ, BR_Map, BR_RulesLines)
      BR_Phase = "BRIEFING"
      local briefSec = math.max(BR_BriefingSeconds,
        BattleRoyale_Main.SpeechSeconds(BR_RulesLines) + 4)
      BR_Deadline = Tournament_Utils.FutureSec(BR_TZOffsetMin, briefSec)
      BroadcastMessage("Battle Royale: gate closed, " .. #BR_Roster
        .. " fighters, rules briefing...", BR_BcHue)
      BattleRoyale_Main.SaveState()
    end

  CollectWaiting: |
    function()
      local ppl = Tournament_Utils.PlayersIn(BR_Map, BR_Lobby)
      for i, p in ipairs(ppl) do
        if p.Serial and not BattleRoyale_Main.IsIgnored(p.Serial) then
          if not Tournament_Utils.IsAlive(p) then Resurrect(p.Serial) end
          if not BattleRoyale_Main.InRoster(p.Serial) then
            table.insert(BR_Roster, p.Serial)
          end
        end
      end
    end

  GrantVoteRights: |
    function()
      if not TournamentVote_Main then return end
      local ok, err = pcall(TournamentVote_Main.GrantVoteRights, BR_Roster, true)
      if not ok then BR_LastError = "GrantVoteRights: " .. tostring(err) end
    end

  TickBriefing: |
    function()
      if not Tournament_Utils.Expired(BR_TZOffsetMin, BR_Deadline) then return end

      BattleRoyale_Main.SetRegion(true)
      BattleRoyale_Main.ScatterRoster()

      BR_Phase    = "COUNTDOWN"
      BR_Deadline = Tournament_Utils.FutureSec(BR_TZOffsetMin, BR_CountdownSeconds)
      BR_CountdownSpoken = false

      Tournament_Utils.MasterSay(BR_RefX, BR_RefY, BR_RefZ, BR_Map, {
        { Text = "Все на арене. Чары держат вас до начала боя.", Pause = 3500 },
        { Text = "До схватки " .. Tournament_Utils.SpanText(BR_CountdownSeconds)
          .. " - готовьте макросы и зелья.", Pause = 4000 },
      })

      BroadcastMessage("Battle Royale: fighters are on the arena, "
        .. Tournament_Utils.SpanTextEn(BR_CountdownSeconds) .. " to the start!", BR_BcHue)
      BattleRoyale_Main.SaveState()
    end

  ScatterRoster: |
    function()
      local now = Tournament_Utils.ParseDT(CreateDT(0, BR_TZOffsetMin))
      local seed = 1
      if now then
        seed = (now.year * 1000000 + now.month * 10000 + now.day * 100 + now.hour) + now.min * 60 + now.sec
      end
      math.randomseed(seed % 2147483647)
      math.random(); math.random(); math.random()

      local used = {}
      for i, s in ipairs(BR_Roster) do
        local x, y, z = BattleRoyale_Main.ScatterPoint(used)
        Resurrect(s)
        MoveToWorld(s, x, y, z, BR_Map)
        BattleRoyale_Main.Spellbind(s)
        table.insert(used, { x = x, y = y })
      end
    end

  SpellbindAll: |
    function()
      for i, s in ipairs(BR_Roster) do
        BattleRoyale_Main.Spellbind(s)
      end
    end

  ScatterPoint: |
    function(used)
      local half = math.floor(BR_ScatterAttempts / 2)
      for attempt = 1, BR_ScatterAttempts do
        local x = math.random(BR_Arena.x1, BR_Arena.x2)
        local y = math.random(BR_Arena.y1, BR_Arena.y2)
        local z = BattleRoyale_Main.FindValidZ(x, y, BR_Arena.z)
        if z then
          if attempt > half or BattleRoyale_Main.FarEnough(used, x, y) then
            return x, y, z
          end
        end
      end
      return BR_Arena.cx, BR_Arena.cy, BR_Arena.z
    end

  FarEnough: |
    function(used, x, y)
      for i, p in ipairs(used) do
        local dx = p.x - x
        local dy = p.y - y
        if (dx * dx + dy * dy) < (BR_MinSpawnGap * BR_MinSpawnGap) then return false end
      end
      return true
    end

  FindValidZ: |
    function(x, y, baseZ)
      if CanSpawnMobile(BR_Map, x, y, baseZ) then return baseZ end
      for delta = 1, BR_ScatterZSearch do
        if CanSpawnMobile(BR_Map, x, y, baseZ + delta) then return baseZ + delta end
        if CanSpawnMobile(BR_Map, x, y, baseZ - delta) then return baseZ - delta end
      end
      return nil
    end

  TickCountdown: |
    function()
      local left = tonumber(BR_Deadline) - Tournament_Utils.NowSec(BR_TZOffsetMin)

      if left <= 0 then
        BattleRoyale_Main.BeginFight()
        return
      end

      BattleRoyale_Main.SpellbindAll()

      if left <= BR_CountdownTail and not BR_CountdownSpoken then
        BR_CountdownSpoken = true
        local lines = {}
        for nsec = math.min(left, BR_CountdownTail), 1, -1 do
          table.insert(lines, { Text = "Старт через: " .. nsec, Pause = 1000 })
        end
        table.insert(lines, { Text = "БОЙ! Зверь идёт!", Pause = 1500 })
        Tournament_Utils.MasterSay(BR_RefX, BR_RefY, BR_RefZ, BR_Map, lines)
      end
    end

  BeginFight: |
    function()
      for i, s in ipairs(BR_Roster) do
        BattleRoyale_Main.Release(s)
      end

      BR_Phase    = "RUNNING"
      BR_ArmAt    = Tournament_Utils.FutureSec(BR_TZOffsetMin, BR_ArmDelaySeconds)
      BR_Deadline = ""
      BR_AwolSince = {}

      BattleRoyale_Main.SwapMonster()

      BroadcastMessage("Battle Royale has begun! " .. #BR_Roster
        .. " fighters and the beast are on the arena.", BR_BcHue)
      BattleRoyale_Main.SaveState()
    end

  RemoveMonster: |
    function()
      if BR_Monster and BR_Monster ~= 0 then
        DeleteTimer(BR_Monster, 0)
        BR_Monster = 0
      end
      if BR_SpawnerSerial ~= 0 then
        BattleRoyale_Main.WipeArenaMonsters()
      end
      BR_MonsterTarget = 0
    end

  FindArenaMonsters: |
    function()
      local out = {}
      local objs = GetObjectsInRange(BR_Map, BR_Arena.cx, BR_Arena.cy, BR_Arena.z, BR_Arena.r, 1)
      if not objs then return out end
      for i, o in ipairs(objs) do
        local cls = string.lower(tostring(o.Class or ""))
        if o.Serial and o.Serial ~= BR_SpawnerSerial and cls ~= "refereenpc"
           and Tournament_Utils.InRect(BR_Arena, o.x, o.y) then
          table.insert(out, o.Serial)
        end
      end
      return out
    end

  WipeArenaMonsters: |
    function()
      for i, s in ipairs(BattleRoyale_Main.FindArenaMonsters()) do
        DeleteTimer(s, 0)
      end
    end

  AlivePlayersOnArena: |
    function()
      local out = {}
      local ppl = Tournament_Utils.PlayersIn(BR_Map, BR_Arena)
      for i, p in ipairs(ppl) do
        if p.Serial and Tournament_Utils.IsAlive(p) and BattleRoyale_Main.InRoster(p.Serial) then
          table.insert(out, p)
        end
      end
      return out
    end

  AliveSerialsOnArena: |
    function()
      local out = {}
      for i, p in ipairs(BattleRoyale_Main.AlivePlayersOnArena()) do
        table.insert(out, p.Serial)
      end
      return out
    end

  MonsterSpawnPoint: |
    function()
      local cand = BattleRoyale_Main.AlivePlayersOnArena()
      if #cand == 0 then return BattleRoyale_Main.ScatterPoint({}) end

      local p  = cand[math.random(1, #cand)]
      local px = tonumber(p.x) or BR_Arena.cx
      local py = tonumber(p.y) or BR_Arena.cy
      local pz = tonumber(p.z) or BR_Arena.z

      for r = BR_MonsterSpawnGap, BR_MonsterSpawnGap + 3 do
        for attempt = 1, 8 do
          local x = px + math.random(-r, r)
          local y = py + math.random(-r, r)
          if Tournament_Utils.InRect(BR_Arena, x, y) and CanSpawnMobile(BR_Map, x, y, pz) then
            return x, y, pz
          end
        end
      end

      return px, py, pz
    end

  ArmMonster: |
    function()
      if BR_Monster == 0 or BR_MonsterAggroProps == "" then return end
      pcall(SetProps, BR_Monster, BR_MonsterAggroProps)
    end

  AimMonster: |
    function(alive)
      if BR_Monster == 0 or BR_CombatantProp == "" then return end
      if not alive then alive = BattleRoyale_Main.AliveSerialsOnArena() end
      if #alive == 0 then
        BR_MonsterTarget = 0
        return
      end

      local keep = false
      for i, s in ipairs(alive) do
        if s == BR_MonsterTarget then keep = true end
      end
      if not keep then
        BR_MonsterTarget = alive[math.random(1, #alive)]
      end

      pcall(SetProps, BR_Monster,
        BR_CombatantProp .. " " .. BattleRoyale_Main.SerialText(BR_MonsterTarget))
    end

  SerialText: |
    function(serial)
      if BR_CombatantHex then return string.format("0x%X", serial) end
      return string.format("%d", serial)
    end

  SpawnFromSpawner: |
    function()
      Spawn(BR_SpawnerSerial)
      local found = BattleRoyale_Main.FindArenaMonsters()
      BR_Monster = found[1] or 0
    end

  SpawnByScript: |
    function()
      if not BR_Monsters or #BR_Monsters == 0 then return end

      local idx = math.random(1, #BR_Monsters)
      if #BR_Monsters > 1 then
        local guard = 0
        while idx == BR_MonsterIdx and guard < 10 do
          idx = math.random(1, #BR_Monsters)
          guard = guard + 1
        end
      end
      BR_MonsterIdx = idx
      local m = BR_Monsters[idx]

      local x, y, z = BattleRoyale_Main.MonsterSpawnPoint()
      local props = BR_MonsterProps
        .. " RangeHome " .. BR_Arena.r
        .. " Home (" .. x .. "," .. y .. "," .. z .. ")"
      if m.Props and m.Props ~= "" then props = props .. " " .. m.Props end

      BR_Monster = tonumber(CreateNPCAtLocation(m.Type, x, y, z, props)) or 0
    end

  SwapMonster: |
    function()
      BattleRoyale_Main.RemoveMonster()

      if BR_SpawnerSeri
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения

Сообщений в этой теме
nullptr   UO Inferno   24.3.2025, 19:30
nullptr   Видео замесов. https://rutube.ru/video/9b17c729c93...   23.11.2025, 19:33
olduo.com   - есть какие мероприятия в мире для масс пвп? - ка...   24.11.2025, 8:32
nullptr   - есть какие мероприятия в мире для масс пвп? - к...   24.11.2025, 11:55
Legnes   Маркетплейс удобно сделан. А Time в маркетплейсе п...   25.11.2025, 17:05
olduo.com   Хаха прикольно )) С турнирами понятно и в целом в...   2.9.2026, 18:54
nullptr   Хаха прикольно )) С турнирами понятно и в целом ...   2.9.2026, 19:02
olduo.com   Обалдеть, я редко встречаю в любительской среде ра...   2.9.2026, 19:21
nullptr   Обалдеть, я редко встречаю в любительской среде р...   4.9.2026, 20:08
RL_ka   В плюсах без шансов, считай надо точно знать кто ...   5.9.2026, 14:00
olduo.com   не совсем понимаю с чем из этого списка не справи...   5.9.2026, 14:03
RL_ka   Я вообще не большой умелец использовать ИИ для пр...   5.9.2026, 14:10
nullptr   я думаю claude способен написать эмулятор сервера...   5.9.2026, 14:41
olduo.com   Я вообще не большой умелец использовать ИИ для пр...   5.9.2026, 17:37
nullptr   Нулевой поинтер, а ответб плз на это. Ато там стр...   5.9.2026, 18:15
TeS   Я вообще не большой умелец использовать ИИ для пр...   7.9.2026, 18:13
olduo.com   посмотри ютуб там люди только одними запросами де...   7.9.2026, 18:25
TeS   Я кодил года с 93го прошлого века и лет 5 назад м...   7.9.2026, 18:39
nullptr   сначала заполонили книжками от ии, сейчас заполон...   8.9.2026, 11:30
TeS   Это вопрос восприятия, ии в моем понимании это ст...   8.9.2026, 13:37
olduo.com   для чпу надо снять гараж купить это устройство, з...   8.9.2026, 13:54
TeS   А мне понравилась Suno. Вот мои шедевры [url=htt...   8.9.2026, 15:12
olduo.com   Странно что у людей такая вялая реакция на этот ше...   4.9.2026, 12:59
RL_ka   Когда состоится релиз на гитхабе? Я бы портировал...   4.9.2026, 14:27
olduo.com   Правильно ли я понял, что ты написал API с которым...   5.9.2026, 13:51
olduo.com   Правильно ли я понял, что ты написал API с которы...   5.9.2026, 18:24
nullptr   Сорян, не то процитировал. Мне ии для програмиров...   5.9.2026, 22:01


Ответить в эту темуОткрыть новую тему
18 чел. читают эту тему (гостей: 18, скрытых пользователей: 0)
Пользователей: 0

 

- Текстовая версия | Версия для КПК Сейчас: 21.9.2026, 2:14
Designed by Nickostyle