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_menu/passiveBox.otui +112-0 View file
@@ -0,0 +1,112 @@
1+MainWindow
2+ id: passiveWindow
3+ size: 310 350
4+ image-source: /images/custom/Shop/item_shop_view
5+ draggable: false
6+ focusable: true
7+ @onEscape: closer_boxShop()
8+
9+ UIButton
10+ id: categoria_1
11+ size: 60 35
12+ anchors.top: parent.top
13+ anchors.left: parent.left
14+ margin-top: 05
15+ margin-left: 10
16+ @onClick: onLoadPassives(01)
17+ opacity: 0.8
18+ $pressed:
19+ opacity: 0.6
20+ $hover !pressed:
21+ opacity: 1.0
22+
23+ UIButton
24+ id: categoria_2
25+ size: 60 35
26+ anchors.top: parent.top
27+ anchors.left: categoria_1.right
28+ margin-left: 05
29+ margin-top: 05
30+ @onClick: onLoadPassives(02)
31+ opacity: 0.8
32+ $pressed:
33+ opacity: 0.6
34+ $hover !pressed:
35+ opacity: 1.0
36+
37+ UIButton
38+ id: categoria_3
39+ size: 60 35
40+ anchors.top: parent.top
41+ anchors.left: categoria_2.right
42+ margin-left: 05
43+ margin-top: 05
44+ @onClick: onLoadPassives(03)
45+ opacity: 0.8
46+ $pressed:
47+ opacity: 0.6
48+ $hover !pressed:
49+ opacity: 1.0
50+
51+ UIButton
52+ id: categoria_4
53+ size: 60 35
54+ anchors.top: parent.top
55+ anchors.left: categoria_3.right
56+ margin-left: 05
57+ margin-top: 05
58+ @onClick: onLoadPassives(04)
59+ opacity: 0.8
60+ $pressed:
61+ opacity: 0.6
62+ $hover !pressed:
63+ opacity: 1.0
64+
65+ VerticalScrollBar
66+ id: PassiveListScrollBar
67+ anchors.top: categoria_1.bottom
68+ anchors.bottom: parent.bottom
69+ anchors.right: parent.right
70+ step: 24
71+ opacity: 0.8
72+ margin-bottom: 50
73+ margin-top: 10
74+ margin-right: 5
75+ pixels-scroll: true
76+
77+ ScrollablePanel
78+ id: passivePanel
79+ size: 250 64
80+ anchors.right: parent.right
81+ anchors.right: prev.left
82+ anchors.top: categoria_1.bottom
83+ anchors.bottom: parent.bottom
84+ vertical-scrollbar: PassiveListScrollBar
85+ margin-top: 10
86+ margin-left: 02
87+ margin-right: 02
88+ margin-bottom: 50
89+ layout:
90+ type: grid
91+ cell-size: 250 100
92+ flow: true
93+ margin-top: 5
94+
95+ UIButton
96+ id: exitButton
97+ !text: tr('CANCELAR')
98+ image-source: /images/custom/UI/button
99+ height: 30
100+ font: verdana-11px-antialised
101+ anchors.bottom: parent.bottom
102+ anchors.right: parent.right
103+ anchors.left: parent.left
104+ margin-left: 10
105+ margin-right: 15
106+ margin-bottom: 05
107+ @onClick: closer_boxShop()
108+ opacity: 0.8
109+ $pressed:
110+ opacity: 0.6
111+ $hover !pressed:
112+ opacity: 1.0
A RD_Client/modules/game_menu/pix.otui +73-0 View file
@@ -0,0 +1,73 @@
1+MainWindow
2+ !text: tr('Editar seu PIX')
3+ size: 280 230
4+
5+ Label
6+ id: nomeReal
7+ !text: tr('Nome real do titular')
8+ text-wrap: true
9+ text-auto-resize: true
10+ anchors.top: parent.top
11+ anchors.left: parent.left
12+ anchors.right: parent.right
13+
14+ TextEdit
15+ id: nomeTextEdit
16+ text-wrap: true
17+ size: 250 24
18+ anchors.top: nomeReal.bottom
19+ margin-top: 4
20+ margin-bottom: 8
21+
22+ Label
23+ id: keyPix
24+ !text: tr('Chave Pix (Somente Email)')
25+ text-wrap: true
26+ text-auto-resize: true
27+ anchors.top: nomeTextEdit.bottom
28+ anchors.left: parent.left
29+ anchors.right: parent.right
30+ margin-top: 5
31+
32+ TextEdit
33+ id: pixTextEdit
34+ text-wrap: true
35+ size: 250 24
36+ anchors.top: keyPix.bottom
37+ margin-top: 4
38+ margin-bottom: 8
39+
40+ Label
41+ id: keyBank
42+ !text: tr('Institui��o Financeira (Banco)')
43+ text-wrap: true
44+ text-auto-resize: true
45+ anchors.top: pixTextEdit.bottom
46+ anchors.left: parent.left
47+ anchors.right: parent.right
48+ margin-top: 5
49+
50+ TextEdit
51+ id: bankTextEdit
52+ text-wrap: true
53+ size: 250 24
54+ anchors.top: keyBank.bottom
55+ margin-top: 4
56+ margin-bottom: 8
57+
58+ Button
59+ id: sendButton
60+ !text: tr('Salvar')
61+ anchors.bottom: cancelButton.bottom
62+ anchors.right: cancelButton.left
63+ margin-right: 10
64+ width: 80
65+ &onClick: onSavePix
66+
67+ Button
68+ id: cancelButton
69+ !text: tr('Cancel')
70+ anchors.bottom: parent.bottom
71+ anchors.right: parent.right
72+ width: 80
73+ @onClick: closer_boxShop()
A RD_Client/modules/game_menu/premiumBox.otui +80-0 View file
@@ -0,0 +1,80 @@
1+MainWindow
2+ id: premiumWindow
3+ size: 223 320
4+ image-source: /images/custom/Shop/item_shop_view
5+ draggable: false
6+ focusable: true
7+ @onEscape: closer_boxShop()
8+
9+ $mobile:
10+ size: 223 280
11+
12+ UIButton
13+ id: itemName
14+ size: 200 25
15+ font: verdana-11px-antialised
16+ color: #E2D524
17+ anchors.top: parent.top
18+ anchors.left: parent.left
19+ margin-left: -02
20+ margin-top: -20
21+
22+ UIButton
23+ id: paymentVip
24+ !tooltip: tr('Compre usando suas Vip Coins.')
25+ image-source: /images/custom/Shop/buyBt_3
26+ size: 91 20
27+ font: verdana-11px-antialised
28+ anchors.top: itemName.bottom
29+ anchors.left: parent.left
30+ margin-left: 05
31+ margin-top: 05
32+ opacity: 0.8
33+ $pressed:
34+ opacity: 0.6
35+ $hover !pressed:
36+ opacity: 1.0
37+
38+ UIButton
39+ id: paymentOnline
40+ image-source: /images/custom/Shop/buyBt_2
41+ size: 91 20
42+ font: verdana-11px-antialised
43+ anchors.top: itemName.bottom
44+ anchors.left: paymentVip.right
45+ margin-left: 05
46+ margin-top: 05
47+ opacity: 0.8
48+ $pressed:
49+ opacity: 0.6
50+ $hover !pressed:
51+ opacity: 1.0
52+
53+ Label
54+ id: itemDetalhes
55+ size: 198 190
56+ text-wrap: true
57+ text-offset: 05 05
58+ anchors.top: paymentVip.bottom
59+ anchors.left: parent.left
60+ margin-left: -02
61+ margin-top: 10
62+
63+ UIButton
64+ id: exitButton
65+ !text: tr('CANCELAR')
66+ image-source: /images/custom/UI/button
67+ height: 30
68+ font: verdana-11px-antialised
69+ anchors.bottom: parent.bottom
70+ anchors.right: parent.right
71+ anchors.left: parent.left
72+ margin-left: 05
73+ margin-right: 05
74+ margin-bottom: 05
75+ @onClick: closer_boxShop()
76+ opacity: 0.8
77+ $pressed:
78+ opacity: 0.6
79+ $hover !pressed:
80+ opacity: 1.0
A RD_Client/modules/game_menu/spy.otui +200-0 View file
@@ -0,0 +1,200 @@
1+MainWindow
2+ id: spyWindow
3+ text: Player
4+ font: sans-bold-16px
5+ size: 253 350
6+ focusable: true
7+ @onEscape: onSpyClose()
8+
9+ UIButton
10+ id: fecharSpy
11+ height: 25
12+ !text: tr('CERRAR')
13+ image-source: /images/custom/UI/button_3
14+ anchors.bottom: parent.bottom
15+ anchors.right: parent.right
16+ anchors.left: parent.left
17+ margin-left: 5
18+ margin-right: 5
19+ margin-bottom: 5
20+ opacity: 0.8
21+ @onClick: onSpyClose()
22+ $pressed:
23+ opacity: 0.6
24+ $hover !pressed:
25+ opacity: 1.0
26+
27+ UIButton
28+ id: showSpy
29+ size: 240 275
30+ anchors.top: parent.top
31+ anchors.left: parent.left
32+ margin-top: -10
33+ margin-left: -10
34+
35+ UIButton
36+ id: SpyAmulet
37+ size: 42 50
38+ image-source: /images/game/slots/amulet
39+ icon-rect: 5 5 32 32
40+ anchors.top: parent.top
41+ anchors.left: parent.left
42+ margin-left: 6
43+ margin-top: 10
44+
45+ UIButton
46+ id: SpyHelmet
47+ size: 42 50
48+ image-source: /images/game/slots/helmet
49+ icon-rect: 5 5 32 32
50+ anchors.top: parent.top
51+ anchors.left: SpyAmulet.right
52+ margin-left: 5
53+ margin-top: 10
54+
55+ UIButton
56+ id: SpyBackpack
57+ size: 42 50
58+ image-source: /images/game/slots/backpack
59+ icon-rect: 5 5 32 32
60+ anchors.top: parent.top
61+ anchors.left: SpyHelmet.right
62+ margin-left: 5
63+ margin-top: 10
64+
65+ UIButton
66+ id: SpySummon
67+ size: 89 153
68+ icon-align: center
69+ image-source: /images/custom/UI/infoInv
70+ icon: /images/custom/Monster/0
71+ anchors.top: parent.top
72+ anchors.left: SpyBackpack.right
73+ margin-left: 7
74+ margin-top: 10
75+
76+ UIButton
77+ id: SpyImgSummonEquip
78+ size: 64 64
79+ phantom: true
80+ icon: /images/custom/Monster/0
81+ anchors.top: parent.top
82+ anchors.left: parent.left
83+ margin-left: 13
84+ margin-top: 35
85+
86+ UIButton
87+ id: SpyStarSummonEquip
88+ size: 31 10
89+ image-source: /images/custom/UI/star_0
90+ anchors.top: SpyImgSummonEquip.bottom
91+ anchors.left: parent.left
92+ margin-left: 30
93+ margin-top: 37
94+
95+ UIButton
96+ id: SpyGun01
97+ size: 42 50
98+ image-source: /images/game/slots/gun
99+ icon-rect: 5 5 32 32
100+ anchors.top: SpyAmulet.bottom
101+ anchors.left: parent.left
102+ margin-left: 6
103+ margin-top: 3
104+
105+ UIButton
106+ id: SpyArmor
107+ size: 42 50
108+ image-source: /images/game/slots/armor
109+ icon-rect: 5 5 32 32
110+ anchors.top: SpyAmulet.bottom
111+ anchors.left: SpyGun01.right
112+ margin-left: 5
113+ margin-top: 3
114+
115+ UIButton
116+ id: SpyGlove
117+ size: 42 50
118+ image-source: /images/game/slots/glove
119+ icon-rect: 5 5 32 32
120+ anchors.top: SpyAmulet.bottom
121+ anchors.right: SpySummon.left
122+ margin-right: 7
123+ margin-top: 3
124+
125+ UIButton
126+ id: SpyRing01
127+ size: 42 50
128+ image-source: /images/game/slots/ring
129+ anchors.top: SpyGun01.bottom
130+ icon-rect: 5 5 32 32
131+ anchors.left: parent.left
132+ margin-left: 6
133+ margin-top: 3
134+
135+ UIButton
136+ id: SpyLegs
137+ size: 42 50
138+ image-source: /images/game/slots/leg
139+ icon-rect: 5 5 32 32
140+ anchors.top: SpyGun01.bottom
141+ anchors.left: SpyRing01.right
142+ margin-left: 5
143+ margin-top: 3
144+
145+ UIButton
146+ id: SpyBoots
147+ size: 42 50
148+ image-source: /images/game/slots/boot
149+ icon-rect: 5 5 32 32
150+ anchors.top: SpyGun01.bottom
151+ anchors.left: SpyLegs.right
152+ margin-left: 5
153+ margin-top: 3
154+
155+ UIButton
156+ id: SpyGoldCoin
157+ size: 135 20
158+ text: 0
159+ image-source: /images/custom/UI/goldInfo
160+ anchors.top: SpyRing01.bottom
161+ anchors.left: parent.left
162+ margin-left: 6
163+ margin-top: 10
164+
165+ UIButton
166+ id: SpyOnlineCoin
167+ size: 86 20
168+ text: 0
169+ image-source: /images/custom/UI/onlineCoin
170+ anchors.top: SpyRing01.bottom
171+ anchors.left: SpyGoldCoin.right
172+ margin-left: 7
173+ margin-top: 10
174+
175+ UIButton
176+ id: SpySkill1
177+ size: 38 38
178+ image-source: /images/custom/UI/insert_skill
179+ anchors.top: SpyOnlineCoin.bottom
180+ anchors.left: parent.left
181+ margin-left: 30
182+ margin-top: 20
183+
184+ UIButton
185+ id: SpySkill2
186+ size: 38 38
187+ image-source: /images/custom/UI/insert_skill
188+ anchors.top: SpyOnlineCoin.bottom
189+ anchors.left: SpySkill1.right
190+ margin-left: 30
191+ margin-top: 20
192+
193+ UIButton
194+ id: SpySkill3
195+ size: 38 38
196+ image-source: /images/custom/UI/insert_skill
197+ anchors.top: SpyOnlineCoin.bottom
198+ anchors.left: SpySkill2.right
199+ margin-left: 30
200+ margin-top: 20
A RD_Client/modules/game_minimap/minimap.lua +512-0 View file
@@ -0,0 +1,512 @@
1+--[[
2+ game_minimap - Minimap module using native UIMinimap engine widget
3+
4+ Engine API exposed (from uiminimap.cpp / luafunctions_client.cpp):
5+ minimapWidget:zoomIn() -- zoom +1 step
6+ minimapWidget:zoomOut() -- zoom -1 step
7+ minimapWidget:setZoom(int) -- set absolute zoom (-5 to 5)
8+ minimapWidget:setMixZoom(int) -- set minimum zoom level
9+ minimapWidget:setMaxZoom(int) -- set maximum zoom level
10+ minimapWidget:getZoom() -- get current zoom
11+ minimapWidget:getMinZoom() -- get min zoom
12+ minimapWidget:getMaxZoom() -- get max zoom
13+ minimapWidget:getScale() -- get current scale factor
14+ minimapWidget:setCameraPosition(pos) -- center map on position
15+ minimapWidget:getCameraPosition() -- get current center position
16+ minimapWidget:floorUp() -- move camera floor up
17+ minimapWidget:floorDown() -- move camera floor down
18+ minimapWidget:getTilePoint(pos) -- screen point for map position
19+ minimapWidget:getTilePosition(pt) -- map position for screen point
20+ minimapWidget:getTileRect(pos) -- screen rect for map position
21+ minimapWidget:anchorPosition(w, edge, pos, edge2)
22+ minimapWidget:fillPosition(w, pos)
23+ minimapWidget:centerInPosition(w, pos)
24+
25+ OTUI style nodes supported (onStyleApply):
26+ zoom: int -- initial zoom
27+ min-zoom: int -- minimum zoom
28+ max-zoom: int -- maximum zoom
29+
30+ Lua callbacks fired by engine:
31+ minimapWidget:onZoomChange(zoom, oldZoom)
32+ minimapWidget:onCameraPositionChange(pos, oldPos)
33+--]]
34+
35+minimapWindow = nil
36+minimapWidget = nil
37+safeModeButtonPopup = nil
38+
39+-- Fallback PvP mode constants in case this client build doesn't define them globally
40+PvpPeaceful = PvpPeaceful or 0
41+PvpDefensive = PvpDefensive or 1
42+PvpOffensive = PvpOffensive or 2
43+
44+local directionAbbr = {
45+ [0] = 'N',
46+ [1] = 'E',
47+ [2] = 'S',
48+ [3] = 'W'
49+}
50+
51+local pvpModes = {
52+ ['skull'] = PvpOffensive,
53+ ['shield'] = PvpPeaceful,
54+ ['sword'] = PvpDefensive
55+}
56+
57+local pvpButtons = {
58+ [PvpPeaceful] = {
59+ { id = 'shield', icon = 'icon_shield', background = 'icon_border_off', color = '#A6AAB2', tooltip = 'Peaceful' },
60+ { id = 'sword', icon = 'icon_sword', color = '#A6AAB2', tooltip = 'Defensive' },
61+ { id = 'skull', icon = 'icon_skull', color = '#A6AAB2', tooltip = 'Offensive' },
62+ },
63+ [PvpDefensive] = {
64+ { id = 'sword', icon = 'icon_sword', background = 'icon_border_yellow', color = '#1F2124', tooltip = 'Defensive' },
65+ { id = 'shield', icon = 'icon_shield', color = '#A6AAB2', tooltip = 'Peaceful' },
66+ { id = 'skull', icon = 'icon_skull', color = '#A6AAB2', tooltip = 'Offensive' },
67+ },
68+ [PvpOffensive] = {
69+ { id = 'skull', icon = 'icon_skull', background = 'icon_border_red', color = '#1F2124', tooltip = 'Offensive' },
70+ { id = 'shield', icon = 'icon_shield', color = '#A6AAB2', tooltip = 'Peaceful' },
71+ { id = 'sword', icon = 'icon_sword', color = '#A6AAB2', tooltip = 'Defensive' },
72+ },
73+}
74+
75+-- setAnglePosition: positions a widget around a circular parent widget at a given angle.
76+-- Used by all the circular minimap buttons.
77+UIWidget.setAnglePosition = function(self, parent, angle, additionalDistance)
78+ if not parent or parent:isDestroyed() then return end
79+
80+ local parentWidth = parent:getWidth()
81+ local parentHeight = parent:getHeight()
82+ local parentPos = parent:getPosition()
83+ local parentCenter = {
84+ x = parentPos.x + parentWidth / 2,
85+ y = parentPos.y + parentHeight / 2
86+ }
87+
88+ local radius = parentWidth / 2 + (additionalDistance or 0) + 5
89+ self.additionalDistance = additionalDistance
90+ self.angle = angle
91+
92+ local rad = math.rad(angle)
93+ local x = parentCenter.x + radius * math.cos(rad)
94+ local y = parentCenter.y + radius * math.sin(rad)
95+
96+ self:setPosition({
97+ x = x - self:getWidth() / 2,
98+ y = y - self:getHeight() / 2
99+ })
100+
101+ -- Update position when parent geometry changes
102+ if self.positionChangeAngleEvent then
103+ for _, event in pairs(self.positionChangeAngleEvent) do
104+ event()
105+ end
106+ self.positionChangeAngleEvent = nil
107+ end
108+
109+ local disconnects = connect(parent, {
110+ onGeometryChange = function()
111+ addEvent(function()
112+ if self and not self:isDestroyed() then
113+ self:setAnglePosition(parent, self.angle, self.additionalDistance)
114+ end
115+ end)
116+ end
117+ })
118+
119+ self.positionChangeAngleEvent = disconnects
120+end
121+
122+function init()
123+ g_ui.importStyle('minimap.otui')
124+
125+ -- Resolve the HUD parent panel safely.
126+ -- This client's game_interface uses gameRootPanel; getHUDPanel() is not defined here.
127+ -- (gameRootPanel is not visible from here: game_interface is sandboxed, so use
128+ -- its getter. The root widget fallback made the minimap show on the login screen.)
129+ local hudPanel = nil
130+ if modules.game_interface and modules.game_interface.getHUDPanel then
131+ hudPanel = modules.game_interface.getHUDPanel()
132+ end
133+ if not hudPanel and modules.game_interface and modules.game_interface.getRootPanel then
134+ hudPanel = modules.game_interface.getRootPanel()
135+ end
136+ if not hudPanel then
137+ hudPanel = g_ui.getRootWidget()
138+ end
139+
140+ minimapWindow = g_ui.createWidget('MinimapWindow', hudPanel)
141+ minimapWindow:hide() -- shown by onGameStart, only once in game
142+ minimapWidget = minimapWindow:recursiveGetChildById('minimap')
143+
144+ safeModeButtonPopup = minimapWindow.safeModeButtonPopup
145+ if minimapWindow.safeModeButton then
146+ minimapWindow.safeModeButton.popup = safeModeButtonPopup
147+ end
148+
149+ -- Wire Lua callbacks from the native UIMinimap
150+ if minimapWidget then
151+ minimapWidget.onZoomChange = function(widget, zoom, oldZoom)
152+ -- Update zoom buttons visual state if needed
153+ end
154+
155+ minimapWidget.onCameraPositionChange = function(widget, pos, oldPos)
156+ -- Called each time the camera moves
157+ end
158+ end
159+
160+ -- Only connect events that actually exist in this client build.
161+ -- onTimeChange and onRemoveAutomapFlag don't exist here.
162+ -- LocalPlayer is nil at module load time — connect it in onGameStart.
163+ connect(g_game, {
164+ onGameStart = onGameStart,
165+ onGameEnd = onGameEnd,
166+ })
167+
168+ if g_game.isOnline() then
169+ onGameStart()
170+ end
171+end
172+
173+function terminate()
174+ disconnect(g_game, {
175+ onGameStart = onGameStart,
176+ onGameEnd = onGameEnd,
177+ onPVPModeChange = onPvpModeChange,
178+ })
179+
180+ -- Disconnect LocalPlayer only if it exists
181+ local lp = g_game.getLocalPlayer and g_game.getLocalPlayer()
182+ if lp then
183+ disconnect(lp, { onPositionChange = onPositionChange })
184+ end
185+
186+ -- Safety save of minimap data
187+ g_minimap.saveOtmm('/minimap.otmm')
188+
189+ if minimapWindow and not minimapWindow:isDestroyed() then
190+ minimapWindow:destroy()
191+ end
192+
193+ minimapWindow = nil
194+ minimapWidget = nil
195+ safeModeButtonPopup = nil
196+end
197+
198+-- Called every time the player moves.
199+-- The engine's setCameraPosition re-centers the UIMinimap on the new position.
200+local playerIcon = nil
201+
202+function onPositionChange(player, newPos, oldPos)
203+ if not minimapWidget or not newPos then return end
204+ minimapWidget:setCameraPosition(newPos)
205+
206+ if not playerIcon or playerIcon:isDestroyed() then
207+ playerIcon = g_ui.createWidget('MiniMapPlayerIcon', minimapWidget)
208+ end
209+
210+ if playerIcon then
211+ minimapWidget:centerInPosition(playerIcon, newPos)
212+
213+ local outfit = playerIcon:getChildById('playerOutfit')
214+ if outfit then
215+ outfit:setDirection(player:getDirection())
216+ end
217+
218+ local frame = playerIcon:getChildById('playerOutfitFrame')
219+ if frame then
220+ -- directions: 0: North, 1: East, 2: South, 3: West
221+ -- Inverted: 0: 180, 1: 270, 2: 0, 3: 90
222+ local rotations = { [0] = 180, [1] = 270, [2] = 0, [3] = 90 }
223+ frame:setRotation(rotations[player:getDirection()] or 0)
224+ end
225+ end
226+end
227+
228+function onGameStart()
229+ minimapWindow:show()
230+
231+ local player = g_game.getLocalPlayer()
232+ if player then
233+ local pos = player:getPosition()
234+ if pos and minimapWidget then
235+ minimapWidget:setCameraPosition(pos)
236+ end
237+ -- Connect position updates now that LocalPlayer exists
238+ connect(player, { onPositionChange = onPositionChange })
239+ end
240+
241+ -- Restore saved PvP mode
242+ if g_settings then
243+ local char = g_game.getCharacterName and g_game.getCharacterName() or ''
244+ local settings = g_settings.getNode('game_minimap')
245+ if settings and settings[char] then
246+ g_game.setPVPMode(settings[char].pvpMode or PvpPeaceful)
247+ end
248+ end
249+
250+ -- Load explored minimap data
251+ if g_resources.fileExists('/minimap.otmm') then
252+ g_minimap.loadOtmm('/minimap.otmm')
253+ end
254+
255+ -- g_game.getTime() doesn't exist in this client — skip clock update on start
256+ -- (clock will update when onTimeChange fires, if the event exists)
257+end
258+
259+function onGameEnd()
260+ minimapWindow:hide()
261+
262+ -- Save PvP mode per character
263+ if g_settings then
264+ local char = g_game.getCharacterName()
265+ local settings = g_settings.getNode('game_minimap') or {}
266+ settings[char] = { pvpMode = g_game.getPVPMode() }
267+ g_settings.setNode('game_minimap', settings)
268+ g_settings.save()
269+ end
270+
271+ -- Save explored minimap data
272+ g_minimap.saveOtmm('/minimap.otmm')
273+end
274+
275+-- Updates the clock label in the top panel
276+function updateTime(time)
277+ if not minimapWindow or not time then return end
278+ local topPanel = minimapWindow.topPanel
279+ if not topPanel then return end
280+ local timeWidget = topPanel.time
281+ if not timeWidget then return end
282+
283+ local pm = time.hour >= 12
284+ local hour = time.hour % 12
285+ if hour == 0 then hour = 12 end
286+
287+ timeWidget:setText(string.format("%02d:%02d %s", hour, time.minute, pm and 'PM' or 'AM'))
288+end
289+
290+-- Zoom in: increases zoom level (+1 step). Range is -5 to 5.
291+function zoomIn()
292+ if minimapWidget then
293+ minimapWidget:zoomIn()
294+ end
295+end
296+
297+-- Zoom out: decreases zoom level (-1 step).
298+function zoomOut()
299+ if minimapWidget then
300+ minimapWidget:zoomOut()
301+ end
302+end
303+
304+-- Handle a click on the minimap widget.
305+-- Uses getTilePosition() (engine method) to convert screen click to world pos,
306+-- then issues a server autoWalk request.
307+function onMinimapClick(mousePos)
308+ if not minimapWidget or not g_game.isOnline() then return end
309+
310+ local player = g_game.getLocalPlayer()
311+ if not player then return end
312+
313+ -- Convert screen pos to world position using engine method
314+ local destination = minimapWidget:getTilePosition(mousePos)
315+ if not destination or (destination.x == 0 and destination.y == 0) then
316+ return
317+ end
318+
319+ -- Keep same floor as the player
320+ local playerPos = player:getPosition()
321+ destination.z = playerPos.z
322+
323+ -- Guard: don't walk if player is in special state
324+ if player:isServerWalking() then
325+ g_game.stop()
326+ end
327+
328+ player:autoWalk(destination, nil, true)
329+end
330+
331+-- Drag support: reposition buttons when dragged around the minimap circle
332+function onMinimapButtonDragEnter(widget, mousePos)
333+ if not minimapWidget then return end
334+
335+ local minimapPos = minimapWidget:getPosition()
336+ local minimapCenter = {
337+ x = minimapPos.x + minimapWidget:getWidth() / 2,
338+ y = minimapPos.y + minimapWidget:getHeight() / 2
339+ }
340+
341+ local angle = math.deg(math.atan2(
342+ mousePos.y - minimapCenter.y,
343+ mousePos.x - minimapCenter.x
344+ ))
345+
346+ widget:setAnglePosition(minimapWidget, angle, widget.additionalDistance)
347+ widget:raise()
348+end
349+
350+-- PvP mode button: toggles between peaceful / defensive / offensive
351+function onPvpButtonClicked(id)
352+ local pvpMode = pvpModes[id]
353+ if pvpMode then
354+ g_game.setPVPMode(pvpMode)
355+ end
356+
357+ local btn = minimapWindow:recursiveGetChildById('safeModeButton')
358+ if btn then
359+ btn:setOn(false)
360+ end
361+end
362+
363+-- Called by the engine event onPVPModeChange
364+function onPvpModeChange()
365+ local pvpMode = g_game.getPVPMode()
366+ local cfg = pvpButtons[pvpMode]
367+ if not cfg then return end
368+
369+ local popup = safeModeButtonPopup or minimapWindow:recursiveGetChildById('safeModeButtonPopup')
370+ local btn = minimapWindow:recursiveGetChildById('safeModeButton')
371+
372+ if btn then
373+ btn:setTooltip(string.format('PvP Mode: %s', cfg[1].tooltip))
374+ btn:setImageSource(string.format('/images/ui/windows/minimap/%s', cfg[1].background))
375+ btn:setIcon(string.format('/images/ui/windows/minimap/%s', cfg[1].icon))
376+ btn:setIconColor(cfg[1].color)
377+ end
378+
379+ if popup then
380+ local b1 = popup:getChildByIndex(1)
381+ if b1 then
382+ b1:setTooltip(cfg[2].tooltip)
383+ b1:setId(cfg[2].id)
384+ b1:setIcon(string.format('/images/ui/windows/minimap/%s', cfg[2].icon))
385+ end
386+ local b2 = popup:getChildByIndex(2)
387+ if b2 then
388+ b2:setTooltip(cfg[3].tooltip)
389+ b2:setId(cfg[3].id)
390+ b2:setIcon(string.format('/images/ui/windows/minimap/%s', cfg[3].icon))
391+ end
392+ end
393+end
394+
395+-- Floor indicator button (placeholder - floor change display is engine-rendered)
396+function onFloorIndicatorButtonClicked(id)
397+ -- Floor indicators are rendered natively by the minimap engine
398+ -- This button can cycle between floor modes if needed
399+end
400+
401+-- Update the region name label in the top panel
402+function updateRegionName(region, color)
403+ if not minimapWindow or not minimapWindow.topPanel then return end
404+ local widget = minimapWindow.topPanel.regionName
405+ if not widget then return end
406+
407+ local visible = region and region ~= ''
408+ widget:setVisible(visible)
409+ if visible then
410+ widget:setText(region)
411+ if color then widget:setColor(color) end
412+ end
413+end
414+
415+-- ============================================================
416+-- Safe button wrappers — called from OTUI onClick handlers.
417+-- Each one gracefully handles missing modules.
418+-- ============================================================
419+
420+function btnQuestTracker(self)
421+ if modules.game_settings and modules.game_settings.toggleOption then
422+ modules.game_settings.toggleOption('showQuestTracker')
423+ elseif modules.game_questlog then
424+ -- fallback: toggle quest log if quest tracker not available
425+ local m = modules.game_questlog
426+ if m.toggle then m.toggle() end
427+ end
428+ if self then self:setOn(not self:isOn()) end
429+end
430+
431+function btnTeleport(self)
432+ if modules.game_hearthteleport and modules.game_hearthteleport.GameHearthTeleport then
433+ modules.game_hearthteleport.GameHearthTeleport:teleport()
434+ end
435+end
436+
437+function btnHouseMode(self)
438+ if modules.game_house and modules.game_house.GameHouse then
439+ modules.game_house.GameHouse:toggleHouseMode()
440+ else
441+ if self then self:setOn(not self:isOn()) end
442+ end
443+end
444+
445+function btnChannelSwitch(self)
446+ if modules.game_channel_selector and modules.game_channel_selector.GameSwitchChannel then
447+ modules.game_channel_selector.GameSwitchChannel:toggle()
448+ elseif modules.game_chat then
449+ -- fallback: toggle chat panel
450+ end
451+ if self then self:setOn(not self:isOn()) end
452+end
453+
454+function btnBattleList(self)
455+ if modules.game_battle and modules.game_battle.BattleList then
456+ modules.game_battle.BattleList:toggle()
457+ elseif modules.game_battle and modules.game_battle.toggle then
458+ modules.game_battle.toggle()
459+ end
460+ if self then self:setOn(not self:isOn()) end
461+end
462+
463+function btnMail(self)
464+ if modules.game_social and modules.game_social.GameSocial then
465+ modules.game_social.GameSocial:selectTab('mail')
466+ modules.game_social.GameSocial:toggle()
467+ end
468+ if self then self:setOn(not self:isOn()) end
469+end
470+
471+-- Quest tracking functions
472+rastrearMarkers = rastrearMarkers or {}
473+
474+function onRemoveRastrear()
475+ for _, marker in ipairs(rastrearMarkers) do
476+ if marker and not marker:isDestroyed() then
477+ marker:destroy()
478+ end
479+ end
480+ rastrearMarkers = {}
481+end
482+
483+function onRastrearQuest(pos, desc, count)
484+ if not minimapWidget or not pos then return end
485+ local ok, pt = pcall(function() return minimapWidget:getTilePoint(pos) end)
486+ local parent = minimapWindow
487+ if not parent or parent:isDestroyed() then return end
488+ local marker = g_ui.createWidget('UIWidget', minimapWidget)
489+ if not marker then return end
490+ marker:setSize({width = 12, height = 12})
491+ marker:setImageSource('/images/ui/windows/minimap/icon_compass_blue')
492+ marker:setTooltip(desc or '')
493+ marker:setImageAutoResize(true)
494+ if ok and pt and pt.x > 0 then
495+ marker:setPosition({x = pt.x - 6, y = pt.y - 6})
496+ else
497+ marker._mapPos = pos
498+ end
499+ table.insert(rastrearMarkers, marker)
500+end
501+
502+function onMiniMapaShow()
503+ if not minimapWindow or minimapWindow:isDestroyed() then return end
504+ minimapWindow:show()
505+ if minimapWidget and g_game.isOnline() then
506+ local player = g_game.getLocalPlayer()
507+ if player then
508+ local pos = player:getPosition()
509+ if pos then minimapWidget:setCameraPosition(pos) end
510+ end
511+ end
512+end
A RD_Client/modules/game_minimap/minimap.otmod +9-0 View file
@@ -0,0 +1,9 @@
1+Module
2+ name: game_minimap
3+ description: Circular minimap using native UIMinimap engine renderer
4+ author: EriaRPG
5+ sandboxed: true
6+ scripts: [ minimap ]
7+ dependencies: [ game_interface ]
8+ @onLoad: init()
9+ @onUnload: terminate()
A RD_Client/modules/game_minimap/minimap.otui +415-0 View file
@@ -0,0 +1,415 @@
1+MinimapToggleButton < UIWidget
2+ size: 30 30
3+ icon-size: 16 16
4+ icon-align: center
5+ icon-color: #A6AAB2
6+ opacity: 0.9
7+ image-source: /images/ui/windows/minimap/icon_border_off
8+ $hover:
9+ opacity: 1.0
10+ $pressed:
11+ opacity: 0.9
12+ $on:
13+ icon-color: #181A1D
14+ image-source: /images/ui/windows/minimap/icon_border_on
15+
16+ @onDragEnter: function(a,b,c) modules.game_minimap.onMinimapButtonDragEnter(a,b,c) end
17+ @onSetup: |
18+ function(self)
19+ if self.setupFunc then
20+ self:setupFunc()
21+ end
22+ end
23+
24+MinimapExpandingButton < UIWidget
25+ size: 30 30
26+ icon-size: 16 16
27+ icon-align: center
28+ icon-color: #A6AAB2
29+ opacity: 0.9
30+ image-source: /images/ui/windows/minimap/icon_border_off
31+ $hover:
32+ opacity: 1.0
33+ $pressed:
34+ opacity: 0.9
35+ $on:
36+ //icon-color: #181A1D
37+ //image-source: /images/ui/windows/minimap/icon_border_on
38+
39+ @onSetup: |
40+ function(self)
41+ if self.setupFunc then
42+ self:setupFunc()
43+ end
44+ end
45+
46+// Native engine minimap widget - renders g_minimap directly
47+Minimap < UIMinimap
48+ focusable: false
49+ color: alpha
50+ opacity: 1.0
51+ zoom: 0
52+ min-zoom: -5
53+ max-zoom: 5
54+
55+MinimapWindow < UIWindow
56+ id: minimapWindow
57+ size: 260 260
58+ draggable: true
59+
60+ @onMouseWheel: |
61+ function(widget, mousePos, direction)
62+ if direction == MouseWheelDown then
63+ modules.game_minimap.zoomOut()
64+ elseif direction == MouseWheelUp then
65+ modules.game_minimap.zoomIn()
66+ end
67+ end
68+
69+ // Tile container hidden - engine handles rendering via UIMinimap
70+ UIWidget
71+ id: minimapTileContainer
72+ size: 200 200
73+ anchors.top: parent.top
74+ anchors.left: parent.left
75+ phantom: true
76+ margin-left: 30
77+ margin-top: 40
78+ visible: false
79+
80+ UIWidget
81+ id: background
82+ anchors.fill: minimap
83+ image-source: /images/ui/windows/minimap/empty_background
84+ phantom: true
85+
86+ // Native UIMinimap renderer
87+ Minimap
88+ id: minimap
89+ anchors.centerIn: border
90+ size: 196 196
91+ clipping: true
92+ background-color: #00000000
93+ color: #00000000
94+
95+
96+ UIWidget
97+ id: border
98+ anchors.top: topPanel.bottom
99+ anchors.horizontalCenter: parent.horizontalCenter
100+ image-source: /images/ui/windows/minimap/border
101+ margin-top: 5
102+ phantom: true
103+
104+ // Region name / clock header. Hidden: nothing in this client fills it
105+ // (updateTime/updateRegionName are never called). Kept as the anchor for
106+ // the minimap border so the minimap stays where it is.
107+ UIWidget
108+ id: topPanel
109+ visible: false
110+ anchors.left: parent.left
111+ anchors.right: parent.right
112+ anchors.top: parent.top
113+ margin-left: 10
114+ margin-right: 10
115+ image-border: 15
116+ padding: 5
117+ image-source: /images/ui/windows/minimap/btnTime
118+
119+ UIWidget
120+ id: time
121+ anchors.right: parent.right
122+ anchors.verticalCenter: parent.verticalCenter
123+ margin-right: 10
124+ text-auto-resize: true
125+ text-align: right
126+ text-offset: 0 2
127+
128+ UIWidget
129+ id: regionName
130+ anchors.fill: parent
131+ text-auto-resize: true
132+ text-align: left
133+ text-offset: 0 2
134+ margin-left: 10
135+
136+ UIWidget
137+ id: zoomIn
138+ size: 25 25
139+ opacity: 0.9
140+ $hover:
141+ opacity: 1.0
142+ $pressed:
143+ opacity: 0.9
144+ &offset: -3
145+ @onClick: modules.game_minimap.zoomIn()
146+ @onSetup: |
147+ self:setAnglePosition(self:getParent().minimap, 52.5, self.offset)
148+
149+ UIWidget
150+ id: zoomInImage
151+ image-source: /images/ui/windows/minimap/zoomin
152+ image-auto-resize: true
153+ phantom: true
154+ &offset: -3
155+ @onSetup: |
156+ self:setAnglePosition(self:getParent().minimap, 52.5, self.offset)
157+
158+ UIWidget
159+ id: zoomOut
160+ size: 25 25
161+ opacity: 0.9
162+ $hover:
163+ opacity: 1.0
164+ $pressed:
165+ opacity: 0.9
166+ &offset: -3
167+ @onClick: modules.game_minimap.zoomOut()
168+ @onSetup: |
169+ self:setAnglePosition(self:getParent().minimap, 37.5, self.offset)
170+
171+ UIWidget
172+ id: zoomOutImage
173+ image-source: /images/ui/windows/minimap/zoomout
174+ image-auto-resize: true
175+ phantom: true
176+ &offset: -3
177+ @onSetup: |
178+ self:setAnglePosition(self:getParent().minimap, 37.5, self.offset)
179+
180+ MinimapToggleButton
181+ id: questTrackerButton
182+ draggable: true
183+ icon-source: /images/ui/windows/minimap/icon_questTracker
184+ @onClick: modules.game_minimap.btnQuestTracker(self)
185+ &offset: -3
186+ &tooltip: 'Toggle quest tracker'
187+ @onSetup: |
188+ self:setAnglePosition(self:getParent().minimap, 95, self.offset)
189+
190+ MinimapToggleButton
191+ id: teleportButton
192+ draggable: true
193+ image-source: /images/ui/windows/minimap/hearth_background
194+ icon-source: /images/ui/windows/minimap/icon_hearth
195+ icon-color: #36F991
196+ $on:
197+ image-source: /images/ui/windows/minimap/hearth_background
198+ icon-color: #A6AAB2
199+ $disabled:
200+ icon-color: #A6AAB2
201+ opacity: 0.8
202+ @onClick: modules.game_minimap.btnTeleport(self)
203+ &offset: -3
204+ &tooltip: 'Teleport to Ravencrest'
205+ @onSetup: |
206+ self:setAnglePosition(self:getParent().minimap, 350, self.offset)
207+
208+ UIWidget
209+ id: safeModeButtonPopup
210+ anchors.verticalCenter: safeModeButton.verticalCenter
211+ anchors.right: safeModeButton.left
212+ image-source: /images/ui/windows/minimap/background
213+ margin-right: -20
214+ visible: false
215+
216+ UIWidget
217+ size: 16 16
218+ anchors.left: parent.left
219+ anchors.verticalCenter: parent.verticalCenter
220+ margin-left: 5
221+ @onClick: modules.game_minimap.onPvpButtonClicked(self:getId())
222+ opacity: 0.8
223+ $hover:
224+ opacity: 1.0
225+
226+ UIWidget
227+ size: 16 16
228+ anchors.left: parent.left
229+ anchors.verticalCenter: parent.verticalCenter
230+ margin-left: 30
231+ @onClick: modules.game_minimap.onPvpButtonClicked(self:getId())
232+ opacity: 0.8
233+ $hover:
234+ opacity: 1.0
235+
236+ UIWidget
237+ width: 1
238+ anchors.left: parent.left
239+ anchors.top: prev.top
240+ anchors.bottom: prev.bottom
241+ background-color: #3A3D43
242+ margin-left: 25
243+
244+ MinimapExpandingButton
245+ id: safeModeButton
246+ draggable: true
247+ @onClick: |
248+ self:setOn(not self:isOn())
249+ local popup = modules.game_minimap.safeModeButtonPopup
250+ if popup then popup:setVisible(self:isOn()) end
251+ &offset: 5
252+ &tooltip:'Toggle player attack (PvP)'
253+ &setupFunc: |
254+ function(self)
255+ self:setAnglePosition(self:getParent().minimap, 225, self.offset)
256+ end
257+
258+ UIWidget
259+ id: floorIndicatorButtonPopup
260+ anchors.verticalCenter: floorIndicatorButton.verticalCenter
261+ anchors.right: floorIndicatorButton.left
262+ image-source: /images/ui/windows/minimap/background
263+ margin-right: -20
264+ visible: false
265+
266+ UIWidget
267+ size: 16 16
268+ anchors.left: parent.left
269+ anchors.verticalCenter: parent.verticalCenter
270+ margin-left: 5
271+ @onClick: modules.game_minimap.onFloorIndicatorButtonClicked(self:getId())
272+ opacity: 0.8
273+ $hover:
274+ opacity: 1.0
275+
276+ UIWidget
277+ size: 16 16
278+ anchors.left: parent.left
279+ anchors.verticalCenter: parent.verticalCenter
280+ margin-left: 30
281+ @onClick: modules.game_minimap.onFloorIndicatorButtonClicked(self:getId())
282+ opacity: 0.8
283+ $hover:
284+ opacity: 1.0
285+
286+ UIWidget
287+ width: 1
288+ anchors.left: parent.left
289+ anchors.top: prev.top
290+ anchors.bottom: prev.bottom
291+ background-color: #3A3D43
292+ margin-left: 25
293+
294+ MinimapExpandingButton
295+ id: floorIndicatorButton
296+ draggable: true
297+ icon-source: /images/ui/windows/minimap/icon_floor_enabled
298+ @onClick: |
299+ self:setOn(not self:isOn())
300+ &offset: -2
301+ &tooltip:'Toggle floor change indicators'
302+ &setupFunc: |
303+ function(self)
304+ self:setAnglePosition(self:getParent().minimap, 130, self.offset)
305+ end
306+
307+ MinimapToggleButton
308+ id: houseMakerButton
309+ draggable: true
310+ icon-source: /images/ui/windows/minimap/icon_house
311+ @onClick: modules.game_minimap.btnHouseMode(self)
312+ &offset: 2
313+ &tooltip: 'Toggle house mode'
314+ &setupFunc: |
315+ function(self)
316+ self:setAnglePosition(self:getParent().minimap, 195, self.offset)
317+ end
318+
319+ MinimapToggleButton
320+ id: channelSwitchButton
321+ draggable: true
322+ @onClick: modules.game_minimap.btnChannelSwitch(self)
323+ &offset: -3
324+ &tooltip: 'Channel Selector'
325+ text-offset: 0 2
326+ @onSetup: |
327+ self:setAnglePosition(self:getParent().minimap, 15, self.offset)
328+
329+ MinimapToggleButton
330+ id: battleListButton
331+ draggable: true
332+ icon-source: /images/ui/windows/minimap/battle_list
333+ @onClick: modules.game_minimap.btnBattleList(self)
334+ &offset: 0
335+ &tooltip: 'Battle List'
336+ text-offset: 0 2
337+ icon-offset: -1 0
338+ @onSetup: |
339+ self:setAnglePosition(self:getParent().minimap, 165, self.offset)
340+
341+ MinimapToggleButton
342+ id: mailButton
343+ draggable: true
344+ icon-source: /images/ui/windows/minimap/mail
345+ @onClick: modules.game_minimap.btnMail(self)
346+ &offset: 2
347+ &tooltip: 'Mailbox'
348+ $hover:
349+ icon-source: /images/ui/windows/minimap/mail_hover
350+ $on:
351+ icon-color: white
352+ text-offset: 0 2
353+ @onSetup: |
354+ self:setAnglePosition(self:getParent().minimap, 300, self.offset)
355+
356+ UIWidget
357+ id: iconWind
358+ image-source: /images/ui/windows/minimap/icon_wind
359+ size: 56 45
360+ text: S
361+ text-offset: 31 0
362+ @onSetup: |
363+ self:setAnglePosition(self:getParent().minimap, 330)
364+
365+
366+MiniMapPlayerIcon < UIWidget
367+ anchors.top: parent.top
368+ anchors.left: parent.left
369+ size: 24 30
370+
371+ UICreature
372+ id: playerOutfit
373+ anchors.top: parent.top
374+ anchors.left: parent.left
375+ size: 20 20
376+ margin-top: 2
377+ margin-left: 2
378+ clipping: true
379+ outfit-scale: 1
380+ outfit-offset: 5 3
381+ outfit-direction: 2
382+ phantom: true
383+
384+ UIWidget
385+ id: playerOutfitFrame
386+ anchors.fill: parent
387+ image-source: /images/ui/windows/minimap/arrows/worldmap/player_arrow
388+ image-auto-resize: true
389+ phantom: true
390+
391+MiniMapPartyIcon < UIWidget
392+ anchors.top: parent.top
393+ anchors.left: parent.left
394+ size: 24 30
395+
396+ UICreature
397+ id: partyOutfit
398+ anchors.top: parent.top
399+ anchors.left: parent.left
400+ size: 20 20
401+ margin-top: 2
402+ margin-left: 2
403+ clipping: true
404+ outfit-scale: 1
405+ outfit-offset: 5 3
406+ outfit-direction: 2
407+ phantom: true
408+
409+ UIWidget
410+ id: partyOutfitFrame
411+ anchors.fill: parent
412+ image-source: /images/ui/windows/minimap/arrow/frame_party
413+ image-auto-resize: true
414+ margin-top: 1
415+ phantom: true
Top