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
- Open with
/teleportcommand orTeleportSystem:open() - Browse the destination list
- Use mouse wheel or arrow keys to navigate
- Click a destination or press Enter
- Check level/zen requirements are enforced
- Verify teleport works correctly
- Test with insufficient level/zen
This example shows how to build more complex, data-driven interfaces with proper state management!