Skip to main content

Teleport System

A more advanced example showing a teleport system with a scrollable destination list, level requirements, and zen costs. This demonstrates dynamic UI and complex server validation.

What You'll Learn

  • Creating scrollable lists
  • Dynamic UI rendering based on data
  • Item selection and highlighting
  • Complex server-side validation
  • Player teleportation
  • Cost requirements

Features

  • Scrollable destination list
  • Level and zen requirements per destination
  • Selected item highlighting
  • Server-side requirement validation
  • Visual feedback and effects
  • Easy to add new destinations

Client Side

--[[
Teleport System - CLIENT
Scrollable list of teleport destinations
]]

TeleportSystem = {
index = GetWindowIndex(),
group = UI_GROUP_INGAME,
width = 450,
height = 400,

destinations = {},
selectedIndex = -1,
scrollOffset = 0,
visibleItems = 7,
}

PluginPacket = PluginPacket or {}
PluginPacket.Teleport = 15200

function TeleportSystem.renderWindow(window)
UIRenderWindow(window, 0, 0, window.w, window.h, 0)

-- Title bar
window:renderColor(0, 0, window.w, 35, RGBA(0, 0, 0, 200))
window:renderText("Teleport System", 0, 10, window.w, 30, 4,
RGBA(255, 215, 0, 255), FontL)

-- Instructions
window:renderText("Select a destination:", 20, 45, window.w-40, 20, 1,
RGBA(200, 200, 200, 255), FontM)

-- List background
local listX = 20
local listY = 70
local listW = window.w - 40
local listH = 280
window:renderColor(listX, listY, listW, listH, RGBA(20, 20, 20, 200))

-- Render destinations
local itemHeight = 38
local startIndex = TeleportSystem.scrollOffset
local endIndex = math.min(startIndex + TeleportSystem.visibleItems,
#TeleportSystem.destinations)

for i = startIndex + 1, endIndex do
local dest = TeleportSystem.destinations[i]
local itemY = listY + ((i - startIndex - 1) * itemHeight) + 2

-- Selection highlight
local bgColor = RGBA(30, 30, 30, 200)
if i == TeleportSystem.selectedIndex then
bgColor = RGBA(60, 120, 180, 200) -- Blue highlight
elseif window:checkHover(listX, itemY, listW, itemHeight - 4) then
bgColor = RGBA(50, 50, 50, 200) -- Hover
end

window:renderColor(listX + 2, itemY, listW - 4, itemHeight - 4, bgColor)

-- Destination name
window:renderText(dest.name, listX + 10, itemY + 3, 150, 20, 1,
RGBA(255, 255, 255, 255), FontM)

-- Level requirement
local levelColor = RGBA(255, 255, 0, 255) -- Yellow
window:renderText(
string.format("Lv.%d", dest.levelReq),
listX + 170, itemY + 3, 80, 20, 1,
levelColor, FontS
)

-- Zen cost
local costColor = RGBA(255, 215, 0, 255) -- Gold
window:renderText(
string.format("%d zen", dest.zenCost),
listX + 250, itemY + 3, 150, 20, 2,
costColor, FontS
)

-- Description
if dest.desc then
window:renderText(dest.desc, listX + 10, itemY + 20, listW - 20, 15, 1,
RGBA(150, 150, 150, 255), FontS)
end
end

-- Scroll indicator
if #TeleportSystem.destinations > TeleportSystem.visibleItems then
local scrollBarX = listX + listW - 15
local scrollBarY = listY + 5
local scrollBarH = listH - 10
local scrollBarW = 8

-- Track
window:renderColor(scrollBarX, scrollBarY, scrollBarW, scrollBarH,
RGBA(50, 50, 50, 255))

-- Thumb
local maxScroll = #TeleportSystem.destinations - TeleportSystem.visibleItems
local thumbHeight = math.max(20, scrollBarH / #TeleportSystem.destinations * TeleportSystem.visibleItems)
local thumbY = scrollBarY + (TeleportSystem.scrollOffset / maxScroll) * (scrollBarH - thumbHeight)

window:renderColor(scrollBarX, thumbY, scrollBarW, thumbHeight,
RGBA(100, 100, 100, 255))
end

-- Selected info panel
if TeleportSystem.selectedIndex > 0 then
local dest = TeleportSystem.destinations[TeleportSystem.selectedIndex]
local infoY = listY + listH + 10

window:renderColor(20, infoY, window.w - 40, 35, RGBA(0, 0, 50, 180))
window:renderText(
string.format("Selected: %s | Lv.%d | %d zen",
dest.name, dest.levelReq, dest.zenCost),
30, infoY + 8, window.w - 60, 20, 4,
RGBA(255, 255, 255, 255), FontM
)
end
end

function TeleportSystem.updateWindow(window)
if window.state == 0 then
return false
end

-- Handle list clicks
local listX = 20
local listY = 70
local listW = window.w - 40
local itemHeight = 38

if window.click == 1 then -- Just clicked
local mouseX = InputGetMouseX()
local mouseY = InputGetMouseY()

-- Check if clicked in list area
if InputCheckMousePos(window.x + listX, window.y + listY,
listW, 280) then
-- Calculate which item was clicked
local relativeY = mouseY - (window.y + listY)
local clickedItem = math.floor(relativeY / itemHeight) +
TeleportSystem.scrollOffset + 1

if clickedItem > 0 and clickedItem <= #TeleportSystem.destinations then
TeleportSystem.selectedIndex = clickedItem
SoundPlay(10)
end
end
end

-- Handle scrolling
if window.hover == 1 and window.wheel ~= 0 then
local maxScroll = math.max(0, #TeleportSystem.destinations - TeleportSystem.visibleItems)

if window.wheel > 0 then
TeleportSystem.scrollOffset = math.max(0, TeleportSystem.scrollOffset - 1)
else
TeleportSystem.scrollOffset = math.min(maxScroll, TeleportSystem.scrollOffset + 1)
end
end

-- Handle keyboard
if not InputCheckChatOpen() then
-- Arrow keys to navigate
if InputCheckKeyPress(VK_UP, 1) and TeleportSystem.selectedIndex > 1 then
TeleportSystem.selectedIndex = TeleportSystem.selectedIndex - 1

-- Auto-scroll if needed
if TeleportSystem.selectedIndex <= TeleportSystem.scrollOffset then
TeleportSystem.scrollOffset = TeleportSystem.selectedIndex - 1
end
end

if InputCheckKeyPress(VK_DOWN, 1) and
TeleportSystem.selectedIndex < #TeleportSystem.destinations then
TeleportSystem.selectedIndex = TeleportSystem.selectedIndex + 1

-- Auto-scroll if needed
if TeleportSystem.selectedIndex > TeleportSystem.scrollOffset + TeleportSystem.visibleItems then
TeleportSystem.scrollOffset = TeleportSystem.selectedIndex - TeleportSystem.visibleItems
end
end

-- Enter to teleport
if InputCheckKeyPress(VK_RETURN, 1) and TeleportSystem.selectedIndex > 0 then
TeleportSystem:teleport()
end

-- ESC to close
if InputCheckKeyPress(VK_ESCAPE, 1) then
window:setState(0)
end
end

return true
end

function TeleportSystem.renderTeleportButton(window, button)
local enabled = TeleportSystem.selectedIndex > 0

if not enabled then
button:renderAsset(GlobalAsset.buttonN, 0, 0, button.w, button.h)
button:renderText("Select Destination", 0, 0, button.w, button.h, 4,
RGBA(150, 150, 150, 255), FontM)
else
if button.hover == 0 then
button:renderAsset(GlobalAsset.buttonN, 0, 0, button.w, button.h)
elseif button.click == 0 then
button:renderAsset(GlobalAsset.buttonH, 0, 0, button.w, button.h)
elseif button.click == 3 then
button:renderAsset(GlobalAsset.buttonC, 0, 0, button.w, button.h)
TeleportSystem:teleport()
end

button:renderText("Teleport", 0, 0, button.w, button.h, 4,
RGBA(255, 255, 255, 255), FontM)
end
end

function TeleportSystem.renderCloseButton(window, button)
if button.hover == 0 then
button:renderAsset(GlobalAsset.buttonCloseN, 0, 0, button.w, button.h)
elseif button.click == 0 then
button:renderAsset(GlobalAsset.buttonCloseH, 0, 0, button.w, button.h)
else
button:renderAsset(GlobalAsset.buttonCloseC, 0, 0, button.w, button.h)
if button.click == 3 then
window:setState(0)
window:setSpot(window.ox, window.oy)
end
end
end

function TeleportSystem:teleport()
if self.selectedIndex > 0 then
SoundPlay(10)
MagicWorld:sendData(PluginPacket.Teleport, {"teleport", self.selectedIndex})
NoticeSend(1, "Teleporting...")
end
end

function TeleportSystem:open()
local window = UIWindowGet(self.group, self.index)
if window then
if window.state == 0 then
-- Request destination list
MagicWorld:sendData(PluginPacket.Teleport, {"request_list"})
window:setState(1)
else
window:setState(0)
window:setSpot(window.ox, window.oy)
end
end
end

-- Initialize
BridgeFunction:push("OnLoad", function()
local window = UIWindowAdd(
TeleportSystem.group, TeleportSystem.index,
0, 2,
GetWindowCenterX(TeleportSystem.width),
GetWindowCenterY(TeleportSystem.height),
TeleportSystem.width, TeleportSystem.height, 1,
TeleportSystem.renderWindow,
TeleportSystem.updateWindow
)

if window then
window:setDragArea(0, 0, TeleportSystem.width, 35)

-- Close button
window:addButton(0, 1, TeleportSystem.width - 30, 8, 20, 20,
TeleportSystem.renderCloseButton)

-- Teleport button
window:addButton(1, 1, 160, 360, 130, 35,
TeleportSystem.renderTeleportButton)

LogPrint("[TeleportSystem] Window created!")
end
end)

-- Receive from server
MagicWorld:pushRecv(PluginPacket.Teleport, function(packet)
local responseType = packet:getString()

if responseType == "destinations" then
local count = packet:getNumber()
TeleportSystem.destinations = {}

for i = 1, count do
local dest = {
name = packet:getString(),
levelReq = packet:getNumber(),
zenCost = packet:getNumber(),
desc = packet:getString()
}
table.insert(TeleportSystem.destinations, dest)
end

TeleportSystem.selectedIndex = -1
TeleportSystem.scrollOffset = 0

LogPrint(string.format("[TeleportSystem] Loaded %d destinations", count))

elseif responseType == "success" then
NoticeSend(1, "Teleported successfully!")
local window = UIWindowGet(TeleportSystem.group, TeleportSystem.index)
if window then
window:setState(0)
end

elseif responseType == "error" then
local errorMsg = packet:getString()
NoticeSend(1, errorMsg)
SoundPlay(12) -- Error sound
end

packet:free()
end)

Server Side

--[[
Teleport System - SERVER
Manages destinations, validates requirements, teleports players
]]

TeleportSystem = {}
TeleportSystem.ScriptVersion = "1.0.0"

-- Destination configuration
TeleportSystem.Destinations = {
{name = "Lorencia", map = 0, x = 130, y = 130, levelReq = 1, zenCost = 0,
desc = "Starting city"},
{name = "Devias", map = 2, x = 220, y = 40, levelReq = 10, zenCost = 50000,
desc = "Trading village"},
{name = "Noria", map = 3, x = 170, y = 110, levelReq = 20, zenCost = 100000,
desc = "Seaport town"},
{name = "Dungeon", map = 1, x = 108, y = 165, levelReq = 50, zenCost = 300000,
desc = "Dangerous dungeon"},
{name = "Atlans", map = 7, x = 20, y = 14, levelReq = 80, zenCost = 500000,
desc = "Ancient ruins"},
{name = "Tarkan", map = 8, x = 185, y = 55, levelReq = 100, zenCost = 750000,
desc = "Arid desert"},
{name = "Icarus", map = 10, x = 15, y = 13, levelReq = 150, zenCost = 1000000,
desc = "Mystic sky"},
{name = "Aida", map = 33, x = 80, y = 120, levelReq = 200, zenCost = 1500000,
desc = "Shadow land"},
{name = "Elbeland", map = 51, x = 130, y = 125, levelReq = 250, zenCost = 2000000,
desc = "Green fields"},
{name = "Karutan", map = 80, x = 130, y = 130, levelReq = 300, zenCost = 3000000,
desc = "Dense forest"},
}

PluginPacket = PluginPacket or {}
PluginPacket.Teleport = 15200

-- Send destination list
function TeleportSystem:sendDestinations(aIndex)
local data = {"destinations", #self.Destinations}

for _, dest in ipairs(self.Destinations) do
table.insert(data, dest.name)
table.insert(data, dest.levelReq)
table.insert(data, dest.zenCost)
table.insert(data, dest.desc or "")
end

MagicWorld:sendData(aIndex, PluginPacket.Teleport, data)

LogPrint(string.format("[Teleport] List sent to %s",
GetObjectName(aIndex)))
end

-- Teleport player
function TeleportSystem:teleport(aIndex, destIndex)
local charName = GetObjectName(aIndex)

-- Validate index
if destIndex < 1 or destIndex > #self.Destinations then
MagicWorld:sendData(aIndex, PluginPacket.Teleport, {
"error", "Invalid destination!"
})
return
end

local dest = self.Destinations[destIndex]

-- Validate level
local level = GetObjectLevel(aIndex)
if level < dest.levelReq then
MagicWorld:sendData(aIndex, PluginPacket.Teleport, {
"error",
string.format("Level %d required!", dest.levelReq)
})
return
end

-- Validate zen
local zen = GetObjectMoney(aIndex)
if zen < dest.zenCost then
MagicWorld:sendData(aIndex, PluginPacket.Teleport, {
"error",
string.format("Need %d zen!", dest.zenCost)
})
return
end

-- Charge zen
if dest.zenCost > 0 then
SetObjectMoney(aIndex, zen - dest.zenCost)
MoneySend(aIndex, GetObjectMoney(aIndex))
end

-- Teleport
MoveUserEx(aIndex, dest.map, dest.x, dest.y)

-- Send success
MagicWorld:sendData(aIndex, PluginPacket.Teleport, {"success"})

-- Log
LogColor(2, string.format(
"[Teleport] %s -> %s (cost: %d zen)",
charName, dest.name, dest.zenCost
))

-- Notify
NoticeSend(aIndex, 1, string.format("Teleported to %s!", dest.name))

-- Visual effect
FireworksSend(aIndex, 0, 0)
end

-- Handle packets
MagicWorld:pushRecv(PluginPacket.Teleport, function(aIndex, packet)
local action = packet:getString()

if action == "request_list" then
TeleportSystem:sendDestinations(aIndex)

elseif action == "teleport" then
local destIndex = packet:getNumber()
TeleportSystem:teleport(aIndex, destIndex)
end
end)

-- Command
TeleportSystem.CommandCode = Toolkit:getUniqueIndex()

BridgeFunction:push("OnReadScript", function()
CommandAddInfo("/teleport", TeleportSystem.CommandCode)
end)

BridgeFunction:push("OnCommandManager", function(aIndex, code, args)
if code == TeleportSystem.CommandCode then
TeleportSystem:sendDestinations(aIndex)
NoticeSend(aIndex, 1, "Check teleport window!")
return 1
end
return 0
end)

LogColor(3, string.format("[TeleportSystem] Loaded - Version: %s",
TeleportSystem.ScriptVersion))
LogColor(3, string.format("[TeleportSystem] %d destinations configured",
#TeleportSystem.Destinations))

Key Concepts Demonstrated

1. Dynamic Lists

  • Rendering items from a data array
  • Scrollable list with mouse wheel
  • Keyboard navigation (arrow keys)
  • Selection highlighting

2. Advanced UI

  • Hover effects
  • Selected item visual feedback
  • Scroll bar indicator
  • Responsive to different input methods

3. Data Synchronization

  • Server sends list of destinations
  • Client stores and displays data
  • Selection state management
  • Scroll position tracking

4. Complex Validation

  • Multiple requirement checks (level, zen)
  • Clear error messages for each failure
  • Server-side authority (can't be cheated)

5. User Experience

  • Visual feedback for all actions
  • Keyboard shortcuts
  • Clear requirement display
  • Effects on teleport

Installation

Same as Daily Reward example - place files in appropriate folders and register packet IDs.

Adding New Destinations

Simply add entries to the Destinations table on the server:

{
name = "New Place",
map = 99,
x = 100,
y = 100,
levelReq = 350,
zenCost = 5000000,
desc = "A mysterious new location"
}

Possible Improvements

  • Favorite destinations (saved per player)
  • Recently visited list
  • Map preview images
  • Party teleport (teleport whole party)
  • VIP discounts on teleport costs
  • Cooldown between teleports
  • Quest-locked destinations
  • Custom waypoints set by players
  • Teleport history
  • Search/filter destinations

Testing

  1. Open with /teleport command or TeleportSystem:open()
  2. Browse the destination list
  3. Use mouse wheel or arrow keys to navigate
  4. Click a destination or press Enter
  5. Check level/zen requirements are enforced
  6. Verify teleport works correctly
  7. Test with insufficient level/zen

This example shows how to build more complex, data-driven interfaces with proper state management!