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

2 страниц V  1 2 >  
Ответить в эту темуОткрыть новую тему
> UO Inferno
nullptr
сообщение 24.3.2025, 19:30
Сообщение #1


***

Novice
Сообщений: 65
Регистрация: 13.3.2021
Группа: Пользователи
Наличность: 25
Пользователь №: 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Отправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
nullptr
сообщение 23.11.2025, 19:33
Сообщение #2


***

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



Видео замесов.
https://rutube.ru/video/9b17c729c9331d6975f...fa6d9b0/?r=plwd
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
olduo.com
сообщение 24.11.2025, 8:32
Сообщение #3


******

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



- есть какие мероприятия в мире для масс пвп?
- какой средний онлайн?
- ненахожу дискорда ток телеграм. это так задуманно или ошибка какая?

сайт прям оч нравица, ничего лишнего, тёмный всё читается хорошо, шрифт JetBrains, чусвуеца рука программиста, а не дизайнера =)


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


***

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



Цитата(olduo.com @ 24.11.2025, 10:32) *

- есть какие мероприятия в мире для масс пвп?
- какой средний онлайн?
- ненахожу дискорда ток телеграм. это так задуманно или ошибка какая?

сайт прям оч нравица, ничего лишнего, тёмный всё читается хорошо, шрифт JetBrains, чусвуеца рука программиста, а не дизайнера =)
Discord в рф заблокирован, многие попросту не смогут поставить обход.
В выходные ~40, в будни бывает и 10.

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


******

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



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

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


--------------------
Пользователь в онлайне!Delete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
Legnes
сообщение 25.11.2025, 17:05
Сообщение #6


***

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



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


***

Novice
Сообщений: 65
Регистрация: 13.3.2021
Группа: Пользователи
Наличность: 25
Пользователь №: 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
Сообщение #8


***

Novice
Сообщений: 65
Регистрация: 13.3.2021
Группа: Пользователи
Наличность: 25
Пользователь №: 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
Сообщение #9


***

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



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

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


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


******

Expert
Сообщений: 515
Регистрация: 31.7.2016
Группа: Пользователи
Наличность: 234
Пользователь №: 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
Сообщение #11


***

Novice
Сообщений: 65
Регистрация: 13.3.2021
Группа: Пользователи
Наличность: 25
Пользователь №: 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Отправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
olduo.com
сообщение 2.9.2026, 18:54
Сообщение #12


******

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



Хаха прикольно ))

С турнирами понятно и в целом видиться не очень сложным.
А на сколько сложные квесты, наверняка это принеси подай?

Я почему спрашиваю, Я хоть и не первый год уже не слежу за игровой индустрией, но я помню влажные мечты разработчиков о том что им контент будет генерировать нейросеть.
Но сгенерировать квесты где нужно убить и принести пять шкурок это пустяк, по сравнению с разветвлёнными квестами, например, mass effect.

Поэтому вот хотелось бы понять крутость генерируемых квестов.

А в целом естественно жму руку, очень крутая идея и реализация.


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


***

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



Цитата(olduo.com @ 2.9.2026, 20:54) *

Хаха прикольно ))

С турнирами понятно и в целом видиться не очень сложным.
А на сколько сложные квесты, наверняка это принеси подай?

Я почему спрашиваю, Я хоть и не первый год уже не слежу за игровой индустрией, но я помню влажные мечты разработчиков о том что им контент будет генерировать нейросеть.
Но сгенерировать квесты где нужно убить и принести пять шкурок это пустяк, по сравнению с разветвлёнными квестами, например, mass effect.

Поэтому вот хотелось бы понять крутость генерируемых квестов.

А в целом естественно жму руку, очень крутая идея и реализация.
По сути, ограничений нет — при желании можно хоть Fallout 2 изобретать с кучей разных состояний, веток и условий.

Для примера могу скинуть куски детективной цепочки.

Код
Handlers:
  OnTalk: |
    function(player, npcName)
      local questName = "TavernMurder"

      _G.TavernMurder = _G.TavernMurder or {}
      _G.TavernMurderCompleted = _G.TavernMurderCompleted or {}

      if _G.TavernMurderCompleted[player] then
        RunDialog(player, "TavernMurder_Main", "captain_already_completed")
        return
      end

      if not Utility.QHave(player, questName) then
        if _G.TavernMurder.Active and TavernMurder_Main.CheckCluesExist() then
          RunDialog(player, "TavernMurder_Main", "captain_quest_locked")
          return
        end
      end

      if not Utility.QHave(player, questName) then
        RunDialog(player, "TavernMurder_Main", "captain_intro")
        return
      end

      if Utility.QComplete(player, questName, 1) then
        RunDialog(player, "TavernMurder_Main", "captain_accusation")
        return
      end

      RunDialog(player, "TavernMurder_Main", "captain_investigation")
    end

Methods:
  CheckCluesExist: |
    function()
      local objects = GetObjectsInRange("Felucca", 1494, 1681, 20, 10, 0)

      if not objects then
        Say(1, "CheckClues: объектов не найдено")
        return false
      end

      Say(1, string.format("CheckClues: найдено объектов: %d", #objects))

      for _, obj in ipairs(objects) do
        if obj and obj.Name then
          local name = tostring(obj.Name)

          if name == "BloodStain" or name == "BloodyKnife" or
             name == "BrokenChair" or name == "TornLetter" then
            Say(1, "НАЙДЕНА УЛИКА: " .. name)
            return true
          end
        end
      end

      Say(1, "CheckClues: улик среди объектов нет")
      return false
    end

  CheckAndResetIfExpired: |
    function()
      _G.TavernMurder = _G.TavernMurder or {}

      if not _G.TavernMurder.Active then
        Say(1, "Задание не активно - проверять нечего")
        return
      end

      Say(1, "Проверяю, не исчезли ли улики...")

      if not TavernMurder_Main.CheckCluesExist() then
        _G.TavernMurder.Active = false
        Say(1, "Улики исчезли - флаг сброшен - задание доступно")
      else
        Say(1, "Улики на месте - задание пока закрыто")
      end
    end

  AddQuest: |
    function(player)
      local questName = "TavernMurder"

      _G.TavernMurder = _G.TavernMurder or {}
      _G.TavernMurderCompleted = _G.TavernMurderCompleted or {}

      if _G.TavernMurder.Active then
        if TavernMurder_Main.CheckCluesExist() then
          Say(player, "Расследование уже ведёт другой.")
          return
        else
          _G.TavernMurder.Active = false
          Say(player, "Прошлое расследование закрыто. Начинаем новое.")
        end
      end

      AddQuest(player, questName)

      _G.TavernMurder.Active = true

      Utility.SetQuestVar(player, questName, "Clue_BloodyKnife", "0")
      Utility.SetQuestVar(player, questName, "Clue_TornLetter", "0")
      Utility.SetQuestVar(player, questName, "Clue_BloodStain", "0")
      Utility.SetQuestVar(player, questName, "Clue_BrokenMug", "0")
      Utility.SetQuestVar(player, questName, "Witness_Innkeeper", "0")
      Utility.SetQuestVar(player, questName, "Witness_Maid", "0")
      Utility.SetQuestVar(player, questName, "Witness_Drunk", "0")

      Say(player, "Задание принято.")

      local deleteTime = CreateDT(0, 30)

      CreateNPCAtLocation("WallBlood", 1494, 1680, 20,
        "Name BloodStain Movable False OnUseScript Clue_BloodStain.OnUse Expired " .. deleteTime)

      CreateNPCAtLocation("BloodBlade", 1495, 1681, 20,
        "Name BloodyKnife Movable False OnUseScript Clue_BloodyKnife.OnUse Expired " .. deleteTime)

      CreateNPCAtLocation("BrokenChair", 1494, 1681, 20,
        "Name BrokenChair Movable False OnUseScript Clue_BrokenMug.OnUse Expired " .. deleteTime)

      CreateNPCAtLocation("BlankMap", 1494, 1682, 20,
        "Name TornLetter Movable False OnUseScript Clue_TornLetter.OnUse Expired " .. deleteTime)

      Say(player, "Место преступления готово.")
      Say(player, "Улики исчезнут через 30 минут.")
    end

  TryAdvanceStage: |
    function(player)
      local questName = "TavernMurder"

      if not Utility.QHave(player, questName) then
        return
      end

      if Utility.QComplete(player, questName, 1) then
        return
      end

      local clues = 0
      if Utility.GetQuestVar(player, questName, "Clue_BloodyKnife") == "1" then clues = clues + 1 end
      if Utility.GetQuestVar(player, questName, "Clue_TornLetter") == "1" then clues = clues + 1 end
      if Utility.GetQuestVar(player, questName, "Clue_BloodStain") == "1" then clues = clues + 1 end
      if Utility.GetQuestVar(player, questName, "Clue_BrokenMug") == "1" then clues = clues + 1 end

      local witnesses = 0
      if Utility.GetQuestVar(player, questName, "Witness_Innkeeper") == "1" then witnesses = witnesses + 1 end
      if Utility.GetQuestVar(player, questName, "Witness_Maid") == "1" then witnesses = witnesses + 1 end
      if Utility.GetQuestVar(player, questName, "Witness_Drunk") == "1" then witnesses = witnesses + 1 end

      Say(player, string.format("Собрано: улик %d/4, свидетелей %d/3", clues, witnesses))

      if clues >= 4 and witnesses >= 3 then
        CompleteObjective(player, questName)
        Say(player, "Все доказательства собраны.")
      end
    end

  UnlockQuest: |
    function()
      _G.TavernMurder = _G.TavernMurder or {}
      _G.TavernMurder.Active = false
      Say(1, "Задание снова доступно.")
    end

  ResetCompletedFlag: |
    function(player)
      _G.TavernMurderCompleted = _G.TavernMurderCompleted or {}
      _G.TavernMurderCompleted[player] = nil
      Say(player, "Задание можно взять снова.")
    end

  DoAccuse: |
    function(player, accusedKey)
      local questName = "TavernMurder"

      if not Utility.QHave(player, questName) then
        Say(player, "У вас нет этого задания.")
        return
      end

      if not Utility.QComplete(player, questName, 1) then
        Say(player, "Сначала соберите все доказательства.")
        return
      end

      if accusedKey == "JealousHusband" then
        Say(player, "В точку! Маркус виновен.")
        CreateItemInBackpack(player, "Gold", "Amount 3000")

        _G.TavernMurderCompleted = _G.TavernMurderCompleted or {}
        _G.TavernMurderCompleted[player] = true

        RunDialog(player, "TavernMurder_Main", "captain_accusation_correct")

      else
        Say(player, "Мимо. Настоящий убийца ушёл.")
        CreateItemInBackpack(player, "Gold", "Amount 300")

        RunDialog(player, "TavernMurder_Main", "captain_accusation_wrong")
      end

      CompleteQuest(player, questName)
      DeleteCompleteQuest(player, questName)

      Utility.ClearQuestVar(player, questName, "Clue_BloodyKnife")
      Utility.ClearQuestVar(player, questName, "Clue_TornLetter")
      Utility.ClearQuestVar(player, questName, "Clue_BloodStain")
      Utility.ClearQuestVar(player, questName, "Clue_BrokenMug")
      Utility.ClearQuestVar(player, questName, "Witness_Innkeeper")
      Utility.ClearQuestVar(player, questName, "Witness_Maid")
      Utility.ClearQuestVar(player, questName, "Witness_Drunk")
    end

  AccuseJealousHusband: |
    function(player)
      TavernMurder_Main.DoAccuse(player, "JealousHusband")
    end

  AccuseBusinessRival: |
    function(player)
      TavernMurder_Main.DoAccuse(player, "BusinessRival")
    end

  AccuseDebtor: |
    function(player)
      TavernMurder_Main.DoAccuse(player, "Debtor")
    end

Quests:
  TavernMurder:
    Name: "Убийство в таверне"
    Reject: true
    Reward:
      - {Type: "Item", ItemId: 0x0EED, Hue: 0x0, ToolTip: "Gold"}
    Objectives:
      - Description: |
          В таверне «Пьяный гном» убит богатый торговец. Капитан стражи поручает вам
          расследование. Осмотрите место преступления, соберите улики и опросите свидетелей.
          ЗАДАЧИ:
          1. Собрать 4 улики на месте преступления
          2. Опросить 3 свидетелей
          ВНИМАНИЕ: Улики исчезнут через 30 минут!
        Actions:
          - Progress: "Собрать улики"
          - Progress: "Опросить свидетелей"

      - Description: |
          Расследование завершено! У вас есть все улики и показания свидетелей.
          Вернитесь к капитану стражи и назовите виновного.

          ПОДОЗРЕВАЕМЫЕ:
          1. Ревнивый муж жертвы (Marcus)
          2. Конкурент по бизнесу (Victor)
          3. Должник (Thomas)
        Actions:
          - Progress: "Назвать убийцу капитану"

nodes:
  - Id: captain_intro
    Text: |
      Капитан стражи хмуро смотрит на вас.
      «Торговца нашли мёртвым в таверне "Пьяный гном". Ножевое ранение.
      Нужен посторонний следователь. Возьмёте дело?»
    Options:
      - Text: "Я займусь расследованием."
        To: captain_accept
      - Text: "Не сейчас."
        To: captain_refuse

  - Id: captain_accept
    Text: |
      *кивает*
      «Хорошо. Я отправлю людей подготовить место преступления.
      Поговорите с теми, кто был в таверне той ночью. Ищите улики.
      Когда соберёте доказательства — возвращайтесь.»
    Options:
      - Text: "[Принять задание]"
        To: captain_after_accept
        Action: 'AddQuest'

  - Id: captain_after_accept
    Text: "Жду вашего доклада. Удачи, следователь."
    Options: []

  - Id: captain_refuse
    Text: "Жаль. Придётся обойтись своими силами."
    Options: []

  - Id: captain_investigation
    Text: |
      «Как продвигается расследование? Чем больше улик — тем крепче дело.»
    Options:
      - Text: "Я продолжаю собирать улики."
        To: captain_investigation_continue

  - Id: captain_investigation_continue
    Text: "Место преступления оцеплено. Можете продолжать осмотр."
    Options: []

  - Id: captain_accusation
    Text: |
      «Итак, вы собрали все улики и опросили свидетелей.
      Кого обвиняете в убийстве?
      ПОДОЗРЕВАЕМЫЕ:
      1. MARCUS — ревнивый муж жертвы
      2. VICTOR — конкурент по бизнесу
      3. THOMAS — должник
      Ваше решение?»
    Options:
      - Text: "Обвиняю Marcus — ревнивого мужа."
        To: captain_accusation_confirm_marcus
      - Text: "Обвиняю Victor — конкурента."
        To: captain_accusation_confirm_victor
      - Text: "Обвиняю Thomas — должника."
        To: captain_accusation_confirm_thomas

  - Id: captain_accusation_confirm_marcus
    Text: |
      «Marcus, ревнивый муж? Вы уверены?
      После обвинения дело будет закрыто.»
    Options:
      - Text: "Да, уверен. Marcus виновен!"
        Action: 'AccuseJealousHusband'
      - Text: "Нет, позвольте подумать ещё."
        To: captain_accusation

  - Id: captain_accusation_confirm_victor
    Text: |
      «Victor, конкурент? Вы уверены?
      После обвинения дело будет закрыто.»
    Options:
      - Text: "Да, уверен. Victor виновен!"
        Action: 'AccuseBusinessRival'
      - Text: "Нет, позвольте подумать ещё."
        To: captain_accusation

  - Id: captain_accusation_confirm_thomas
    Text: |
      «Thomas, должник? Вы уверены?
      После обвинения дело будет закрыто.»
    Options:
      - Text: "Да, уверен. Thomas виновен!"
        Action: 'AccuseDebtor'
      - Text: "Нет, позвольте подумать ещё."
        To: captain_accusation

  - Id: captain_accusation_correct
    Text: |
      *капитан зовёт стражников*
      «ОТЛИЧНАЯ РАБОТА! Вы правы — это Marcus!
      Улики указывают на ревнивого мужа:
      - Письмо с угрозами подписано его именем
      - Нож от кузнеца — Marcus работает кузнецом
      - Свидетели видели его ссору с жертвой
      Стражники арестовали Marcus. Правосудие свершилось!
      Вот ваша награда — 3000 золотых монет.
      Вы доказали себя как талантливый следователь!»
    Options: []

  - Id: captain_accusation_wrong
    Text: |
      *капитан качает головой*
      «Хм... Я проверил ваше обвинение.
      Улики не подтверждают вашу версию.
      Ваши доказательства слишком слабы.
      Пока мы проверяли этого подозреваемого,
      настоящий убийца успел скрыться.
      Правосудие не свершилось.
      Вот ваша оплата за работу — 300 золотых.
      Вы можете попробовать снова, если найдёте новые улики.»
    Options: []

  - Id: captain_quest_locked
    Text: |
      *качает головой*
      «Другой следователь уже работает над этим делом.
      Место преступления оцеплено, улики под охраной.
      Приходите позже — через 30 минут место освободится.»
    Options: []

  - Id: captain_already_completed
    Text: |
      «Вы уже расследовали это убийство и назвали виновного.
      Дело закрыто, повторное расследование не требуется.
      Спасибо за вашу работу.»
    Options: []
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
olduo.com
сообщение 2.9.2026, 19:21
Сообщение #14


******

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



Обалдеть, я редко встречаю в любительской среде разработку уровня коммерческого геймдева.

Нет слов, круто! Маркус злодей ))

Главное не забывай, что возможно, мы ответим за то что игроки пялились на пиксели, а не жили с миром и природой.


--------------------
Пользователь в онлайне!Delete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
olduo.com
сообщение 4.9.2026, 12:59
Сообщение #15


******

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



Странно что у людей такая вялая реакция на этот шедевр.

Когда состоится релиз на гитхабе? Я бы портировал на сферу хаха ))


--------------------
Пользователь в онлайне!Delete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
RL_ka
сообщение 4.9.2026, 14:27
Сообщение #16


*****

Разработчик Injection
Сообщений: 481
Регистрация: 13.7.2013
Группа: Модераторы
Наличность: 15352
Из: Екатеринбург
Пользователь №: 16.211



Цитата(olduo.com @ 4.9.2026, 11:59) *

Когда состоится релиз на гитхабе? Я бы портировал на сферу хаха ))

так попроси тот же ИИ сделать тебе такое, он сделает.
В текущих реалиях я бы вообще для УОшки ничего больше руками не делал учитывая сколько ей лет, сколько инфы в интернете и наработок. В 90% случаев ИИ сделает лучше чем человек-самоучка без должных навыков (IMG:style_emoticons/default/smile.gif)
То на что мы раньше тратили месяцы при разработке инжекта разбирая клиент на инструкции, ИИ делает за минуту

Сообщение отредактировал RL_ka - 4.9.2026, 14:34
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
nullptr
сообщение 4.9.2026, 20:08
Сообщение #17


***

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



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

Обалдеть, я редко встречаю в любительской среде разработку уровня коммерческого геймдева.

Нет слов, круто! Маркус злодей ))

Главное не забывай, что возможно, мы ответим за то что игроки пялились на пиксели, а не жили с миром и природой.
Я не силён в сфере, теоретически можно портнуть, но это точно надо делать на плюсах в ядре.
Это ещё один скриптовый движок поверх ядра, у многих свои версии сферы, тут ещё геморнее.

Цитата(RL_ka @ 4.9.2026, 16:27) *

так попроси тот же ИИ сделать тебе такое, он сделает.
В плюсах без шансов, считай надо точно знать кто держит ссылки, use after free краш сервера. Должны кидаться чистые серийники, никаких объектов, каждый метод должен заново получать объект если он есть и делать операции. Тут надо строго задавать границу и хорошо знать архитектуру сферы. Теже таймеры, когда отдаёшь управление в луа.
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
olduo.com
сообщение 5.9.2026, 13:51
Сообщение #18


******

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



Правильно ли я понял, что ты написал API с которым ознакомил чатgpt.
Чатужпт пишется текстовое описание, он генерирует YAML, который передаётся серверу, который знает как его обрабатывать.

Правильно ли я понял или что-то напутал?


--------------------
Пользователь в онлайне!Delete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
RL_ka
сообщение 5.9.2026, 14:00
Сообщение #19


*****

Разработчик Injection
Сообщений: 481
Регистрация: 13.7.2013
Группа: Модераторы
Наличность: 15352
Из: Екатеринбург
Пользователь №: 16.211



Цитата(nullptr @ 4.9.2026, 19:08) *

В плюсах без шансов, считай надо точно знать кто держит ссылки, use after free краш сервера. Должны кидаться чистые серийники, никаких объектов, каждый метод должен заново получать объект если он есть и делать операции. Тут надо строго задавать границу и хорошо знать архитектуру сферы. Теже таймеры, когда отдаёшь управление в луа.


не совсем понимаю с чем из этого списка не справится ИИ ну ладно:)
Пользователь в офлайнеDelete PostОтправить личное сообщение
Вернуться в начало страницы
+Ответить с цитированием данного сообщения
olduo.com
сообщение 5.9.2026, 14:03
Сообщение #20


******

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



Цитата(RL_ka @ 5.9.2026, 14:00) *

не совсем понимаю с чем из этого списка не справится ИИ ну ладно:)

Я вообще не большой умелец использовать ИИ для программирования, ну там регулярку мне напишет или небольшой кусочек кода. Но если изменения производятся в ядре с кучей всего, как там может помочь ИИ и написать за меня, я лично не представляю или просто не умею им пользоваться.

Я слышал про копилот или какойто ии ассистент прямо в IDE, который сам пишет код, но не пользовался.


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

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

 

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