Client Upload Part 2

RD Core / Commit #5

Commit Client Upload Part 2

Alex Alex committed 19/10/2026 00:06 main Patch
1,244 files +271,215 -0
A RD_Client/modules/game_textmessage/textmessage.otui +100-0 View file
@@ -0,0 +1,100 @@
1+TextMessageLabel < UILabel
2+ font: sono_medium_border_14
3+ text-align: center
4+ text-wrap: true
5+ text-auto-resize: true
6+ margin-bottom: 2
7+ visible: false
8+
9+Panel
10+ anchors.fill: gameMapPanel
11+ focusable: false
12+
13+ Panel
14+ id: centerTextMessagePanel
15+ layout:
16+ type: verticalBox
17+ fit-children: true
18+ width: 360
19+ anchors.centerIn: parent
20+
21+ TextMessageLabel
22+ id: highCenterLabel
23+ TextMessageLabel
24+ id: middleCenterLabel
25+ TextMessageLabel
26+ id: lowCenterLabel
27+
28+ TextMessageLabel
29+ id: privateLabel
30+ anchors.top: parent.top
31+ anchors.bottom: centerTextMessagePanel.top
32+ anchors.horizontalCenter: parent.horizontalCenter
33+ text-auto-resize: false
34+ width: 275
35+
36+ TextMessageLabel
37+ id: statusLabel
38+ anchors.bottom: parent.bottom
39+ anchors.left: parent.left
40+ anchors.right: parent.right
41+
42+ TextMessageLabel
43+ id: customMessage
44+ height: 32
45+ anchors.top: parent.top
46+ anchors.horizontalCenter: parent.horizontalCenter
47+ background-color: #00000890
48+ border: 1 #cc0c0c
49+ margin-top: 80
50+ text-auto-resize: false
51+ text-wrap: false
52+
53+ UIWidget
54+ id: privateAlertWindow
55+ size: 256 128
56+ anchors.top: parent.top
57+ anchors.horizontalCenter: parent.horizontalCenter
58+ margin-top: 80
59+ image-source: /images/game/private_mensaje_alert
60+ image-fixed-ratio: true
61+ visible: false
62+ phantom: true
63+
64+ UIWidget
65+ id: textContainer
66+ anchors.left: parent.left
67+ anchors.verticalCenter: parent.verticalCenter
68+ margin-left: 24
69+ margin-top: 12
70+ width: 224
71+ layout:
72+ type: verticalBox
73+ fit-children: true
74+
75+ Label
76+ id: titleLabel
77+ font: sono_medium_border_14
78+ color: #FF00FF
79+ text-auto-resize: true
80+
81+ Label
82+ id: messageLabel
83+ font: sono_medium_border_14
84+ color: #FFFFFF
85+ text-wrap: true
86+ text-auto-resize: true
87+ text-align: left
88+
89+ UIWidget
90+ id: silentPMAlert
91+ size: 64 64
92+ anchors.top: parent.top
93+ anchors.right: parent.right
94+ margin-top: 120
95+ margin-right: 20
96+ image-source: /images/game/1
97+ visible: false
98+ phantom: false
99+ @onClick: modules.game_chat.onSilentAlertClick()
100+
A RD_Client/modules/game_textwindow/textwindow.lua +144-0 View file
@@ -0,0 +1,144 @@
1+windows = {}
2+
3+function init()
4+ g_ui.importStyle('textwindow')
5+
6+ connect(g_game, { onEditText = onGameEditText,
7+ onEditList = onGameEditList,
8+ onGameEnd = destroyWindows })
9+end
10+
11+function terminate()
12+ disconnect(g_game, { onEditText = onGameEditText,
13+ onEditList = onGameEditList,
14+ onGameEnd = destroyWindows })
15+
16+ destroyWindows()
17+end
18+
19+function destroyWindows()
20+ for _,window in pairs(windows) do
21+ window:destroy()
22+ end
23+ windows = {}
24+end
25+
26+function onGameEditText(id, itemId, maxLength, text, writer, time)
27+ local textWindow = g_ui.createWidget('TextWindow', rootWidget)
28+
29+ local writeable = #text < maxLength and maxLength > 0
30+ local textItem = textWindow:getChildById('textItem')
31+ local description = textWindow:getChildById('description')
32+ local textEdit = textWindow:getChildById('text')
33+ local okButton = textWindow:getChildById('okButton')
34+ local cancelButton = textWindow:getChildById('cancelButton')
35+
36+ local textScroll = textWindow:getChildById('textScroll')
37+
38+ if textItem:isHidden() then
39+ textItem:show()
40+ end
41+
42+ textItem:setItemId(itemId)
43+ textEdit:setMaxLength(maxLength)
44+ textEdit:setText(text)
45+ textEdit:setEditable(writeable)
46+ textEdit:setCursorVisible(writeable)
47+
48+ local desc = ''
49+ if #writer > 0 then
50+ desc = tr('You read the following, written by \n%s\n', writer)
51+ if #time > 0 then
52+ desc = desc .. tr('on %s.\n', time)
53+ end
54+ elseif #time > 0 then
55+ desc = tr('You read the following, written on \n%s.\n', time)
56+ end
57+
58+ if #text == 0 and not writeable then
59+ desc = tr("It is empty.")
60+ elseif writeable then
61+ desc = desc .. tr('You can enter new text.')
62+ end
63+
64+ local lines = #{string.find(desc, '\n')}
65+ if lines < 2 then desc = desc .. '\n' end
66+
67+ description:setText(desc)
68+
69+ if not writeable then
70+ textWindow:setText(tr('Show Text'))
71+ cancelButton:hide()
72+ cancelButton:setWidth(0)
73+ okButton:setMarginRight(0)
74+ else
75+ textWindow:setText(tr('Edit Text'))
76+ end
77+
78+ if description:getHeight() < 64 then
79+ description:setHeight(64)
80+ end
81+
82+ local function destroy()
83+ textWindow:destroy()
84+ table.removevalue(windows, textWindow)
85+ end
86+
87+ local doneFunc = function()
88+ if writeable then
89+ g_game.editText(id, textEdit:getText())
90+ end
91+ destroy()
92+ end
93+
94+ okButton.onClick = doneFunc
95+ cancelButton.onClick = destroy
96+
97+ if not writeable then
98+ textWindow.onEnter = doneFunc
99+ end
100+
101+ textWindow.onEscape = destroy
102+
103+ table.insert(windows, textWindow)
104+end
105+
106+function onGameEditList(id, doorId, text)
107+ local textWindow = g_ui.createWidget('TextWindow', rootWidget)
108+
109+ local textEdit = textWindow:getChildById('text')
110+ local description = textWindow:getChildById('description')
111+ local okButton = textWindow:getChildById('okButton')
112+ local cancelButton = textWindow:getChildById('cancelButton')
113+
114+ local textItem = textWindow:getChildById('textItem')
115+ if textItem and not textItem:isHidden() then
116+ textItem:hide()
117+ end
118+
119+ textEdit:setMaxLength(8192)
120+ textEdit:setText(text)
121+ textEdit:setEditable(true)
122+ description:setText(tr('Enter one name per line.'))
123+ textWindow:setText(tr('Edit List'))
124+
125+ if description:getHeight() < 64 then
126+ description:setHeight(64)
127+ end
128+
129+ local function destroy()
130+ textWindow:destroy()
131+ table.removevalue(windows, textWindow)
132+ end
133+
134+ local doneFunc = function()
135+ g_game.editList(id, doorId, textEdit:getText())
136+ destroy()
137+ end
138+
139+ okButton.onClick = doneFunc
140+ cancelButton.onClick = destroy
141+ textWindow.onEscape = destroy
142+
143+ table.insert(windows, textWindow)
144+end
A RD_Client/modules/game_textwindow/textwindow.otmod +10-0 View file
@@ -0,0 +1,10 @@
1+Module
2+ name: game_textwindow
3+ description: Allow to edit text books and lists
4+ author: edubart, BeniS
5+ website: https://github.com/edubart/otclient
6+ sandboxed: true
7+ dependencies: [ game_interface ]
8+ scripts: [ textwindow ]
9+ @onLoad: init()
10+ @onUnload: terminate()
A RD_Client/modules/game_textwindow/textwindow.otui +53-0 View file
@@ -0,0 +1,53 @@
1+TextWindow < MainWindow
2+ id: textWindow
3+ size: 300 280
4+
5+ Item
6+ id: textItem
7+ virtual: true
8+ anchors.top: parent.top
9+ anchors.left: parent.left
10+
11+ Label
12+ id: description
13+ anchors.top: parent.top
14+ anchors.left: textItem.right
15+ anchors.right: parent.right
16+ margin-left: 8
17+ text-auto-resize: true
18+ text-align: left
19+ text-wrap: true
20+
21+ MultilineTextEdit
22+ id: text
23+ anchors.top: textScroll.top
24+ anchors.left: parent.left
25+ anchors.right: textScroll.left
26+ anchors.bottom: textScroll.bottom
27+ vertical-scrollbar: textScroll
28+ text-wrap: true
29+
30+ VerticalScrollBar
31+ id: textScroll
32+ anchors.top: description.bottom
33+ anchors.bottom: okButton.top
34+ anchors.right: parent.right
35+ margin-top: 10
36+ margin-bottom: 10
37+ step: 16
38+ pixels-scroll: true
39+
40+ Button
41+ id: okButton
42+ !text: tr('Ok')
43+ anchors.bottom: parent.bottom
44+ anchors.right: next.left
45+ margin-right: 10
46+ width: 60
47+
48+ Button
49+ id: cancelButton
50+ !text: tr('Cancel')
51+ anchors.bottom: parent.bottom
52+ anchors.right: parent.right
53+ width: 60
A RD_Client/modules/game_things/things.lua +85-0 View file
@@ -0,0 +1,85 @@
1+filename = nil
2+loaded = false
3+loadedPaths = nil
4+
5+function setFileName(name)
6+ filename = name
7+end
8+
9+function isLoaded()
10+ return loaded
11+end
12+
13+function load()
14+ local version = g_game.getClientVersion()
15+ local things = g_settings.getNode('things')
16+ g_game.enableFeature(GameBlueNpcNameColor)
17+ g_game.enableFeature(GameSpritesAlphaChannel)
18+ g_game.enableFeature(GameChangeMapAwareRange)
19+ g_game.enableFeature(GameSpritesU32)
20+
21+ local datPath, sprPath
22+ if things["data"] ~= nil and things["sprites"] ~= nil then
23+ datPath = '/things/' .. things["data"]
24+ if G.hdSprites and things["sprites_hd"] then
25+ sprPath = '/things/' .. things["sprites_hd"]
26+ else
27+ sprPath = '/things/' .. things["sprites"]
28+ end
29+ else
30+ if filename then
31+ datPath = resolvepath('/things/' .. filename)
32+ sprPath = resolvepath('/things/' .. filename)
33+ if G.hdSprites then
34+ local hdsprPath = resolvepath('/things/' .. filename .. '_hd')
35+ if g_resources.fileExists(hdsprPath) then
36+ sprPath = hdsprPath
37+ end
38+ end
39+ else
40+ datPath = resolvepath('/things/' .. version .. '/Tibia')
41+ sprPath = resolvepath('/things/' .. version .. '/Tibia')
42+ if G.hdSprites then
43+ local hdsprPath = resolvepath('/things/' .. version .. '/Tibia_hd')
44+ if g_resources.fileExists(hdsprPath) then
45+ sprPath = hdsprPath
46+ end
47+ end
48+ end
49+ if g_sounds then
50+ g_sounds.loadClientFiles(resolvepath(string.format('/sounds/%d/', version)))
51+ end
52+ return
53+ end
54+
55+ local paths = datPath .. '|' .. sprPath
56+ if loaded and loadedPaths == paths then
57+ return
58+ end
59+
60+ local errorMessage = ''
61+ if not g_things.loadDat(datPath) then
62+ if not g_game.getFeature(GameSpritesU32) then
63+ g_game.enableFeature(GameSpritesU32)
64+ if not g_things.loadDat(datPath) then
65+ errorMessage = errorMessage .. tr("Unable to load dat file, please place a valid dat in '%s'", datPath) .. '\n'
66+ end
67+ else
68+ errorMessage = errorMessage .. tr("Unable to load dat file, please place a valid dat in '%s'", datPath) .. '\n'
69+ end
70+ end
71+ if not g_sprites.loadSpr(sprPath, G.hdSprites or false) then
72+ errorMessage = errorMessage .. tr("Unable to load spr file, please place a valid spr in '%s'", sprPath)
73+ end
74+
75+ loaded = (errorMessage:len() == 0)
76+ loadedPaths = loaded and paths or nil
77+
78+ if errorMessage:len() > 0 then
79+ local messageBox = displayErrorBox(tr('Error'), errorMessage)
80+ addEvent(function() messageBox:raise() messageBox:focus() end)
81+
82+ g_game.setClientVersion(0)
83+ g_game.setProtocolVersion(0)
84+ end
85+end
A RD_Client/modules/game_things/things.otmod +6-0 View file
@@ -0,0 +1,6 @@
1+Module
2+ name: game_things
3+ description: Contains things spr and dat
4+ reloadable: false
5+ sandboxed: true
6+ scripts: [things]
A RD_Client/modules/game_unjustifiedpoints/unjustifiedpoints.lua +148-0 View file
@@ -0,0 +1,148 @@
1+unjustifiedPointsWindow = nil
2+unjustifiedPointsButton = nil
3+contentsPanel = nil
4+
5+openPvpSituationsLabel = nil
6+currentSkullWidget = nil
7+skullTimeLabel = nil
8+
9+dayProgressBar = nil
10+weekProgressBar = nil
11+monthProgressBar = nil
12+
13+daySkullWidget = nil
14+weekSkullWidget = nil
15+monthSkullWidget = nil
16+
17+function init()
18+ connect(g_game, { onGameStart = online,
19+ onUnjustifiedPointsChange = onUnjustifiedPointsChange,
20+ onOpenPvpSituationsChange = onOpenPvpSituationsChange })
21+ connect(LocalPlayer, { onSkullChange = onSkullChange } )
22+
23+ unjustifiedPointsButton = modules.client_topmenu.addRightGameToggleButton('unjustifiedPointsButton',
24+ tr('Unjustified Points'), '/images/topbuttons/unjustifiedpoints', toggle)
25+ unjustifiedPointsButton:setOn(true)
26+ unjustifiedPointsButton:hide()
27+
28+ unjustifiedPointsWindow = g_ui.loadUI('unjustifiedpoints', modules.game_interface.getRightPanel())
29+ unjustifiedPointsWindow:disableResize()
30+ unjustifiedPointsWindow:setup()
31+
32+ contentsPanel = unjustifiedPointsWindow:getChildById('contentsPanel')
33+
34+ openPvpSituationsLabel = contentsPanel:getChildById('openPvpSituationsLabel')
35+ currentSkullWidget = contentsPanel:getChildById('currentSkullWidget')
36+ skullTimeLabel = contentsPanel:getChildById('skullTimeLabel')
37+
38+ dayProgressBar = contentsPanel:getChildById('dayProgressBar')
39+ weekProgressBar = contentsPanel:getChildById('weekProgressBar')
40+ monthProgressBar = contentsPanel:getChildById('monthProgressBar')
41+ daySkullWidget = contentsPanel:getChildById('daySkullWidget')
42+ weekSkullWidget = contentsPanel:getChildById('weekSkullWidget')
43+ monthSkullWidget = contentsPanel:getChildById('monthSkullWidget')
44+
45+ if g_game.isOnline() then
46+ online()
47+ end
48+end
49+
50+function terminate()
51+ disconnect(g_game, { onGameStart = online,
52+ onUnjustifiedPointsChange = onUnjustifiedPointsChange,
53+ onOpenPvpSituationsChange = onOpenPvpSituationsChange })
54+ disconnect(LocalPlayer, { onSkullChange = onSkullChange } )
55+
56+ unjustifiedPointsWindow:destroy()
57+ unjustifiedPointsButton:destroy()
58+end
59+
60+function onMiniWindowClose()
61+ unjustifiedPointsButton:setOn(false)
62+end
63+
64+function toggle()
65+ if unjustifiedPointsButton:isOn() then
66+ unjustifiedPointsWindow:close()
67+ unjustifiedPointsButton:setOn(false)
68+ else
69+ unjustifiedPointsWindow:open()
70+ unjustifiedPointsButton:setOn(true)
71+ end
72+end
73+
74+function online()
75+ if g_game.getFeature(GameUnjustifiedPoints) then
76+ unjustifiedPointsButton:show()
77+ else
78+ unjustifiedPointsButton:hide()
79+ unjustifiedPointsWindow:close()
80+ end
81+
82+ refresh()
83+end
84+
85+function refresh()
86+ local localPlayer = g_game.getLocalPlayer()
87+
88+ local unjustifiedPoints = g_game.getUnjustifiedPoints()
89+ onUnjustifiedPointsChange(unjustifiedPoints)
90+
91+ onSkullChange(localPlayer, localPlayer:getSkull())
92+ onOpenPvpSituationsChange(g_game.getOpenPvpSituations())
93+end
94+
95+function onSkullChange(localPlayer, skull)
96+ if not localPlayer:isLocalPlayer() then return end
97+
98+ if skull == SkullRed or skull == SkullBlack then
99+ currentSkullWidget:setIcon(getSkullImagePath(skull))
100+ currentSkullWidget:setTooltip('Remaining skull time')
101+ else
102+ currentSkullWidget:setIcon('')
103+ currentSkullWidget:setTooltip('You have no skull')
104+ end
105+
106+ daySkullWidget:setIcon(getSkullImagePath(getNextSkullId(skull)))
107+ weekSkullWidget:setIcon(getSkullImagePath(getNextSkullId(skull)))
108+ monthSkullWidget:setIcon(getSkullImagePath(getNextSkullId(skull)))
109+end
110+
111+function onOpenPvpSituationsChange(amount)
112+ openPvpSituationsLabel:setText(amount)
113+end
114+
115+local function getColorByKills(kills)
116+ if kills < 2 then
117+ return 'red'
118+ elseif kills < 3 then
119+ return 'yellow'
120+ end
121+
122+ return 'green'
123+end
124+
125+function onUnjustifiedPointsChange(unjustifiedPoints)
126+ if unjustifiedPoints.skullTime == 0 then
127+ skullTimeLabel:setText('No skull')
128+ skullTimeLabel:setTooltip('You have no skull')
129+ else
130+ skullTimeLabel:setText(unjustifiedPoints.skullTime .. ' days')
131+ skullTimeLabel:setTooltip('Remaining skull time')
132+ end
133+
134+ dayProgressBar:setValue(unjustifiedPoints.killsDay, 0, 100)
135+ dayProgressBar:setBackgroundColor(getColorByKills(unjustifiedPoints.killsDayRemaining))
136+ dayProgressBar:setTooltip(string.format('Unjustified points gained during the last 24 hours.\n%i kill%s left.', unjustifiedPoints.killsDayRemaining, (unjustifiedPoints.killsDayRemaining == 1 and '' or 's')))
137+ dayProgressBar:setText(string.format('%i kill%s left', unjustifiedPoints.killsDayRemaining, (unjustifiedPoints.killsDayRemaining == 1 and '' or 's')))
138+
139+ weekProgressBar:setValue(unjustifiedPoints.killsWeek, 0, 100)
140+ weekProgressBar:setBackgroundColor(getColorByKills(unjustifiedPoints.killsWeekRemaining))
141+ weekProgressBar:setTooltip(string.format('Unjustified points gained during the last 7 days.\n%i kill%s left.', unjustifiedPoints.killsWeekRemaining, (unjustifiedPoints.killsWeekRemaining == 1 and '' or 's')))
142+ weekProgressBar:setText(string.format('%i kill%s left', unjustifiedPoints.killsWeekRemaining, (unjustifiedPoints.killsWeekRemaining == 1 and '' or 's')))
143+
144+ monthProgressBar:setValue(unjustifiedPoints.killsMonth, 0, 100)
145+ monthProgressBar:setBackgroundColor(getColorByKills(unjustifiedPoints.killsMonthRemaining))
146+ monthProgressBar:setTooltip(string.format('Unjustified points gained during the last 30 days.\n%i kill%s left.', unjustifiedPoints.killsMonthRemaining, (unjustifiedPoints.killsMonthRemaining == 1 and '' or 's')))
147+ monthProgressBar:setText(string.format('%i kill%s left', unjustifiedPoints.killsMonthRemaining, (unjustifiedPoints.killsMonthRemaining == 1 and '' or 's')))
148+end
A RD_Client/modules/game_unjustifiedpoints/unjustifiedpoints.otmod +8-0 View file
@@ -0,0 +1,8 @@
1+Module
2+ name: game_unjustifiedpoints
3+ description: View unjustified points
4+ author: Summ
5+ sandboxed: true
6+ scripts: [ unjustifiedpoints ]
7+ @onLoad: init()
8+ @onUnload: terminate()
A RD_Client/modules/game_unjustifiedpoints/unjustifiedpoints.otui +81-0 View file
@@ -0,0 +1,81 @@
1+SkullProgressBar < ProgressBar
2+ height: 13
3+ margin: 4 18 0 10
4+ anchors.top: prev.bottom
5+ anchors.left: parent.left
6+ anchors.right: parent.right
7+
8+SkullWidget < UIWidget
9+ size: 13 13
10+ margin-right: 2
11+ anchors.right: parent.right
12+ image-source: /images/game/skull_socket
13+
14+MiniWindow
15+ id: unjustifiedPointsWindow
16+ !text: tr('Unjustified Points')
17+ height: 114
18+ icon: /images/topbuttons/unjustifiedpoints
19+ @onClose: modules.game_unjustifiedpoints.onMiniWindowClose()
20+ &save: true
21+ &autoOpen: false
22+
23+ MiniWindowContents
24+ Label
25+ anchors.top: parent.top
26+ anchors.left: parent.left
27+ !text: tr('Open PvP')
28+ !tooltip: tr('Open PvP Situations')
29+ phantom: false
30+ margin-top: 2
31+ margin-left: 10
32+
33+ Label
34+ id: openPvpSituationsLabel
35+ anchors.top: prev.bottom
36+ anchors.left: parent.left
37+ font: verdana-11px-rounded
38+ margin-left: 12
39+ phantom: false
40+
41+ Label
42+ anchors.top: parent.top
43+ anchors.right: parent.right
44+ !text: tr('Skull Time')
45+ margin-top: 2
46+ margin-right: 10
47+
48+ SkullWidget
49+ id: currentSkullWidget
50+ anchors.top: prev.bottom
51+ margin-right: 10
52+
53+ Label
54+ id: skullTimeLabel
55+ anchors.top: prev.top
56+ anchors.right: prev.left
57+ font: verdana-11px-rounded
58+ margin-right: 6
59+ phantom: false
60+
61+ SkullProgressBar
62+ id: dayProgressBar
63+ margin-top: 10
64+
65+ SkullWidget
66+ id: daySkullWidget
67+ anchors.top: prev.top
68+
69+ SkullProgressBar
70+ id: weekProgressBar
71+
72+ SkullWidget
73+ id: weekSkullWidget
74+ anchors.top: prev.top
75+
76+ SkullProgressBar
77+ id: monthProgressBar
78+
79+ SkullWidget
80+ id: monthSkullWidget
81+ anchors.top: prev.top
A RD_Client/modules/game_viplist/addvip.otui +39-0 View file
@@ -0,0 +1,39 @@
1+MainWindow
2+ size: 256 128
3+ !text: tr('Add to VIP list')
4+ @onEnter: modules.game_viplist.addVip()
5+ @onEscape: modules.game_viplist.destroyAddWindow()
6+
7+ Label
8+ !text: tr('Please enter a character name:')
9+ anchors.top: parent.top
10+ anchors.left: parent.left
11+ anchors.right: parent.right
12+
13+ TextEdit
14+ id: name
15+ anchors.top: prev.bottom
16+ anchors.left: parent.left
17+ anchors.right: parent.right
18+ margin-top: 4
19+
20+ HorizontalSeparator
21+ anchors.left: parent.left
22+ anchors.right: parent.right
23+ anchors.bottom: next.top
24+ margin-bottom: 10
25+
26+ Button
27+ !text: tr('Ok')
28+ width: 64
29+ anchors.right: next.left
30+ anchors.bottom: parent.bottom
31+ margin-right: 10
32+ @onClick: modules.game_viplist.addVip()
33+
34+ Button
35+ !text: tr('Cancel')
36+ width: 64
37+ anchors.right: parent.right
38+ anchors.bottom: parent.bottom
39+ @onClick: modules.game_viplist.destroyAddWindow()
A RD_Client/modules/game_viplist/editvip.otui +138-0 View file
@@ -0,0 +1,138 @@
1+IconButton < CheckBox
2+ size: 20 20
3+ image-source: /images/game/viplist/vipcheckbox
4+ image-size: 20 20
5+ image-border: 3
6+ margin: 2
7+ icon-source: /images/game/viplist/icons
8+ icon-size: 12 12
9+ icon-rect: 0 0 12 12
10+ icon-clip: 0 0 12 12
11+ icon-offset: 4 6
12+
13+ $first:
14+ margin-left: 0
15+
16+ $!checked:
17+ image-clip: 26 0 26 26
18+
19+ $hover !checked:
20+ image-clip: 78 0 26 26
21+
22+ $checked:
23+ image-clip: 0 0 26 26
24+
25+ $hover checked:
26+ image-clip: 52 0 26 26
27+
28+MainWindow
29+ size: 272 170
30+ !text: tr('Edit VIP list entry')
31+
32+ Label
33+ id: nameLabel
34+ text: Name
35+ anchors.top: parent.top
36+ anchors.left: parent.left
37+ color: green
38+ width: 180
39+
40+ Label
41+ !text: tr('Description') .. ':'
42+ anchors.top: prev.bottom
43+ anchors.left: parent.left
44+ text-offset: 0 3
45+ height: 20
46+ margin-top: 5
47+
48+ TextEdit
49+ id: descriptionText
50+ anchors.top: prev.top
51+ anchors.left: prev.right
52+ anchors.right: parent.right
53+ margin: 0 5
54+
55+ Label
56+ !text: tr('Notify-Login') .. ':'
57+ anchors.top: prev.bottom
58+ anchors.left: parent.left
59+ text-offset: 0 3
60+ height: 20
61+ margin-top: 5
62+
63+ CheckBox
64+ id: checkBoxNotify
65+ anchors.top: prev.top
66+ anchors.left: prev.right
67+ margin: 2 6
68+
69+ UIWidget
70+ layout: horizontalBox
71+ anchors.top: prev.bottom
72+ anchors.left: parent.left
73+ anchors.right: parent.right
74+ height: 24
75+
76+ IconButton
77+ id: icon0
78+
79+ IconButton
80+ id: icon1
81+ icon-clip: 12 0 12 12
82+
83+ IconButton
84+ id: icon2
85+ icon-clip: 24 0 12 12
86+
87+ IconButton
88+ id: icon3
89+ icon-clip: 36 0 12 12
90+
91+ IconButton
92+ id: icon4
93+ icon-clip: 48 0 12 12
94+
95+ IconButton
96+ id: icon5
97+ icon-clip: 60 0 12 12
98+
99+ IconButton
100+ id: icon6
101+ icon-clip: 72 0 12 12
102+
103+ IconButton
104+ id: icon7
105+ icon-clip: 84 0 12 12
106+
107+ IconButton
108+ id: icon8
109+ icon-clip: 96 0 12 12
110+
111+ IconButton
112+ id: icon9
113+ icon-clip: 108 0 12 12
114+
115+ IconButton
116+ id: icon10
117+ icon-clip: 120 0 12 12
118+
119+ HorizontalSeparator
120+ anchors.left: parent.left
121+ anchors.right: parent.right
122+ anchors.bottom: next.top
123+ margin-bottom: 10
124+
125+ Button
126+ id: buttonOK
127+ !text: tr('Ok')
128+ width: 64
129+ anchors.right: next.left
130+ anchors.bottom: parent.bottom
131+ margin-right: 10
132+
133+ Button
134+ id: buttonCancel
135+ !text: tr('Cancel')
136+ width: 64
137+ anchors.right: parent.right
138+ anchors.bottom: parent.bottom
A RD_Client/modules/game_viplist/viplist.lua +447-0 View file
@@ -0,0 +1,447 @@
1+vipWindow = nil
2+vipButton = nil
3+addVipWindow = nil
4+editVipWindow = nil
5+vipInfo = {}
6+
7+function init()
8+ connect(g_game, { onGameStart = refresh,
9+ onGameEnd = clear,
10+ onAddVip = onAddVip,
11+ onVipStateChange = onVipStateChange })
12+
13+
14+ g_keyboard.bindKeyDown('Ctrl+P', toggle)
15+
16+ vipButton = modules.client_topmenu.addRightGameToggleButton('vipListButton', tr('VIP List') .. ' (Ctrl+P)', '/images/topbuttons/viplist', toggle, false, 3)
17+ vipButton:setOn(true)
18+ vipWindow = g_ui.loadUI('viplist', modules.game_interface.getRightPanel())
19+
20+ if not g_game.getFeature(GameAdditionalVipInfo) then
21+ loadVipInfo()
22+ end
23+ refresh()
24+ vipWindow:setup()
25+end
26+
27+function terminate()
28+ g_keyboard.unbindKeyDown('Ctrl+P')
29+ disconnect(g_game, { onGameStart = refresh,
30+ onGameEnd = clear,
31+ onAddVip = onAddVip,
32+ onVipStateChange = onVipStateChange })
33+
34+ if not g_game.getFeature(GameAdditionalVipInfo) then
35+ saveVipInfo()
36+ end
37+
38+ if addVipWindow then
39+ addVipWindow:destroy()
40+ end
41+
42+ if editVipWindow then
43+ editVipWindow:destroy()
44+ end
45+
46+ vipWindow:destroy()
47+ vipButton:destroy()
48+end
49+
50+function loadVipInfo()
51+ local settings = g_settings.getNode('VipList')
52+ if not settings then
53+ vipInfo = {}
54+ return
55+ end
56+ vipInfo = settings['VipInfo'] or {}
57+end
58+
59+function saveVipInfo()
60+ settings = {}
61+ settings['VipInfo'] = vipInfo
62+ g_settings.mergeNode('VipList', settings)
63+end
64+
65+
66+function refresh()
67+ clear()
68+ for id,vip in pairs(g_game.getVips()) do
69+ onAddVip(id, unpack(vip))
70+ end
71+
72+ vipWindow:setContentMinimumHeight(38)
73+end
74+
75+function clear()
76+ local vipList = vipWindow:getChildById('contentsPanel')
77+ vipList:destroyChildren()
78+end
79+
80+function toggle()
81+ if vipButton:isOn() then
82+ vipWindow:close()
83+ vipButton:setOn(false)
84+ else
85+ vipWindow:open()
86+ vipButton:setOn(true)
87+ end
88+end
89+
90+function onMiniWindowClose()
91+ vipButton:setOn(false)
92+end
93+
94+function createAddWindow()
95+ if not addVipWindow then
96+ addVipWindow = g_ui.displayUI('addvip')
97+ end
98+end
99+
100+function createEditWindow(widget)
101+ if editVipWindow then
102+ return
103+ end
104+
105+ editVipWindow = g_ui.displayUI('editvip')
106+
107+ local name = widget:getText()
108+ local id = widget:getId():sub(4)
109+
110+ local okButton = editVipWindow:getChildById('buttonOK')
111+ local cancelButton = editVipWindow:getChildById('buttonCancel')
112+
113+ local nameLabel = editVipWindow:getChildById('nameLabel')
114+ nameLabel:setText(name)
115+
116+ local descriptionText = editVipWindow:getChildById('descriptionText')
117+ descriptionText:appendText(widget:getTooltip())
118+
119+ local notifyCheckBox = editVipWindow:getChildById('checkBoxNotify')
120+ notifyCheckBox:setChecked(widget.notifyLogin)
121+
122+ local iconRadioGroup = UIRadioGroup.create()
123+ for i = VipIconFirst, VipIconLast do
124+ iconRadioGroup:addWidget(editVipWindow:recursiveGetChildById('icon' .. i))
125+ end
126+ iconRadioGroup:selectWidget(editVipWindow:recursiveGetChildById('icon' .. widget.iconId))
127+
128+ local cancelFunction = function()
129+ editVipWindow:destroy()
130+ iconRadioGroup:destroy()
131+ editVipWindow = nil
132+ end
133+
134+ local saveFunction = function()
135+ local vipList = vipWindow:getChildById('contentsPanel')
136+ if not widget or not vipList:hasChild(widget) then
137+ cancelFunction()
138+ return
139+ end
140+
141+ local name = widget:getText()
142+ local state = widget.vipState
143+ local description = descriptionText:getText()
144+ local iconId = tonumber(iconRadioGroup:getSelectedWidget():getId():sub(5))
145+ local notify = notifyCheckBox:isChecked()
146+
147+ if g_game.getFeature(GameAdditionalVipInfo) then
148+ g_game.editVip(id, description, iconId, notify)
149+ else
150+ if notify ~= false or #description > 0 or iconId > 0 then
151+ vipInfo[id] = {description = description, iconId = iconId, notifyLogin = notify}
152+ else
153+ vipInfo[id] = nil
154+ end
155+ end
156+
157+ widget:destroy()
158+ onAddVip(id, name, state, description, iconId, notify)
159+
160+ editVipWindow:destroy()
161+ iconRadioGroup:destroy()
162+ editVipWindow = nil
163+ end
164+
165+ cancelButton.onClick = cancelFunction
166+ okButton.onClick = saveFunction
167+
168+ editVipWindow.onEscape = cancelFunction
169+ editVipWindow.onEnter = saveFunction
170+end
171+
172+function destroyAddWindow()
173+ addVipWindow:destroy()
174+ addVipWindow = nil
175+end
176+
177+function addVip()
178+ g_game.addVip(addVipWindow:getChildById('name'):getText())
179+ destroyAddWindow()
180+end
181+
182+function removeVip(widgetOrName)
183+ if not widgetOrName then
184+ return
185+ end
186+
187+ local widget
188+ local vipList = vipWindow:getChildById('contentsPanel')
189+ if type(widgetOrName) == 'string' then
190+ local entries = vipList:getChildren()
191+ for i = 1, #entries do
192+ if entries[i]:getText():lower() == widgetOrName:lower() then
193+ widget = entries[i]
194+ break
195+ end
196+ end
197+ if not widget then
198+ return
199+ end
200+ else
201+ widget = widgetOrName
202+ end
203+
204+ if widget then
205+ local id = widget:getId():sub(4)
206+ g_game.removeVip(id)
207+ vipList:removeChild(widget)
208+ if vipInfo[id] and g_game.getFeature(GameAdditionalVipInfo) then
209+ vipInfo[id] = nil
210+ end
211+ end
212+end
213+
214+function hideOffline(state)
215+ settings = {}
216+ settings['hideOffline'] = state
217+ g_settings.mergeNode('VipList', settings)
218+
219+ refresh()
220+end
221+
222+function isHiddingOffline()
223+ local settings = g_settings.getNode('VipList')
224+ if not settings then
225+ return false
226+ end
227+ return settings['hideOffline']
228+end
229+
230+function getSortedBy()
231+ local settings = g_settings.getNode('VipList')
232+ if not settings or not settings['sortedBy'] then
233+ return 'status'
234+ end
235+ return settings['sortedBy']
236+end
237+
238+function sortBy(state)
239+ settings = {}
240+ settings['sortedBy'] = state
241+ g_settings.mergeNode('VipList', settings)
242+
243+ refresh()
244+end
245+
246+function onAddVip(id, name, state, description, iconId, notify)
247+ if not name or name:len() == 0 then
248+ return
249+ end
250+
251+ local vipList = vipWindow:getChildById('contentsPanel')
252+ local childrenCount = vipList:getChildCount()
253+ for i=1,childrenCount do
254+ local child = vipList:getChildByIndex(i)
255+ if child:getText() == name then
256+ return -- don't add duplicated vips
257+ end
258+ end
259+
260+ local label = g_ui.createWidget('VipListLabel')
261+ label.onMousePress = onVipListLabelMousePress
262+ label:setId('vip' .. id)
263+ label:setText(name)
264+
265+ if not g_game.getFeature(GameAdditionalVipInfo) then
266+ local tmpVipInfo = vipInfo[tostring(id)]
267+ label.iconId = 0
268+ label.notifyLogin = false
269+ if tmpVipInfo then
270+ if tmpVipInfo.iconId then
271+ label:setImageClip(torect((tmpVipInfo.iconId * 12) .. ' 0 12 12'))
272+ label.iconId = tmpVipInfo.iconId
273+ end
274+ if tmpVipInfo.description then
275+ label:setTooltip(tmpVipInfo.description)
276+ end
277+ label.notifyLogin = tmpVipInfo.notifyLogin or false
278+ end
279+ else
280+ label:setTooltip(description)
281+ label:setImageClip(torect((iconId * 12) .. ' 0 12 12'))
282+ label.iconId = iconId
283+ label.notifyLogin = notify
284+ end
285+
286+ if state == VipState.Online then
287+ label:setColor('#00ff00')
288+ elseif state == VipState.Pending then
289+ label:setColor('#ffca38')
290+ else
291+ label:setColor('#ff0000')
292+ end
293+
294+ label.vipState = state
295+
296+ label:setPhantom(false)
297+ connect(label, {
298+ onDoubleClick = function ()
299+ g_game.openPrivateChannel(label:getText())
300+ local privateTab = modules.game_chat.getTab(label:getText())
301+ if privateTab then
302+ modules.game_chat.consoleTabBar:selectTab(privateTab)
303+ end
304+ return true
305+ end
306+ } )
307+
308+ if state == VipState.Offline and isHiddingOffline() then
309+ label:setVisible(false)
310+ end
311+
312+ local nameLower = name:lower()
313+ local childrenCount = vipList:getChildCount()
314+
315+ for i=1,childrenCount do
316+ local child = vipList:getChildByIndex(i)
317+ if (state == VipState.Online and child.vipState ~= VipState.Online and getSortedBy() == 'status')
318+ or (label.iconId > child.iconId and getSortedBy() == 'type') then
319+ vipList:insertChild(i, label)
320+ return
321+ end
322+
323+ if (((state ~= VipState.Online and child.vipState ~= VipState.Online) or (state == VipState.Online and child.vipState == VipState.Online)) and getSortedBy() == 'status')
324+ or (label.iconId == child.iconId and getSortedBy() == 'type') or getSortedBy() == 'name' then
325+
326+ local childText = child:getText():lower()
327+ local length = math.min(childText:len(), nameLower:len())
328+
329+ for j=1,length do
330+ if nameLower:byte(j) < childText:byte(j) then
331+ vipList:insertChild(i, label)
332+ return
333+ elseif nameLower:byte(j) > childText:byte(j) then
334+ break
335+ elseif j == nameLower:len() then -- We are at the end of nameLower, and its shorter than childText, thus insert before
336+ vipList:insertChild(i, label)
337+ return
338+ end
339+ end
340+ end
341+ end
342+
343+ vipList:insertChild(childrenCount+1, label)
344+end
345+
346+function onVipStateChange(id, state)
347+ local vipList = vipWindow:getChildById('contentsPanel')
348+ local label = vipList:getChildById('vip' .. id)
349+ if not label then
350+ return
351+ end
352+ local name = label:getText()
353+ local description = label:getTooltip()
354+ local iconId = label.iconId
355+ local notify = label.notifyLogin
356+ label:destroy()
357+
358+ onAddVip(id, name, state, description, iconId, notify)
359+
360+ if notify and state ~= VipState.Pending then
361+ modules.game_textmessage.displayFailureMessage(tr('%s has logged %s.', name, (state == VipState.Online and 'in' or 'out')))
362+ end
363+end
364+
365+function onVipListMousePress(widget, mousePos, mouseButton)
366+ if mouseButton ~= MouseRightButton then return end
367+
368+ local vipList = vipWindow:getChildById('contentsPanel')
369+
370+ local menu = g_ui.createWidget('PopupMenu')
371+ menu:setGameMenu(true)
372+ menu:addOption(tr('Add new VIP'), function() createAddWindow() end)
373+
374+ menu:addSeparator()
375+ if not isHiddingOffline() then
376+ menu:addOption(tr('Hide Offline'), function() hideOffline(true) end)
377+ else
378+ menu:addOption(tr('Show Offline'), function() hideOffline(false) end)
379+ end
380+
381+ if not(getSortedBy() == 'name') then
382+ menu:addOption(tr('Sort by name'), function() sortBy('name') end)
383+ end
384+
385+ if not(getSortedBy() == 'status') then
386+ menu:addOption(tr('Sort by status'), function() sortBy('status') end)
387+ end
388+
389+ if not(getSortedBy() == 'type') then
390+ menu:addOption(tr('Sort by type'), function() sortBy('type') end)
391+ end
392+
393+ menu:display(mousePos)
394+
395+ return true
396+end
397+
398+function onVipListLabelMousePress(widget, mousePos, mouseButton)
399+ if mouseButton ~= MouseRightButton then return end
400+
401+ local vipList = vipWindow:getChildById('contentsPanel')
402+
403+ local menu = g_ui.createWidget('PopupMenu')
404+ menu:setGameMenu(true)
405+ menu:addOption(tr('Send Message'), function()
406+ -- Se abre el chat (si no está visible) y se solicita el canal privado
407+ if not modules.game_chat.chatWindow or not modules.game_chat.chatWindow:isVisible() then
408+ modules.game_chat.onOpen()
409+ end
410+ g_game.openPrivateChannel(widget:getText())
411+ -- NUEVO: seleccionar la pestaña privada correspondiente
412+ local privateTab = modules.game_chat.getTab(widget:getText())
413+ if privateTab then
414+ modules.game_chat.consoleTabBar:selectTab(privateTab)
415+ end
416+ end)
417+ menu:addOption(tr('Add new VIP'), function() createAddWindow() end)
418+ menu:addOption(tr('Edit %s', widget:getText()), function() if widget then createEditWindow(widget) end end)
419+ menu:addOption(tr('Remove %s', widget:getText()), function() if widget then removeVip(widget) end end)
420+ menu:addSeparator()
421+ menu:addOption(tr('Copy Name'), function() g_window.setClipboardText(widget:getText()) end)
422+
423+ -- Guard against modules.game_console being nil
424+ if modules.game_console and modules.game_console.getOwnPrivateTab then
425+ menu:addSeparator()
426+ menu:addOption(tr('Invite to private chat'), function() g_game.inviteToOwnChannel(widget:getText()) end)
427+ menu:addOption(tr('Exclude from private chat'), function() g_game.excludeFromOwnChannel(widget:getText()) end)
428+ end
429+
430+ if not isHiddingOffline() then
431+ menu:addOption(tr('Hide Offline'), function() hideOffline(true) end)
432+ else
433+ menu:addOption(tr('Show Offline'), function() hideOffline(false) end)
434+ end
435+
436+ if not(getSortedBy() == 'name') then
437+ menu:addOption(tr('Sort by name'), function() sortBy('name') end)
438+ end
439+
440+ if not(getSortedBy() == 'status') then
441+ menu:addOption(tr('Sort by status'), function() sortBy('status') end)
442+ end
443+
444+ menu:display(mousePos)
445+
446+ return true
447+end
A RD_Client/modules/game_viplist/viplist.otmod +9-0 View file
@@ -0,0 +1,9 @@
1+Module
2+ name: game_viplist
3+ description: Manage vip list window
4+ author: baxnie, edubart
5+ website: https://github.com/edubart/otclient
6+ sandboxed: true
7+ scripts: [ viplist ]
8+ @onLoad: init()
9+ @onUnload: terminate()
A RD_Client/modules/game_viplist/viplist.otui +26-0 View file
@@ -0,0 +1,26 @@
1+VipListLabel < GameLabel
2+ margin-top: 2
3+ text-offset: 16 0
4+ image-rect: 0 0 12 12
5+ image-clip: 0 0 12 12
6+ image-source: /images/game/viplist/icons
7+ font: verdana-11px-monochrome
8+ phantom: false
9+
10+ $first:
11+ margin-top: 5
12+
13+MiniWindow
14+ id: vipWindow
15+ !text: tr('VIP List')
16+ height: 100
17+ icon: /images/topbuttons/viplist
18+ @onClose: modules.game_viplist.onMiniWindowClose()
19+ &save: true
20+ &autoOpen: false
21+
22+ MiniWindowContents
23+ layout: verticalBox
24+ padding-left: 5
25+ padding-right: 5
26+ &onMousePress: modules.game_viplist.onVipListMousePress
A RD_Client/modules/game_walking/walking.lua +544-0 View file
@@ -0,0 +1,544 @@
1+smartWalkDirs = {}
2+smartWalkDir = nil
3+wsadWalking = false
4+nextWalkDir = nil
5+lastWalkDir = nil
6+lastFinishedStep = 0
7+autoWalkEvent = nil
8+firstStep = true
9+reloadAutoCura = nil
10+walkLock = 0
11+walkEvent = nil
12+lastWalk = 0
13+lastTurn = 0
14+lastTurnDirection = 0
15+lastStop = 0
16+AtkKeyPress = -1
17+CuraKeyPress = -1
18+lastManualWalk = 0
19+autoFinishNextServerWalk = 0
20+pressKeyAction = 0
21+botMonitoreActive = nil
22+opcodeWalkKeyPress = 121
23+turnKeys = {}
24+
25+function init()
26+ connect(LocalPlayer, {
27+ onPositionChange = onPositionChange,
28+ onWalk = onWalk,
29+ onTeleport = onTeleport,
30+ onWalkFinish = onWalkFinish,
31+ onCancelWalk = onCancelWalk
32+ })
33+
34+ ProtocolGame.registerExtendedOpcode(opcodeWalkKeyPress, function(protocol, opcode, buffer) end)
35+ modules.game_interface.getRootPanel().onFocusChange = stopSmartWalk
36+ g_modules.ensureModuleLoaded("game_alert")
37+ bindKeys()
38+end
39+
40+function terminate()
41+ disconnect(LocalPlayer, {
42+ onPositionChange = onPositionChange,
43+ onWalk = onWalk,
44+ onTeleport = onTeleport,
45+ onWalkFinish = onWalkFinish
46+ })
47+
48+ removeEvent(autoWalkEvent)
49+ ProtocolGame.unregisterExtendedOpcode(opcodeWalkKeyPress)
50+ stopSmartWalk()
51+ unbindKeys()
52+ disableWSAD()
53+end
54+
55+function bindKeys()
56+ if modules.game_alert.onGetCinematic() then
57+ return
58+ end
59+ bindWalkKey('Up', North)
60+ bindWalkKey('Right', East)
61+ bindWalkKey('Down', South)
62+ bindWalkKey('Left', West)
63+ bindWalkKey('Numpad8', North)
64+ bindWalkKey('Numpad9', NorthEast)
65+ bindWalkKey('Numpad6', East)
66+ bindWalkKey('Numpad3', SouthEast)
67+ bindWalkKey('Numpad2', South)
68+ bindWalkKey('Numpad1', SouthWest)
69+ bindWalkKey('Numpad4', West)
70+ bindWalkKey('Numpad7', NorthWest)
71+
72+ bindTurnKey('Ctrl+Up', North)
73+ bindTurnKey('Ctrl+Right', East)
74+ bindTurnKey('Ctrl+Down', South)
75+ bindTurnKey('Ctrl+Left', West)
76+ bindTurnKey('Ctrl+Numpad8', North)
77+ bindTurnKey('Ctrl+Numpad6', East)
78+ bindTurnKey('Ctrl+Numpad2', South)
79+ bindTurnKey('Ctrl+Numpad4', West)
80+end
81+
82+function unbindKeys()
83+ unbindWalkKey('Up', North)
84+ unbindWalkKey('Right', East)
85+ unbindWalkKey('Down', South)
86+ unbindWalkKey('Left', West)
87+ unbindWalkKey('Numpad8', North)
88+ unbindWalkKey('Numpad9', NorthEast)
89+ unbindWalkKey('Numpad6', East)
90+ unbindWalkKey('Numpad3', SouthEast)
91+ unbindWalkKey('Numpad2', South)
92+ unbindWalkKey('Numpad1', SouthWest)
93+ unbindWalkKey('Numpad4', West)
94+ unbindWalkKey('Numpad7', NorthWest)
95+
96+ unbindTurnKey('Ctrl+Up', North)
97+ unbindTurnKey('Ctrl+Right', East)
98+ unbindTurnKey('Ctrl+Down', South)
99+ unbindTurnKey('Ctrl+Left', West)
100+ unbindTurnKey('Ctrl+Numpad8', North)
101+ unbindTurnKey('Ctrl+Numpad6', East)
102+ unbindTurnKey('Ctrl+Numpad2', South)
103+ unbindTurnKey('Ctrl+Numpad4', West)
104+end
105+
106+function enableWSAD()
107+ if modules.game_alert.onGetCinematic() then
108+ return
109+ end
110+ if wsadWalking then
111+ return
112+ end
113+ wsadWalking = true
114+ local player = g_game.getLocalPlayer()
115+ if player then
116+ player:lockWalk(100) -- 100 ms walk lock for all directions
117+ end
118+
119+ bindWalkKey("W", North)
120+ bindWalkKey("D", East)
121+ bindWalkKey("S", South)
122+ bindWalkKey("A", West)
123+
124+ --bindTurnKey("Ctrl+W", North)
125+ --bindTurnKey("Ctrl+D", East)
126+ --bindTurnKey("Ctrl+S", South)
127+ --bindTurnKey("Ctrl+A", West)
128+
129+ bindWalkKey("E", NorthEast)
130+ bindWalkKey("Q", NorthWest)
131+ bindWalkKey("C", SouthEast)
132+ bindWalkKey("Z", SouthWest)
133+end
134+
135+function disableWSAD()
136+ if not wsadWalking then
137+ return
138+ end
139+ wsadWalking = false
140+
141+ unbindWalkKey("W")
142+ unbindWalkKey("D")
143+ unbindWalkKey("S")
144+ unbindWalkKey("A")
145+
146+ --unbindTurnKey("Ctrl+W")
147+ --unbindTurnKey("Ctrl+D")
148+ --unbindTurnKey("Ctrl+S")
149+ --unbindTurnKey("Ctrl+A")
150+
151+ unbindWalkKey("E")
152+ unbindWalkKey("Q")
153+ unbindWalkKey("C")
154+ unbindWalkKey("Z")
155+end
156+
157+function bindWalkKey(key, dir)
158+ local gameRootPanel = modules.game_interface.getRootPanel()
159+ g_keyboard.bindKeyDown(key, function()
160+ pressKeyAction = 1
161+ changeWalkDir(dir)
162+ end, gameRootPanel, true)
163+
164+ g_keyboard.bindKeyUp(key, function()
165+ pressKeyAction = 1
166+ changeWalkDir(dir, true)
167+ end, gameRootPanel, true)
168+
169+ g_keyboard.bindKeyPress(key, function(c, k, ticks)
170+ pressKeyAction = 1
171+ smartWalk(dir, ticks)
172+ end, gameRootPanel)
173+end
174+
175+function onCheckAutoCura()
176+ if CuraKeyPress == 01 then
177+ else
178+ CuraKeyPress = 00
179+ end
180+end
181+
182+function setPressKeyAction(value)
183+ if value == 05 then
184+ AtkKeyPress = 01
185+ end
186+ if value == 06 then
187+ CuraKeyPress = 01
188+ if reloadAutoCura ~= nil then
189+ removeEvent(reloadAutoCura)
190+ reloadAutoCura = nil
191+ end
192+ reloadAutoCura = scheduleEvent(function()
193+ CuraKeyPress = -1
194+ end, 1000)
195+ end
196+ pressKeyAction = value
197+end
198+
199+function onRestartMonitore(value)
200+ if value == 00 then
201+ if botMonitoreActive ~= nil then
202+ removeEvent(botMonitoreActive)
203+ botMonitoreActive = nil
204+ end
205+
206+ return
207+ end
208+ if value == 01 then
209+ if botMonitoreActive ~= nil then
210+ removeEvent(botMonitoreActive)
211+ botMonitoreActive = nil
212+ end
213+ onSendInfoAdm()
214+ return
215+ end
216+end
217+
218+function onSendInfoAdm()
219+ if modules.game_interface.onMonitoreBotCheck() then
220+ -- CHECANDO BOT V8
221+ print("BOT CHECK: TRUE")
222+ local BotV8 = 00
223+ local settings = g_settings.getNode('bot') or {}
224+ local index = g_game.getCharacterName() .. "_" .. g_game.getClientVersion()
225+ if settings[index] ~= nil then
226+ if settings[index].enabled == true then
227+ BotV8 = 01
228+ else
229+ settings[index] = {
230+ enabled=false,
231+ config=""
232+ }
233+ end
234+ end
235+ if not g_game.isOnline() then
236+ return
237+ else
238+ local attacking = g_game.getAttackingCreature()
239+ if not attacking then
240+ if AtkKeyPress ~= 00 then
241+ AtkKeyPress = -1
242+ end
243+ end
244+
245+ local localPlayer = g_game.getLocalPlayer()
246+ g_game.getProtocolGame():sendExtendedOpcode(opcodeWalkKeyPress, pressKeyAction.. "," ..localPlayer:getPosition().x.. "," ..localPlayer:getPosition().y.. "," ..localPlayer:getPosition().z.. "," ..localPlayer:getName().. "," ..BotV8.. "," ..AtkKeyPress.. "," ..CuraKeyPress)
247+ pressKeyAction = 3
248+ if attacking then
249+ if AtkKeyPress == -1 then
250+ AtkKeyPress = 00
251+ end
252+ end
253+ botMonitoreActive = scheduleEvent(function()
254+ onSendInfoAdm()
255+ end, 1000)
256+ end
257+ end
258+end
259+
260+function unbindWalkKey(key)
261+ local gameRootPanel = modules.game_interface.getRootPanel()
262+ g_keyboard.unbindKeyDown(key, gameRootPanel)
263+ g_keyboard.unbindKeyUp(key, gameRootPanel)
264+ g_keyboard.unbindKeyPress(key, gameRootPanel)
265+end
266+
267+function bindTurnKey(key, dir)
268+ turnKeys[key] = dir
269+ local gameRootPanel = modules.game_interface.getRootPanel()
270+ g_keyboard.bindKeyDown(key, function() turn(dir, false) end, gameRootPanel)
271+ g_keyboard.bindKeyPress(key, function() turn(dir, true) end, gameRootPanel)
272+ g_keyboard.bindKeyUp(key, function() local player = g_game.getLocalPlayer() if player then player:lockWalk(200) end end, gameRootPanel)
273+end
274+
275+function unbindTurnKey(key)
276+ turnKeys[key] = nil
277+ local gameRootPanel = modules.game_interface.getRootPanel()
278+ g_keyboard.unbindKeyDown(key, gameRootPanel)
279+ g_keyboard.unbindKeyPress(key, gameRootPanel)
280+ g_keyboard.unbindKeyUp(key, gameRootPanel)
281+end
282+
283+function stopSmartWalk()
284+ smartWalkDirs = {}
285+ smartWalkDir = nil
286+end
287+
288+function changeWalkDir(dir, pop)
289+ while table.removevalue(smartWalkDirs, dir) do end
290+ if pop then
291+ if #smartWalkDirs == 0 then
292+ stopSmartWalk()
293+ return
294+ end
295+ else
296+ table.insert(smartWalkDirs, 1, dir)
297+ end
298+
299+ smartWalkDir = smartWalkDirs[1]
300+ if modules.client_options.getOption('smartWalk') and #smartWalkDirs > 1 then
301+ for _,d in pairs(smartWalkDirs) do
302+ if (smartWalkDir == North and d == West) or (smartWalkDir == West and d == North) then
303+ smartWalkDir = NorthWest
304+ break
305+ elseif (smartWalkDir == North and d == East) or (smartWalkDir == East and d == North) then
306+ smartWalkDir = NorthEast
307+ break
308+ elseif (smartWalkDir == South and d == West) or (smartWalkDir == West and d == South) then
309+ smartWalkDir = SouthWest
310+ break
311+ elseif (smartWalkDir == South and d == East) or (smartWalkDir == East and d == South) then
312+ smartWalkDir = SouthEast
313+ break
314+ end
315+ end
316+ end
317+end
318+
319+function smartWalk(dir, ticks)
320+ walkEvent = scheduleEvent(function()
321+ if g_keyboard.getModifiers() == KeyboardNoModifier then
322+ local direction = smartWalkDir or dir
323+ walk(direction, ticks)
324+ return true
325+ end
326+ return false
327+ end, 20)
328+end
329+
330+function canChangeFloorDown(pos)
331+ pos.z = pos.z + 1
332+ toTile = g_map.getTile(pos)
333+ return toTile and toTile:hasElevation(3)
334+end
335+
336+function canChangeFloorUp(pos)
337+ pos.z = pos.z - 1
338+ toTile = g_map.getTile(pos)
339+ return toTile and toTile:isWalkable()
340+end
341+
342+function onPositionChange(player, newPos, oldPos)
343+end
344+
345+function onWalk(player, newPos, oldPos)
346+ if autoFinishNextServerWalk + 200 > g_clock.millis() then
347+ player:finishServerWalking()
348+ end
349+end
350+
351+function onTeleport(player, newPos, oldPos)
352+ if not newPos or not oldPos then
353+ return
354+ end
355+ -- floor change is also teleport
356+ if math.abs(newPos.x - oldPos.x) >= 3 or math.abs(newPos.y - oldPos.y) >= 3 or math.abs(newPos.z - oldPos.z) >= 2 then
357+ -- far teleport, lock walk for 100ms
358+ walkLock = g_clock.millis() + g_settings.getNumber('walkTeleportDelay')
359+ else
360+ walkLock = g_clock.millis() + g_settings.getNumber('walkStairsDelay')
361+ end
362+ nextWalkDir = nil -- cancel autowalk
363+end
364+
365+function onWalkFinish(player)
366+ lastFinishedStep = g_clock.millis()
367+ if nextWalkDir ~= nil then
368+ removeEvent(autoWalkEvent)
369+ autoWalkEvent = addEvent(function() if nextWalkDir ~= nil then walk(nextWalkDir, 0) end end, false)
370+ end
371+end
372+
373+function onCancelWalk(player)
374+ pressKeyAction = 3
375+ player:lockWalk(50)
376+end
377+
378+function walk(dir, ticks)
379+ --pressKeyAction = 1
380+ lastManualWalk = g_clock.millis()
381+ local player = g_game.getLocalPlayer()
382+ if not player or g_game.isDead() or player:isDead() then
383+ return
384+ end
385+
386+ if player:isWalkLocked() then
387+ nextWalkDir = nil
388+ return
389+ end
390+
391+ if g_game.isFollowing() then
392+ g_game.cancelFollow()
393+ end
394+
395+ if player:isAutoWalking() then
396+ if lastStop + 100 < g_clock.millis() then
397+ lastStop = g_clock.millis()
398+ player:stopAutoWalk()
399+ g_game.stop()
400+ end
401+ end
402+
403+ local dash = false
404+ local ignoredCanWalk = false
405+ if not g_game.getFeature(GameNewWalking) then
406+ dash = g_settings.getBoolean("dash", false)
407+ end
408+ local ticksToNextWalk = player:getStepTicksLeft()
409+ if not player:canWalk(dir) then -- canWalk return false when previous walk is not finished or not confirmed by server
410+ if dash then
411+ ignoredCanWalk = true
412+ else
413+ if ticksToNextWalk < 500 and (lastWalkDir ~= dir or ticks == 0) then
414+ nextWalkDir = dir
415+ end
416+ if ticksToNextWalk < 30 and lastFinishedStep + 400 > g_clock.millis() and nextWalkDir == nil then -- clicked walk 20 ms too early, try to execute again as soon possible to keep smooth walking
417+ nextWalkDir = dir
418+ end
419+ return
420+ end
421+ end
422+
423+ --if nextWalkDir ~= nil and lastFinishedStep + 200 < g_clock.millis() then
424+ -- print("Cancel " .. nextWalkDir)
425+ -- nextWalkDir = nil
426+ --end
427+ if nextWalkDir ~= nil and nextWalkDir ~= lastWalkDir then
428+ dir = nextWalkDir
429+ end
430+
431+ local toPos = player:getPrewalkingPosition(true)
432+ if dir == North then
433+ toPos.y = toPos.y - 1
434+ elseif dir == East then
435+ toPos.x = toPos.x + 1
436+ elseif dir == South then
437+ toPos.y = toPos.y + 1
438+ elseif dir == West then
439+ toPos.x = toPos.x - 1
440+ elseif dir == NorthEast then
441+ toPos.x = toPos.x + 1
442+ toPos.y = toPos.y - 1
443+ elseif dir == SouthEast then
444+ toPos.x = toPos.x + 1
445+ toPos.y = toPos.y + 1
446+ elseif dir == SouthWest then
447+ toPos.x = toPos.x - 1
448+ toPos.y = toPos.y + 1
449+ elseif dir == NorthWest then
450+ toPos.x = toPos.x - 1
451+ toPos.y = toPos.y - 1
452+ end
453+ local toTile = g_map.getTile(toPos)
454+
455+ if walkLock >= g_clock.millis() and lastWalkDir == dir then
456+ nextWalkDir = nil
457+ return
458+ end
459+
460+ if firstStep and lastWalkDir == dir and lastWalk + g_settings.getNumber('walkFirstStepDelay') > g_clock.millis() then
461+ firstStep = false
462+ walkLock = lastWalk + g_settings.getNumber('walkFirstStepDelay')
463+ return
464+ end
465+
466+ if dash and lastWalkDir == dir and lastWalk + 50 > g_clock.millis() then
467+ return
468+ end
469+
470+ firstStep = (not player:isWalking() and lastFinishedStep + 100 < g_clock.millis() and walkLock + 100 < g_clock.millis())
471+ if player:isServerWalking() and not dash then
472+ walkLock = walkLock + math.max(g_settings.getNumber('walkFirstStepDelay'), 100)
473+ end
474+
475+ nextWalkDir = nil
476+ removeEvent(autoWalkEvent)
477+ autoWalkEvent = nil
478+ local preWalked = false
479+ if toTile and toTile:isWalkable() then
480+ if not player:isServerWalking() and not ignoredCanWalk then
481+ player:preWalk(dir)
482+ preWalked = true
483+ end
484+ else
485+ local playerTile = player:getTile()
486+ if (playerTile and playerTile:hasElevation(3) and canChangeFloorUp(toPos)) or canChangeFloorDown(toPos) or (toTile and toTile:isEmpty() and not toTile:isBlocking()) then
487+ player:lockWalk(100)
488+ elseif player:isServerWalking() then
489+ g_game.stop()
490+ return
491+ elseif not toTile then
492+ player:lockWalk(100) -- bug fix for missing stairs down on map
493+ else
494+ if g_app.isMobile() and dir <= Directions.West then
495+ turn(dir, ticks > 0)
496+ end
497+ return -- not walkable tile
498+ end
499+ end
500+
501+ if player:isServerWalking() and not dash then
502+ g_game.stop()
503+ player:finishServerWalking()
504+ autoFinishNextServerWalk = g_clock.millis() + 200
505+ end
506+ g_game.walk(dir, preWalked)
507+
508+ if not firstStep and lastWalkDir ~= dir then
509+ walkLock = g_clock.millis() + g_settings.getNumber('walkTurnDelay')
510+ end
511+
512+ lastWalkDir = dir
513+ lastWalk = g_clock.millis()
514+ return true
515+end
516+
517+function turn(dir, repeated)
518+ local player = g_game.getLocalPlayer()
519+ if player:isWalking() and player:getWalkDirection() == dir and not player:isServerWalking() then
520+ return
521+ end
522+
523+ removeEvent(walkEvent)
524+
525+ if not repeated or (lastTurn + 100 < g_clock.millis()) then
526+ g_game.turn(dir)
527+ changeWalkDir(dir)
528+ lastTurn = g_clock.millis()
529+ if not repeated then
530+ lastTurn = g_clock.millis() + 50
531+ end
532+ lastTurnDirection = dir
533+ nextWalkDir = nil
534+ player:lockWalk(g_settings.getNumber('walkCtrlTurnDelay'))
535+ end
536+end
537+
538+function checkTurn()
539+ for keys, direction in pairs(turnKeys) do
540+ if g_keyboard.areKeysPressed(keys) then
541+ turn(direction, false)
542+ end
543+ end
544+end
A RD_Client/modules/game_walking/walking.otmod +9-0 View file
@@ -0,0 +1,9 @@
1+Module
2+ name: game_walking
3+ description: Control walking and turns
4+ author: otclient.ovh
5+ website: http://otclient.ovh
6+ scripts: [ walking ]
7+ dependencies: [ game_interface ]
8+ @onLoad: init()
9+ @onUnload: terminate()
A RD_Client/modules/game_wings/game_wings.lua +165-0 View file
@@ -0,0 +1,165 @@
1+local typesWingedOffSet = {
2+ [0] = { -- Is for clone
3+ [North] = {x = 0, y = 0},
4+ [East] = {x = 0, y = 0},
5+ [South] = {x = 0, y = 0},
6+ [West] = {x = 0, y = 0},
7+ },
8+ [1] = { --Jouster
9+ [North] = {x = 0, y = -2},
10+ [East] = {x = -4, y = 1},
11+ [South] = {x = 1, y = -4},
12+ [West] = {x = -2, y = 0},
13+ },
14+ [2] = { --Moth Cape
15+ [North] = {x = -9, y = -5},
16+ [East] = {x = -5, y = -5},
17+ [South] = {x = -5, y = -6},
18+ [West] = {x = -5, y = -9},
19+ },
20+ [3] = {
21+ [North] = {x = -10, y = 10},
22+ [East] = {x = 0, y = 0},
23+ [South] = {x = 0, y = 0},
24+ [West] = {x = 0, y = 0},
25+ },
26+}
27+
28+local typesWingedMountedOffSet = {
29+ [0] = {
30+ [North] = {x = 0, y = 0},
31+ [East] = {x = 0, y = 0},
32+ [South] = {x = 0, y = 0},
33+ [West] = {x = 0, y = 0},
34+ },
35+ [1] = {
36+ [North] = {x = -11, y = -8},
37+ [East] = {x = -9, y = 0},
38+ [South] = {x = 0, y = -8},
39+ [West] = {x = -6, y = -13},
40+ },
41+ [2] = {
42+ [North] = {x = -11, y = -8},
43+ [East] = {x = -9, y = 0},
44+ [South] = {x = 0, y = -9},
45+ [West] = {x = -6, y = -13},
46+ },
47+}
48+
49+local WingsOffsets = {
50+ [1592] = {
51+ [1331] = typesWingedOffSet[1],
52+ [1338] = typesWingedOffSet[2],
53+ },
54+ [1593] = {
55+ [1331] = typesWingedOffSet[1],
56+ [1338] = typesWingedOffSet[2],
57+ },
58+ [1594] = {
59+ [1331] = typesWingedOffSet[1],
60+ [1338] = typesWingedOffSet[2],
61+ },
62+}
63+
64+local WingsMountedOffsets = {
65+ [1592] = {
66+ [1331] = typesWingedMountedOffSet[1],
67+ [1338] = typesWingedMountedOffSet[2],
68+ },
69+ [1593] = {
70+ [1331] = typesWingedMountedOffSet[1],
71+ [1338] = typesWingedMountedOffSet[1],
72+ },
73+ [1594] = {
74+ [1331] = typesWingedMountedOffSet[1],
75+ [1338] = typesWingedMountedOffSet[1],
76+ },
77+}
78+
79+local function translateDir(dir)
80+ if dir == NorthEast or dir == SouthEast then
81+ return East
82+ elseif dir == NorthWest or dir == SouthWest then
83+ return West
84+ end
85+ return dir
86+end
87+
88+local function getWingsInformationOffset(wing, outfit, dir, isMounted)
89+ local offsetType = isMounted and WingsMountedOffsets[wing][outfit] or WingsOffsets[wing][outfit]
90+
91+ if not offsetType then
92+ return {x = 0, y = 0}
93+ end
94+
95+ local translatedDir = translateDir(dir)
96+
97+ if not translatedDir then
98+ return {x = 0, y = 0}
99+ end
100+
101+ local offset = offsetType[translatedDir]
102+
103+ if not offset then
104+ return {x = 0, y = 0}
105+ end
106+
107+ -- Manage auras
108+ if type(offset) == "table" and #offset > 0 then
109+ -- Here you can manipulate auras as needed
110+ -- For example, you can return a specific offset for auras
111+ return {x = offset[1], y = offset[2]}
112+ end
113+
114+ return offset
115+end
116+
117+local function setWingsOffset(creature)
118+ local outfit = creature:getOutfit()
119+ if outfit.wings and outfit.wings > 1 then
120+ local wings = creature:getWings()
121+ local direction = creature:getDirection()
122+
123+ if wings and direction then
124+ local Offset = getWingsInformationOffset(wings, outfit.type, direction, outfit.mount and outfit.mount > 0)
125+ local scale = 0.5 -- factor de escala para el widget en la lista
126+ creature:setWingsOffset(Offset.x * scale, Offset.y * scale)
127+ else
128+ creature:setWingsOffset(0, 0)
129+ end
130+ else
131+ creature:setWingsOffset(0, 0)
132+ end
133+end
134+
135+local function onDirectionChange(creature, oldDirection, newDirection)
136+ setWingsOffset(creature)
137+end
138+
139+local function onPositionChange(player, newPos, oldPos)
140+ setWingsOffset(player)
141+end
142+
143+function init()
144+ connect(LocalPlayer, {onOutfitChange = setWingsOffset, onWalk = setWingsOffset, onDirectionChange = onDirectionChange, onPositionChange = onPositionChange})
145+ connect(Creature, {
146+ onAppear = setWingsOffset,
147+ onWalk = setWingsOffset,
148+ onOutfitChange = setWingsOffset,
149+ onDirectionChange = onDirectionChange,
150+ onPositionChange = onPositionChange
151+ })
152+end
153+
154+function terminate()
155+ disconnect(LocalPlayer, {onOutfitChange = setWingsOffset, onWalk = setWingsOffset, onDirectionChange = onDirectionChange, onPositionChange = onPositionChange})
156+ disconnect(Creature, {
157+ onAppear = setWingsOffset,
158+ onWalk = setWingsOffset,
159+ onOutfitChange = setWingsOffset,
160+ onDirectionChange = onDirectionChange,
161+ onPositionChange = onPositionChange
162+ })
163+end
164+
165+game_wings = { setWingsOffset = setWingsOffset }
A RD_Client/modules/game_wings/game_wings.otmod +10-0 View file
@@ -0,0 +1,10 @@
1+Module
2+ name: game_wings
3+ description: changes the wings offset depending on the direction the player is going.
4+ autoload: true
5+ reloadable: true
6+ sandboxed: true
7+ version: 1.0
8+ scripts: [ game_wings ]
9+ @onLoad: init()
10+ @onUnload: terminate()
A RD_Client/modules/game_wings/game_wings/game_wings.lua +162-0 View file
@@ -0,0 +1,162 @@
1+local typesWingedOffSet = {
2+ [0] = { -- Is for clone
3+ [North] = {x = 0, y = 0},
4+ [East] = {x = 0, y = 0},
5+ [South] = {x = 0, y = 0},
6+ [West] = {x = 0, y = 0},
7+ },
8+ [1] = { --Jouster
9+ [North] = {x = 0, y = -2},
10+ [East] = {x = -4, y = 1},
11+ [South] = {x = 1, y = -4},
12+ [West] = {x = -2, y = 0},
13+ },
14+ [2] = { --Moth Cape
15+ [North] = {x = -9, y = -5},
16+ [East] = {x = -5, y = -5},
17+ [South] = {x = -5, y = -6},
18+ [West] = {x = -5, y = -9},
19+ },
20+ [3] = {
21+ [North] = {x = -10, y = 10},
22+ [East] = {x = 0, y = 0},
23+ [South] = {x = 0, y = 0},
24+ [West] = {x = 0, y = 0},
25+ },
26+}
27+
28+local typesWingedMountedOffSet = {
29+ [0] = {
30+ [North] = {x = 0, y = 0},
31+ [East] = {x = 0, y = 0},
32+ [South] = {x = 0, y = 0},
33+ [West] = {x = 0, y = 0},
34+ },
35+ [1] = {
36+ [North] = {x = -11, y = -8},
37+ [East] = {x = -9, y = 0},
38+ [South] = {x = 0, y = -8},
39+ [West] = {x = -6, y = -13},
40+ },
41+ [2] = {
42+ [North] = {x = -11, y = -8},
43+ [East] = {x = -9, y = 0},
44+ [South] = {x = 0, y = -9},
45+ [West] = {x = -6, y = -13},
46+ },
47+}
48+
49+local WingsOffsets = {
50+ [1592] = {
51+ [1331] = typesWingedOffSet[1],
52+ [1338] = typesWingedOffSet[2],
53+ },
54+ [1593] = {
55+ [1331] = typesWingedOffSet[1],
56+ [1338] = typesWingedOffSet[2],
57+ },
58+ [1594] = {
59+ [1331] = typesWingedOffSet[1],
60+ [1338] = typesWingedOffSet[2],
61+ },
62+}
63+
64+local WingsMountedOffsets = {
65+ [1592] = {
66+ [1331] = typesWingedMountedOffSet[1],
67+ [1338] = typesWingedMountedOffSet[2],
68+ },
69+ [1593] = {
70+ [1331] = typesWingedMountedOffSet[1],
71+ [1338] = typesWingedMountedOffSet[1],
72+ },
73+ [1594] = {
74+ [1331] = typesWingedMountedOffSet[1],
75+ [1338] = typesWingedMountedOffSet[1],
76+ },
77+}
78+
79+local function translateDir(dir)
80+ if dir == NorthEast or dir == SouthEast then
81+ return East
82+ elseif dir == NorthWest or dir == SouthWest then
83+ return West
84+ end
85+ return dir
86+end
87+
88+local function getWingsInformationOffset(wing, outfit, dir, isMounted)
89+ local offsetType = isMounted and WingsMountedOffsets[wing][outfit] or WingsOffsets[wing][outfit]
90+
91+ if not offsetType then
92+ return {x = 0, y = 0}
93+ end
94+
95+ local translatedDir = translateDir(dir)
96+
97+ if not translatedDir then
98+ return {x = 0, y = 0}
99+ end
100+
101+ local offset = offsetType[translatedDir]
102+
103+ if not offset then
104+ return {x = 0, y = 0}
105+ end
106+
107+ -- Manage auras
108+ if type(offset) == "table" and #offset > 0 then
109+ -- Here you can manipulate auras as needed
110+ -- For example, you can return a specific offset for auras
111+ return {x = offset[1], y = offset[2]}
112+ end
113+
114+ return offset
115+end
116+
117+local function setWingsOffset(creature)
118+ local outfit = creature:getOutfit()
119+ if outfit.wings and outfit.wings > 1 then
120+ local wings = creature:getWings()
121+ local direction = creature:getDirection()
122+
123+ if wings and direction then
124+ local Offset = getWingsInformationOffset(wings, outfit.type, direction, outfit.mount and outfit.mount > 0)
125+ creature:setWingsOffset(Offset.x, Offset.y)
126+ else
127+ creature:setWingsOffset(0, 0)
128+ end
129+ else
130+ creature:setWingsOffset(0, 0)
131+ end
132+end
133+
134+local function onDirectionChange(creature, oldDirection, newDirection)
135+ setWingsOffset(creature)
136+end
137+
138+local function onPositionChange(player, newPos, oldPos)
139+ setWingsOffset(player)
140+end
141+
142+function init()
143+ connect(LocalPlayer, {onOutfitChange = setWingsOffset, onWalk = setWingsOffset, onDirectionChange = onDirectionChange, onPositionChange = onPositionChange})
144+ connect(Creature, {
145+ onAppear = setWingsOffset,
146+ onWalk = setWingsOffset,
147+ onOutfitChange = setWingsOffset,
148+ onDirectionChange = onDirectionChange,
149+ onPositionChange = onPositionChange
150+ })
151+end
152+
153+function terminate()
154+ disconnect(LocalPlayer, {onOutfitChange = setWingsOffset, onWalk = setWingsOffset, onDirectionChange = onDirectionChange, onPositionChange = onPositionChange})
155+ disconnect(Creature, {
156+ onAppear = setWingsOffset,
157+ onWalk = setWingsOffset,
158+ onOutfitChange = setWingsOffset,
159+ onDirectionChange = onDirectionChange,
160+ onPositionChange = onPositionChange
161+ })
162+end
A RD_Client/modules/game_wings/game_wings/game_wings.otmod +10-0 View file
@@ -0,0 +1,10 @@
1+Module
2+ name: game_wings
3+ description: changes the wings offset depending on the direction the player is going.
4+ autoload: true
5+ reloadable: true
6+ sandboxed: true
7+ version: 1.0
8+ scripts: [ game_wings ]
9+ @onLoad: init()
10+ @onUnload: terminate()
A RD_Client/modules/game_wings/game_wings/Offset Editor.html +140-0 View file
@@ -0,0 +1,140 @@
1+<!DOCTYPE html>
2+<html lang="es">
3+<head>
4+ <meta charset="UTF-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6+ <title>Offsets Editor</title>
7+ <link rel="stylesheet" href="styles.css">
8+</head>
9+<body>
10+ <h1>Offsets Editor</h1>
11+
12+ <div class="editor-container">
13+ <div class="tab">
14+ <button class="tablinks" onclick="openTab(event, 'typesWingedOffSet')">Wings OffSet List</button>
15+ <button class="tablinks" onclick="openTab(event, 'typesWingedMountedOffSet')">Wings Mounted OffSet List</button>
16+ <button class="tablinks" onclick="openTab(event, 'WingsOffsets')">Combiner Unmount</button>
17+ <button class="tablinks" onclick="openTab(event, 'WingsMountedOffsets')">Combiner Mounted</button>
18+ </div>
19+
20+ <div id="typesWingedOffSet" class="tabcontent">
21+ <h2>Wings OffSet</h2>
22+ <div id="typesWingedOffSetEntries"></div>
23+ <div class="entry-fields">
24+ <label for="offsetIndex">Wings OffSet List:</label>
25+ <input type="number" id="offsetIndex" min="0" value="0">
26+ <label for="comment1">OffSet Name:</label>
27+ <input type="text" id="comment1" placeholder="Name">
28+ </div>
29+ <div class="offset-fields">
30+ <div class="offset-group">
31+ <label for="northX">North X:</label>
32+ <input type="number" id="northX" value="0">
33+ <label for="northY">North Y:</label>
34+ <input type="number" id="northY" value="0">
35+ </div>
36+ <div class="offset-group">
37+ <label for="eastX">East X:</label>
38+ <input type="number" id="eastX" value="0">
39+ <label for="eastY">East Y:</label>
40+ <input type="number" id="eastY" value="0">
41+ </div>
42+ <div class="offset-group">
43+ <label for="southX">South X:</label>
44+ <input type="number" id="southX" value="0">
45+ <label for="southY">South Y:</label>
46+ <input type="number" id="southY" value="0">
47+ </div>
48+ <div class="offset-group">
49+ <label for="westX">West X:</label>
50+ <input type="number" id="westX" value="0">
51+ <label for="westY">West Y:</label>
52+ <input type="number" id="westY" value="0">
53+ </div>
54+ </div>
55+ <button onclick="addEntry('typesWingedOffSet')">Agregar Entrada</button>
56+ </div>
57+
58+ <div id="typesWingedMountedOffSet" class="tabcontent" style="display:none">
59+ <h2>Wings Mounted OffSet</h2>
60+ <div id="typesWingedMountedOffSetEntries"></div>
61+ <div class="entry-fields">
62+ <label for="mountedOffsetIndex">Wings Mounted Offsets List:</label>
63+ <input type="number" id="mountedOffsetIndex" min="0" value="0">
64+ <label for="comment2">OffSet Name:</label>
65+ <input type="text" id="comment2" placeholder="Name">
66+ </div>
67+ <div class="offset-fields">
68+ <div class="offset-group">
69+ <label for="mountedNorthX">North X:</label>
70+ <input type="number" id="mountedNorthX" value="0">
71+ <label for="mountedNorthY">North Y:</label>
72+ <input type="number" id="mountedNorthY" value="0">
73+ </div>
74+ <div class="offset-group">
75+ <label for="mountedEastX">East X:</label>
76+ <input type="number" id="mountedEastX" value="0">
77+ <label for="mountedEastY">East Y:</label>
78+ <input type="number" id="mountedEastY" value="0">
79+ </div>
80+ <div class="offset-group">
81+ <label for="mountedSouthX">South X:</label>
82+ <input type="number" id="mountedSouthX" value="0">
83+ <label for="mountedSouthY">South Y:</label>
84+ <input type="number" id="mountedSouthY" value="0">
85+ </div>
86+ <div class="offset-group">
87+ <label for="mountedWestX">West X:</label>
88+ <input type="number" id="mountedWestX" value="0">
89+ <label for="mountedWestY">West Y:</label>
90+ <input type="number" id="mountedWestY" value="0">
91+ </div>
92+ </div>
93+ <button onclick="addEntry('typesWingedMountedOffSet')">Agregar Entrada</button>
94+ </div>
95+
96+ <div id="WingsOffsets" class="tabcontent" style="display:none">
97+ <h2>Wings + Outfits or Aura Combination List</h2>
98+ <div id="WingsOffsetsEntries"></div>
99+ <div class="entry-fields">
100+ <label for="offsetIndexWings">Wings Offsets List:</label>
101+ <input type="number" id="offsetIndexWings" min="0" value="0">
102+ <label for="comment3">Combination Name:</label>
103+ <input type="text" id="comment3" placeholder="Name">
104+ </div>
105+ <div class="entry-fields">
106+ <label for="wingID">WingID:</label>
107+ <input type="number" id="wingID" min="0" value="1592">
108+ <label for="outfitID">OutfitID:</label>
109+ <input type="number" id="outfitID" min="0" value="1331">
110+ </div>
111+ <button onclick="addEntry('WingsOffsets')">Agregar Entrada</button>
112+ </div>
113+
114+ <div id="WingsMountedOffsets" class="tabcontent" style="display:none">
115+ <h2>Wings + Outfits or Aura Mounted Combination</h2>
116+ <div id="WingsMountedOffsetsEntries"></div>
117+ <div class="entry-fields">
118+ <label for="offsetIndexMounted">Wings Mounted Offsets List:</label>
119+ <input type="number" id="offsetIndexMounted" min="0" value="0">
120+ <label for="comment4">Combination Name:</label>
121+ <input type="text" id="comment4" placeholder="Name">
122+ </div>
123+ <div class="entry-fields">
124+ <label for="mountedWingID">WingID:</label>
125+ <input type="number" id="mountedWingID" min="0" value="1592">
126+ <label for="mountedOutfitID">OutfitID:</label>
127+ <input type="number" id="mountedOutfitID" min="0" value="1331">
128+ </div>
129+ <button onclick="addEntry('WingsMountedOffsets')">Agregar Entrada</button>
130+ </div>
131+
132+ <div class="editor-section">
133+ <h2>Guardar Cambios</h2>
134+ <button id="saveButton">Guardar</button>
135+ </div>
136+ </div>
137+
138+ <script src="script.js"></script>
139+</body>
140+</html>
A RD_Client/modules/game_wings/game_wings/script.js +216-0 View file
@@ -0,0 +1,216 @@
1+const saveButton = document.getElementById('saveButton');
2+
3+function openTab(evt, tabName) {
4+ const tabContents = document.getElementsByClassName("tabcontent");
5+ for (let i = 0; i < tabContents.length; i++) {
6+ tabContents[i].style.display = "none";
7+ }
8+ document.getElementById(tabName).style.display = "block";
9+
10+ const tablinks = document.getElementsByClassName("tablinks");
11+ for (let i = 0; i < tablinks.length; i++) {
12+ tablinks[i].className = tablinks[i].className.replace(" active", "");
13+ }
14+ evt.currentTarget.className += " active";
15+}
16+
17+function addEntry(type) {
18+ let entryContainer;
19+ let newEntry;
20+
21+ switch (type) {
22+ case 'typesWingedOffSet':
23+ entryContainer = document.getElementById('typesWingedOffSetEntries');
24+ const offsetIndex = document.getElementById('offsetIndex');
25+ const comment1 = document.getElementById('comment1');
26+ const northX = document.getElementById('northX');
27+ const northY = document.getElementById('northY');
28+ const eastX = document.getElementById('eastX');
29+ const eastY = document.getElementById('eastY');
30+ const southX = document.getElementById('southX');
31+ const southY = document.getElementById('southY');
32+ const westX = document.getElementById('westX');
33+ const westY = document.getElementById('westY');
34+
35+ if (!entryContainer || !offsetIndex || !comment1 || !northX || !northY || !eastX || !eastY || !southX || !southY || !westX || !westY) {
36+ console.error('Uno o más elementos están faltando para typesWingedOffSet');
37+ return;
38+ }
39+
40+ const offsetIndexValue = parseInt(offsetIndex.value);
41+ const northXValue = parseInt(northX.value);
42+ const northYValue = parseInt(northY.value);
43+ const eastXValue = parseInt(eastX.value);
44+ const eastYValue = parseInt(eastY.value);
45+ const southXValue = parseInt(southX.value);
46+ const southYValue = parseInt(southY.value);
47+ const westXValue = parseInt(westX.value);
48+ const westYValue = parseInt(westY.value);
49+
50+ const existingEntry1 = Array.from(entryContainer.getElementsByClassName('entry')).find(entry => {
51+ return entry.innerText.includes(`[${offsetIndexValue}]`);
52+ });
53+
54+ if (existingEntry1) {
55+ existingEntry1.innerHTML = `
56+ [${offsetIndexValue}] = { -- ${comment1.value}
57+ [North] = {x = ${northXValue}, y = ${northYValue}},
58+ [East] = {x = ${eastXValue}, y = ${eastYValue}},
59+ [South] = {x = ${southXValue}, y = ${southYValue}},
60+ [West] = {x = ${westXValue}, y = ${westYValue}},
61+ },`;
62+ } else {
63+ newEntry = `
64+<div class="entry">
65+ [${offsetIndexValue}] = { -- ${comment1.value}
66+ [North] = {x = ${northXValue}, y = ${northYValue}},
67+ [East] = {x = ${eastXValue}, y = ${eastYValue}},
68+ [South] = {x = ${southXValue}, y = ${southYValue}},
69+ [West] = {x = ${westXValue}, y = ${westYValue}},
70+ },
71+</div>`;
72+ entryContainer.innerHTML += newEntry;
73+ }
74+ break;
75+
76+ case 'typesWingedMountedOffSet':
77+ entryContainer = document.getElementById('typesWingedMountedOffSetEntries');
78+ const mountedOffsetIndex = parseInt(document.getElementById('mountedOffsetIndex').value);
79+ const comment2 = document.getElementById('comment2').value;
80+ const mountedNorthX = parseInt(document.getElementById('mountedNorthX').value);
81+ const mountedNorthY = parseInt(document.getElementById('mountedNorthY').value);
82+ const mountedEastX = parseInt(document.getElementById('mountedEastX').value);
83+ const mountedEastY = parseInt(document.getElementById('mountedEastY').value);
84+ const mountedSouthX = parseInt(document.getElementById('mountedSouthX').value);
85+ const mountedSouthY = parseInt(document.getElementById('mountedSouthY').value);
86+ const mountedWestX = parseInt(document.getElementById('mountedWestX').value);
87+ const mountedWestY = parseInt(document.getElementById('mountedWestY').value);
88+
89+ if (!entryContainer || isNaN(mountedOffsetIndex) || isNaN(mountedNorthX) || isNaN(mountedNorthY) || isNaN(mountedEastX) || isNaN(mountedEastY) || isNaN(mountedSouthX) || isNaN(mountedSouthY) || isNaN(mountedWestX) || isNaN(mountedWestY) || comment2 === undefined) {
90+ console.error('Uno o más elementos están faltando para typesWingedMountedOffSet');
91+ return;
92+ }
93+
94+ const existingEntry2 = Array.from(entryContainer.getElementsByClassName('entry')).find(entry => {
95+ return entry.innerText.includes(`[${mountedOffsetIndex}]`);
96+ });
97+
98+ if (existingEntry2) {
99+ existingEntry2.innerHTML = `
100+ [${mountedOffsetIndex}] = { -- ${comment2}
101+ [North] = {x = ${mountedNorthX}, y = ${mountedNorthY}},
102+ [East] = {x = ${mountedEastX}, y = ${mountedEastY}},
103+ [South] = {x = ${mountedSouthX}, y = ${mountedSouthY}},
104+ [West] = {x = ${mountedWestX}, y = ${mountedWestY}},
105+ },`;
106+ } else {
107+ newEntry = `
108+<div class="entry">
109+ [${mountedOffsetIndex}] = { -- ${comment2}
110+ [North] = {x = ${mountedNorthX}, y = ${mountedNorthY}},
111+ [East] = {x = ${mountedEastX}, y = ${mountedEastY}},
112+ [South] = {x = ${mountedSouthX}, y = ${mountedSouthY}},
113+ [West] = {x = ${mountedWestX}, y = ${mountedWestY}},
114+ },
115+</div>`;
116+ entryContainer.innerHTML += newEntry;
117+ }
118+ break;
119+
120+ case 'WingsOffsets':
121+ entryContainer = document.getElementById('WingsOffsetsEntries');
122+ const wingID = parseInt(document.getElementById('wingID').value);
123+ const outfitID = parseInt(document.getElementById('outfitID').value);
124+ const comment3 = document.getElementById('comment3').value;
125+
126+ if (!entryContainer || isNaN(wingID) || isNaN(outfitID) || comment3 === undefined) {
127+ console.error('Uno o más elementos están faltando para WingsOffsets');
128+ return;
129+ }
130+
131+ const existingWingsEntry = Array.from(entryContainer.getElementsByClassName('entry')).find(entry => {
132+ return entry.innerText.includes(`[${wingID}]`);
133+ });
134+
135+ if (existingWingsEntry) {
136+ existingWingsEntry.innerHTML = `
137+ [${wingID}] = { -- ${comment3}
138+ [${outfitID}] = typesWingedOffSet[1],
139+ },`;
140+ } else {
141+ newEntry = `
142+<div class="entry">
143+ [${wingID}] = { -- ${comment3}
144+ [${outfitID}] = typesWingedOffSet[],
145+ },
146+</div>`;
147+ entryContainer.innerHTML += newEntry;
148+ }
149+ break;
150+
151+ case 'WingsMountedOffsets':
152+ entryContainer = document.getElementById('WingsMountedOffsetsEntries');
153+ const mountedWingID = parseInt(document.getElementById('mountedWingID').value);
154+ const mountedOutfitID = parseInt(document.getElementById('mountedOutfitID').value);
155+ const comment4 = document.getElementById('comment4').value;
156+
157+ if (!entryContainer || isNaN(mountedWingID) || isNaN(mountedOutfitID) || comment4 === undefined) {
158+ console.error('Uno o más elementos están faltando para WingsMountedOffsets');
159+ return;
160+ }
161+
162+ const existingMountedWingsEntry = Array.from(entryContainer.getElementsByClassName('entry')).find(entry => {
163+ return entry.innerText.includes(`[${mountedWingID}]`);
164+ });
165+
166+ if (existingMountedWingsEntry) {
167+ existingMountedWingsEntry.innerHTML = `
168+ [${mountedWingID}] = { -- ${comment4}
169+ [${mountedOutfitID}] = typesWingedMountedOffSet[1],
170+ },`;
171+ } else {
172+ newEntry = `
173+<div class="entry">
174+ [${mountedWingID}] = { -- ${comment4}
175+ [${mountedOutfitID}] = typesWingedMountedOffSet[1],
176+ },
177+</div>`;
178+ entryContainer.innerHTML += newEntry;
179+ }
180+ break;
181+ }
182+}
183+
184+
185+// Rueda del mouse para cambiar valores
186+document.querySelectorAll('input[type="number"]').forEach(input => {
187+ input.addEventListener('wheel', (event) => {
188+ event.preventDefault();
189+ const delta = event.deltaY < 0 ? 1 : -1;
190+ input.value = parseInt(input.value) + delta;
191+ });
192+});
193+
194+saveButton.addEventListener('click', saveStructure);
195+
196+function saveStructure() {
197+ const typesWingedOffSetContent = document.getElementById('typesWingedOffSetEntries').innerText.trim();
198+ const typesWingedMountedOffSetContent = document.getElementById('typesWingedMountedOffSetEntries').innerText.trim();
199+ const wingsOffsetsContent = document.getElementById('WingsOffsetsEntries').innerText.trim();
200+ const wingsMountedOffsetsContent = document.getElementById('WingsMountedOffsetsEntries').innerText.trim();
201+
202+ const content = `typesWingedOffSet = {\n${typesWingedOffSetContent}\n}\n\n` +
203+ `typesWingedMountedOffSet = {\n${typesWingedMountedOffSetContent}\n}\n\n` +
204+ `WingsOffsets = {\n${wingsOffsetsContent}\n}\n\n` +
205+ `WingsMountedOffsets = {\n${wingsMountedOffsetsContent}\n}`;
206+
207+ const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
208+ const url = window.URL.createObjectURL(blob);
209+
210+ const downloadLink = document.createElement('a');
211+ downloadLink.href = url;
212+ downloadLink.download = 'types_wings_structure.lua';
213+ downloadLink.click();
214+
215+ window.URL.revokeObjectURL(url);
216+}
A RD_Client/modules/game_wings/game_wings/styles.css +131-0 View file
@@ -0,0 +1,131 @@
1+body {
2+ font-family: Arial, sans-serif;
3+ margin: 0;
4+ padding: 50px;
5+ background-color: #f9f9f9;
6+}
7+
8+.editor-container {
9+ display: flex;
10+ flex-direction: column;
11+ max-width: 800px;
12+ margin: 0 auto;
13+ gap: 20px;
14+}
15+
16+.tab {
17+ display: flex;
18+ justify-content: center;
19+}
20+
21+.tab button {
22+ background-color: #007BFF;
23+ color: white;
24+ border: none;
25+ padding: 10px 20px;
26+ cursor: pointer;
27+ transition: background-color 0.3s;
28+ border-radius: 5px;
29+}
30+
31+.tab button:hover {
32+ background-color: #0056b3;
33+}
34+
35+.tabcontent {
36+ padding: 10px;
37+ background-color: black;
38+ border-radius: 5px;
39+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
40+ color: white;
41+}
42+
43+h1, h2 {
44+ text-align: center;
45+}
46+
47+.entry-fields {
48+ display: flex;
49+ justify-content: center;
50+ align-items: center;
51+ margin-bottom: 10px;
52+ gap: 10px;
53+}
54+
55+.entry-fields input {
56+ width: 80px;
57+ padding: 5px;
58+ border: 1px solid #ccc;
59+ border-radius: 3px;
60+}
61+
62+.offset-fields {
63+ display: flex;
64+ justify-content: center;
65+ gap: 20px;
66+ flex-wrap: wrap;
67+ margin-top: 10px;
68+}
69+
70+.offset-group {
71+ display: flex;
72+ flex-direction: column;
73+ align-items: center;
74+}
75+
76+.offset-group label {
77+ margin-bottom: 5px;
78+}
79+
80+.offset-group input {
81+ width: 60px;
82+}
83+
84+.entry {
85+ margin-bottom: 10px;
86+ white-space: pre-wrap; /* Mantener el formato de los espacios */
87+}
88+
89+.editor-section {
90+ text-align: center;
91+}
92+
93+button {
94+ padding: 10px 15px;
95+ background-color: #28a745;
96+ color: white;
97+ border: none;
98+ border-radius: 5px;
99+ cursor: pointer;
100+ transition: background-color 0.3s;
101+}
102+
103+button:hover {
104+ background-color: #218838;
105+}
106+
107+<script>
108+function openTab(evt, tabName) {
109+ var i, tabcontent, tablinks;
110+ tabcontent = document.getElementsByClassName("tabcontent");
111+ for (i = 0; i < tabcontent.length; i++) {
112+ tabcontent[i].style.display = "none";
113+ }
114+ tablinks = document.getElementsByClassName("tablinks");
115+ for (i = 0; i < tablinks.length; i++) {
116+ tablinks[i].className = tablinks[i].className.replace(" active", "");
117+ }
118+ document.getElementById(tabName).style.display = "block";
119+ evt.currentTarget.className += " active";
120+}
121+
122+// Abre la primera pestaña por defecto
123+document.addEventListener('DOMContentLoaded', function() {
124+ var firstTab = document.getElementsByClassName("tablinks")[0];
125+ if (firstTab) {
126+ firstTab.click();
127+ }
128+});
129+</script>
130+
131+```
A RD_Client/modules/game_wings/Offset Editor.html +140-0 View file
@@ -0,0 +1,140 @@
1+<!DOCTYPE html>
2+<html lang="es">
3+<head>
4+ <meta charset="UTF-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6+ <title>Offsets Editor</title>
7+ <link rel="stylesheet" href="styles.css">
8+</head>
9+<body>
10+ <h1>Offsets Editor</h1>
11+
12+ <div class="editor-container">
13+ <div class="tab">
14+ <button class="tablinks" onclick="openTab(event, 'typesWingedOffSet')">Wings OffSet List</button>
15+ <button class="tablinks" onclick="openTab(event, 'typesWingedMountedOffSet')">Wings Mounted OffSet List</button>
16+ <button class="tablinks" onclick="openTab(event, 'WingsOffsets')">Combiner Unmount</button>
17+ <button class="tablinks" onclick="openTab(event, 'WingsMountedOffsets')">Combiner Mounted</button>
18+ </div>
19+
20+ <div id="typesWingedOffSet" class="tabcontent">
21+ <h2>Wings OffSet</h2>
22+ <div id="typesWingedOffSetEntries"></div>
23+ <div class="entry-fields">
24+ <label for="offsetIndex">Wings OffSet List:</label>
25+ <input type="number" id="offsetIndex" min="0" value="0">
26+ <label for="comment1">OffSet Name:</label>
27+ <input type="text" id="comment1" placeholder="Name">
28+ </div>
29+ <div class="offset-fields">
30+ <div class="offset-group">
31+ <label for="northX">North X:</label>
32+ <input type="number" id="northX" value="0">
33+ <label for="northY">North Y:</label>
34+ <input type="number" id="northY" value="0">
35+ </div>
36+ <div class="offset-group">
37+ <label for="eastX">East X:</label>
38+ <input type="number" id="eastX" value="0">
39+ <label for="eastY">East Y:</label>
40+ <input type="number" id="eastY" value="0">
41+ </div>
42+ <div class="offset-group">
43+ <label for="southX">South X:</label>
44+ <input type="number" id="southX" value="0">
45+ <label for="southY">South Y:</label>
46+ <input type="number" id="southY" value="0">
47+ </div>
48+ <div class="offset-group">
49+ <label for="westX">West X:</label>
50+ <input type="number" id="westX" value="0">
51+ <label for="westY">West Y:</label>
52+ <input type="number" id="westY" value="0">
53+ </div>
54+ </div>
55+ <button onclick="addEntry('typesWingedOffSet')">Agregar Entrada</button>
56+ </div>
57+
58+ <div id="typesWingedMountedOffSet" class="tabcontent" style="display:none">
59+ <h2>Wings Mounted OffSet</h2>
60+ <div id="typesWingedMountedOffSetEntries"></div>
61+ <div class="entry-fields">
62+ <label for="mountedOffsetIndex">Wings Mounted Offsets List:</label>
63+ <input type="number" id="mountedOffsetIndex" min="0" value="0">
64+ <label for="comment2">OffSet Name:</label>
65+ <input type="text" id="comment2" placeholder="Name">
66+ </div>
67+ <div class="offset-fields">
68+ <div class="offset-group">
69+ <label for="mountedNorthX">North X:</label>
70+ <input type="number" id="mountedNorthX" value="0">
71+ <label for="mountedNorthY">North Y:</label>
72+ <input type="number" id="mountedNorthY" value="0">
73+ </div>
74+ <div class="offset-group">
75+ <label for="mountedEastX">East X:</label>
76+ <input type="number" id="mountedEastX" value="0">
77+ <label for="mountedEastY">East Y:</label>
78+ <input type="number" id="mountedEastY" value="0">
79+ </div>
80+ <div class="offset-group">
81+ <label for="mountedSouthX">South X:</label>
82+ <input type="number" id="mountedSouthX" value="0">
83+ <label for="mountedSouthY">South Y:</label>
84+ <input type="number" id="mountedSouthY" value="0">
85+ </div>
86+ <div class="offset-group">
87+ <label for="mountedWestX">West X:</label>
88+ <input type="number" id="mountedWestX" value="0">
89+ <label for="mountedWestY">West Y:</label>
90+ <input type="number" id="mountedWestY" value="0">
91+ </div>
92+ </div>
93+ <button onclick="addEntry('typesWingedMountedOffSet')">Agregar Entrada</button>
94+ </div>
95+
96+ <div id="WingsOffsets" class="tabcontent" style="display:none">
97+ <h2>Wings + Outfits or Aura Combination List</h2>
98+ <div id="WingsOffsetsEntries"></div>
99+ <div class="entry-fields">
100+ <label for="offsetIndexWings">Wings Offsets List:</label>
101+ <input type="number" id="offsetIndexWings" min="0" value="0">
102+ <label for="comment3">Combination Name:</label>
103+ <input type="text" id="comment3" placeholder="Name">
104+ </div>
105+ <div class="entry-fields">
106+ <label for="wingID">WingID:</label>
107+ <input type="number" id="wingID" min="0" value="1592">
108+ <label for="outfitID">OutfitID:</label>
109+ <input type="number" id="outfitID" min="0" value="1331">
110+ </div>
111+ <button onclick="addEntry('WingsOffsets')">Agregar Entrada</button>
112+ </div>
113+
114+ <div id="WingsMountedOffsets" class="tabcontent" style="display:none">
115+ <h2>Wings + Outfits or Aura Mounted Combination</h2>
116+ <div id="WingsMountedOffsetsEntries"></div>
117+ <div class="entry-fields">
118+ <label for="offsetIndexMounted">Wings Mounted Offsets List:</label>
119+ <input type="number" id="offsetIndexMounted" min="0" value="0">
120+ <label for="comment4">Combination Name:</label>
121+ <input type="text" id="comment4" placeholder="Name">
122+ </div>
123+ <div class="entry-fields">
124+ <label for="mountedWingID">WingID:</label>
125+ <input type="number" id="mountedWingID" min="0" value="1592">
126+ <label for="mountedOutfitID">OutfitID:</label>
127+ <input type="number" id="mountedOutfitID" min="0" value="1331">
128+ </div>
129+ <button onclick="addEntry('WingsMountedOffsets')">Agregar Entrada</button>
130+ </div>
131+
132+ <div class="editor-section">
133+ <h2>Guardar Cambios</h2>
134+ <button id="saveButton">Guardar</button>
135+ </div>
136+ </div>
137+
138+ <script src="script.js"></script>
139+</body>
140+</html>
Top