diff --git a/README.md b/README.md
index 0aaeb866d6..01a261237c 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
1.  [What is otclient?](#whatisotclient)
2. 🚀 [Features](#features)
-6.
[The Mobile Project](#themobileproject)
+6.
[The Mobile Project](#themobileproject)
3. 🔨 [Compiling](#compiling)
4. 🐳 [Docker](#docker)
5. 🩺 [Need help?](#need-help?)
@@ -151,7 +151,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
-|
|
|
|
+|
|
|
|
|-------------------------------------------|---------------|-------------------------|
| ThingCategory Attached Effect | Texture(Png) Attached Effect |
Particule |
@@ -215,7 +215,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
- Tile Widget [Wiki](https://github.com/mehah/otclient/wiki/Tutorial-Attached-Effects)
-|
|
|
|
+|
|
|
|
|-------------------------------------------|---------------|-------------------------|
|Title Attached Effect | Title Widget | Title Particule |
@@ -248,12 +248,17 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
In short, if the ping gets high, the camera moves slower, trying to keep up with the server's response time. If the ping goes down soon after, the camera will move faster. Of course, this all depends on your character's speed.
+-
+ Texture Atlas system
+
+
+
##### 🙋 Community (Features)
- Mobile Support [@tuliomagalhaes](https://github.com/tuliomagalhaes) & [@BenDol](https://github.com/BenDol) & [@SkullzOTS](https://github.com/SkullzOTS)
-|
|
|
|
+|
|
|
|
|-------------------------------------------|---------------|-------------------------|
| Interface | Density Pixel | Joystick (patrykq) |
@@ -271,7 +276,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
-|
|
|
|
+|
|
|
|
|-------------------------------------------|---------------|-------------------------|
| Example interface | Example in game | future discord-game-sdk |
@@ -280,7 +285,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
- Access to widget children via widget.childId by [@Hugo0x1337](https://github.com/Hugo0x1337)
- Shader System Fix (CTRL + Y) by [@FreshyPeshy](https://github.com/FreshyPeshy)
-|
|
|
|
+|
|
|
|
|-------------------------------------------|---------------|-------------------------|
| Creature | Map | Mount |
@@ -339,7 +344,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
-
+
@@ -352,7 +357,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
-
+
@@ -408,7 +413,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
-|
|
|
+|
|
|
|-------------------------------------------|---------------|
| Interface | In-game |
- Imbuement tracker by [@Reyaleman](https://github.com/reyaleman)
@@ -437,7 +442,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
- Shader with Framebuffer ([@SkullzOTS](https://github.com/SkullzOTS), [@Mryukiimaru](https://github.com/Mryukiimaru), [@JeanTheOne](https://github.com/JeanTheOne), [@KizaruHere](https://github.com/KizaruHere))
-|
|
|
|
+|
|
|
|
|-------------------------------------------|---------------|-------------------------|
| Creature. | Items | UICreature |
@@ -454,7 +459,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu
- keybinds
- Cam system
-##
The Mobile Project
+##
The Mobile Project
The Mobile Project
This is a fork of edubart's otclient. The objective of this fork it's to develop a runnable otclient on mobiles devices.
@@ -534,9 +539,9 @@ Have found a bug? Please create an issue in our [bug tracker](https://github.com
| TFS 1.5
(8.0 / 8.60) | Downgrade nekiro /
MillhioreBT | [force-new-walking-formula: true](https://github.com/mehah/otclient/blob/cf7badda978de88cb3724615688e3d9da2ff4207/data/setup.otml#L21)
[item-ticks-per-frame: 500](https://github.com/mehah/otclient/blob/cf7badda978de88cb3724615688e3d9da2ff4207/data/setup.otml#L32) | ✅ |
| TFS 1.4.2
(10.98) | Release Otland | | ✅ |
| TFS 1.6
(13.10) | Main repo
otland (2024) | [See wiki](https://github.com/mehah/otclient/wiki/Tutorial-to-Use-OTC-in-TFS-main) | ✅ |
-| Canary
(13.21 / 13.32 / 13.40) | OpenTibiaBr | [Assets , Enable HTTP login and port 80](https://docs.opentibiabr.com/opentibiabr/projects/otclient-redemption#how-to-connect-on-canary-with-otclient-redemption) | ✅ |
-| Canary
(14.05) | OpenTibiaBr | [Assets , Enable HTTP login and port 80](https://docs.opentibiabr.com/opentibiabr/projects/otclient-redemption#how-to-connect-on-canary-with-otclient-redemption) | ❌ |
-
+| Canary
(13.21 / 13.32 / 13.40) | OpenTibiaBr | [See Wiki](https://docs.opentibiabr.com/opentibiabr/projects/otclient-redemption/about#how-to-connect-on-canary-with-otclient-redemption) | ✅ |
+| Canary
(14.00 ~ 14.12) | OpenTibiaBr | [See Wiki](https://docs.opentibiabr.com/opentibiabr/projects/otclient-redemption/about#how-to-connect-on-canary-with-otclient-redemption) | ✅ |
+| Canary
(15.00 ~ 15.10) | OpenTibiaBr | [See Wiki](https://docs.opentibiabr.com/opentibiabr/projects/otclient-redemption/about#how-to-connect-on-canary-with-otclient-redemption) | ❌ |
diff --git a/data/images/background.png b/data/images/background.png
index f64220061e..ae52adc868 100644
Binary files a/data/images/background.png and b/data/images/background.png differ
diff --git a/data/images/game/cyclopedia/magical_off.png b/data/images/game/cyclopedia/magical_off.png
new file mode 100644
index 0000000000..7afd7fb218
Binary files /dev/null and b/data/images/game/cyclopedia/magical_off.png differ
diff --git a/data/images/game/cyclopedia/magical_on.png b/data/images/game/cyclopedia/magical_on.png
new file mode 100644
index 0000000000..d44b05b482
Binary files /dev/null and b/data/images/game/cyclopedia/magical_on.png differ
diff --git a/data/images/ui/item.png b/data/images/ui/item.png
index 6793eb1904..6b605a3ba6 100644
Binary files a/data/images/ui/item.png and b/data/images/ui/item.png differ
diff --git a/data/setup.otml b/data/setup.otml
index e64bb76f16..896d40eecb 100644
--- a/data/setup.otml
+++ b/data/setup.otml
@@ -1,6 +1,6 @@
game
sprite-size: 32
- last-supported-version: 1340
+ last-supported-version: 1412
draw-typing: false
typing-icon: /images/game/console/typing
diff --git a/modules/game_blessing/blessing.lua b/modules/game_blessing/blessing.lua
index caedf9d2d2..2cf8f3036f 100644
--- a/modules/game_blessing/blessing.lua
+++ b/modules/game_blessing/blessing.lua
@@ -92,6 +92,8 @@ function onUpdateBlessDialog(data)
label.text:setText(entry.playerBlessCount .. " (" .. entry.store .. ")")
if totalCount >= 1 then
label.enabled:setImageSource("images/" .. i .. "_on")
+ else
+ label.enabled:setImageSource("images/" .. i)
end
end
diff --git a/modules/game_cyclopedia/cyclopedia_widgets.otui b/modules/game_cyclopedia/cyclopedia_widgets.otui
index eb43f81459..7d6a6a66f1 100644
--- a/modules/game_cyclopedia/cyclopedia_widgets.otui
+++ b/modules/game_cyclopedia/cyclopedia_widgets.otui
@@ -710,6 +710,10 @@ CharmItem < UICheckBox
image-source: /images/ui/item
image-border: 10
phantom: true
+ @onSetup: |
+ if g_game.getClientVersion() >= 1410 then
+ self:setVisible(false)
+ end
Label
id: Value
anchors.centerIn: parent
@@ -736,8 +740,14 @@ CharmItem < UICheckBox
margin-bottom: 7
image-source: /images/ui/panel_flat
image-border: 10
- size: 37 37
phantom: true
+ @onSetup: |
+ if g_game.getClientVersion() >= 1410 then
+ self:setSize("46 46")
+ self:setMarginBottom(-5)
+ else
+ self:setSize("37 37")
+ end
UIWidget
size: 32 32
id: image
@@ -749,6 +759,22 @@ CharmItem < UICheckBox
anchors.centerIn: parent
image-source: /images/ui/ditherpattern
phantom: true
+ @onSetup: |
+ if g_game.getClientVersion() >= 1410 then
+ self:setSize("43 43")
+ else
+ self:setSize("35 35")
+ end
+ UIWidget
+ id: border
+ anchors.centerIn: parent
+ phantom: true
+ @onSetup: |
+ if g_game.getClientVersion() >= 1410 then
+ self:setVisible(true)
+ else
+ self:setVisible(false)
+ end
MarkListItem < ButtonBox
icon-size: 11 11
icon-offset: 3 3
@@ -1422,4 +1448,4 @@ BlessCreate < UIWidget
margin-left: 8
anchors.left: prev.right
anchors.verticalCenter: parent.verticalCenter
- focusable: false
\ No newline at end of file
+ focusable: false
diff --git a/modules/game_cyclopedia/game_cyclopedia.lua b/modules/game_cyclopedia/game_cyclopedia.lua
index 06644be4bb..309165f861 100644
--- a/modules/game_cyclopedia/game_cyclopedia.lua
+++ b/modules/game_cyclopedia/game_cyclopedia.lua
@@ -21,7 +21,7 @@ local ButtonBestiary = nil
local tabStack = {}
local previousType = nil
local windowTypes = {}
-
+local magicalArchives = nil
function toggle(defaultWindow)
if not controllerCyclopedia.ui then
return
@@ -58,6 +58,7 @@ function controllerCyclopedia:onGameStart()
character = buttonSelection:recursiveGetChildById('character')
bosstiary = buttonSelection:recursiveGetChildById('bosstiary')
bossSlot = buttonSelection:recursiveGetChildById('bossSlot')
+ magicalArchives = buttonSelection:recursiveGetChildById('magicalArchives')
windowTypes = {
items = { obj = items, func = showItems },
@@ -67,7 +68,8 @@ function controllerCyclopedia:onGameStart()
houses = { obj = houses, func = showHouse },
character = { obj = character, func = showCharacter },
bosstiary = { obj = bosstiary, func = showBosstiary },
- bossSlot = { obj = bossSlot, func = showBossSlot }
+ bossSlot = { obj = bossSlot, func = showBossSlot },
+ magicalArchives = { obj = magicalArchives, func = showMagicalArchives },
}
g_ui.importStyle("cyclopedia_widgets")
@@ -93,6 +95,12 @@ function controllerCyclopedia:onGameStart()
onUpdateCyclopediaCharacterItemSummary = Cyclopedia.loadCharacterItems,
onParseCyclopediaCharacterAppearances = Cyclopedia.loadCharacterAppearances,
onParseCyclopediaStoreSummary = Cyclopedia.onParseCyclopediaStoreSummary,
+-- character 14.10
+ onCyclopediaCharacterOffenceStats = Cyclopedia.onCyclopediaCharacterOffenceStats,
+ onCyclopediaCharacterDefenceStats = Cyclopedia.onCyclopediaCharacterDefenceStats,
+ onCyclopediaCharacterMiscStats = Cyclopedia.onCyclopediaCharacterMiscStats,
+
+
-- charms
onUpdateBestiaryCharmsData = Cyclopedia.loadCharms,
-- items
@@ -193,7 +201,9 @@ function controllerCyclopedia:onGameStart()
}})
end
-
+ if g_game.getClientVersion() >= 1410 then
+ controllerCyclopedia.ui.CharmsBase.Icon:setImageSource("/game_cyclopedia/images/monster-icon-bonuspoints")
+ end
end
@@ -227,6 +237,20 @@ function controllerCyclopedia:onTerminate()
trackerMiniWindowBosstiary:destroy()
trackerMiniWindowBosstiary = nil
end
+
+ if CyclopediaButton then
+ CyclopediaButton:destroy()
+ CyclopediaButton = nil
+ end
+ if ButtonBossSlot then
+ ButtonBossSlot:destroy()
+ ButtonBossSlot = nil
+ end
+ if ButtonBestiary then
+ ButtonBestiary:destroy()
+ ButtonBestiary = nil
+ end
+ onTerminateCharm()
end
function hide()
@@ -257,7 +281,7 @@ function show(defaultWindow)
controllerCyclopedia.ui:raise()
controllerCyclopedia.ui:focus()
SelectWindow(defaultWindow, false)
- controllerCyclopedia.ui.GoldBase.Value:setText(Cyclopedia.formatGold(g_game.getLocalPlayer():getResourceBalance(1)))
+ controllerCyclopedia.ui.GoldBase.Value:setText(Cyclopedia.formatGold(g_game.getLocalPlayer():getResourceBalance()))
end
function toggleBack()
diff --git a/modules/game_cyclopedia/game_cyclopedia.otui b/modules/game_cyclopedia/game_cyclopedia.otui
index f9e1ba13d0..825baf9cad 100644
--- a/modules/game_cyclopedia/game_cyclopedia.otui
+++ b/modules/game_cyclopedia/game_cyclopedia.otui
@@ -197,6 +197,28 @@ MainWindow
image-clip: 0 34 34 34
image-source: /images/game/cyclopedia/bossSlot_off
+ UIButton
+ id: magicalArchives
+ anchors.top: prev.top
+ anchors.left: prev.right
+ margin-left: 1
+ @onClick: SelectWindow("magicalArchives")
+
+ $on:
+ size: 150 34
+ image-clip: 0 0 150 34
+ image-source: /images/game/cyclopedia/magical_on
+
+ $!on:
+ size: 34 34
+ image-clip: 0 0 34 34
+ image-source: /images/game/cyclopedia/magical_off
+
+ $pressed:
+ size: 34 34
+ image-clip: 0 34 34 34
+ image-source: /images/game/cyclopedia/magical_off
+
UIWidget
id: contentContainer
anchors.top: prev.bottom
@@ -277,11 +299,36 @@ MainWindow
!text: tr('0')
text-auto-resize: true
color: #BDBDBD
+
+ UIWidget
+ id: CharmsBase1410
+ visible: false
+ anchors.bottom: parent.bottom
+ anchors.left: prev.right
+ margin-left: 5
+ image-source: /images/ui/item
+ image-border: 10
+ size: 100 20
+ !tooltip: tr("Minor Charm echoes\nEarned by unlocking or upgrading major charm and promotion your character")
+ UIWidget
+ id: Icon
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ margin-right: 4
+ image-source: /game_cyclopedia/images/minor-charm-echoes
+ Label
+ id: Value
+ anchors.right: Icon.left
+ anchors.verticalCenter: parent.verticalCenter
+ margin-right: 4
+ !text: tr('0 / 0')
+ text-auto-resize: true
+ color: #BDBDBD
Button
id: BestiaryTrackerButton
size: 90 20
anchors.bottom: parent.bottom
- anchors.left: CharmsBase.right
+ anchors.left: prev.right
margin-left: 5
!text: tr('Bestiary Tracker')
font: small-9px
diff --git a/modules/game_cyclopedia/images/character_icons/icon-character-generalstats-defence.png b/modules/game_cyclopedia/images/character_icons/icon-character-generalstats-defence.png
new file mode 100644
index 0000000000..2630e47952
Binary files /dev/null and b/modules/game_cyclopedia/images/character_icons/icon-character-generalstats-defence.png differ
diff --git a/modules/game_cyclopedia/images/character_icons/icon-character-generalstats-misc.png b/modules/game_cyclopedia/images/character_icons/icon-character-generalstats-misc.png
new file mode 100644
index 0000000000..04a0415604
Binary files /dev/null and b/modules/game_cyclopedia/images/character_icons/icon-character-generalstats-misc.png differ
diff --git a/modules/game_cyclopedia/images/charms/0.png b/modules/game_cyclopedia/images/charms/0.png
deleted file mode 100644
index e24c2c2b53..0000000000
Binary files a/modules/game_cyclopedia/images/charms/0.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/1.png b/modules/game_cyclopedia/images/charms/1.png
deleted file mode 100644
index 942be1e978..0000000000
Binary files a/modules/game_cyclopedia/images/charms/1.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/10.png b/modules/game_cyclopedia/images/charms/10.png
deleted file mode 100644
index 7f0e3f4896..0000000000
Binary files a/modules/game_cyclopedia/images/charms/10.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/11.png b/modules/game_cyclopedia/images/charms/11.png
deleted file mode 100644
index 1355863cc9..0000000000
Binary files a/modules/game_cyclopedia/images/charms/11.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/12.png b/modules/game_cyclopedia/images/charms/12.png
deleted file mode 100644
index 66b2dc50df..0000000000
Binary files a/modules/game_cyclopedia/images/charms/12.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/13.png b/modules/game_cyclopedia/images/charms/13.png
deleted file mode 100644
index aecba95ad1..0000000000
Binary files a/modules/game_cyclopedia/images/charms/13.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/14.png b/modules/game_cyclopedia/images/charms/14.png
deleted file mode 100644
index 2ab52539e0..0000000000
Binary files a/modules/game_cyclopedia/images/charms/14.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/15.png b/modules/game_cyclopedia/images/charms/15.png
deleted file mode 100644
index b08a03dd08..0000000000
Binary files a/modules/game_cyclopedia/images/charms/15.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/16.png b/modules/game_cyclopedia/images/charms/16.png
deleted file mode 100644
index a6ccdd6741..0000000000
Binary files a/modules/game_cyclopedia/images/charms/16.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/17.png b/modules/game_cyclopedia/images/charms/17.png
deleted file mode 100644
index f1dc200331..0000000000
Binary files a/modules/game_cyclopedia/images/charms/17.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/18.png b/modules/game_cyclopedia/images/charms/18.png
deleted file mode 100644
index 6f00013b2e..0000000000
Binary files a/modules/game_cyclopedia/images/charms/18.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/19.png b/modules/game_cyclopedia/images/charms/19.png
deleted file mode 100644
index 6f00013b2e..0000000000
Binary files a/modules/game_cyclopedia/images/charms/19.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/2.png b/modules/game_cyclopedia/images/charms/2.png
deleted file mode 100644
index 511c801f70..0000000000
Binary files a/modules/game_cyclopedia/images/charms/2.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/3.png b/modules/game_cyclopedia/images/charms/3.png
deleted file mode 100644
index 2d22ff3ee2..0000000000
Binary files a/modules/game_cyclopedia/images/charms/3.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/4.png b/modules/game_cyclopedia/images/charms/4.png
deleted file mode 100644
index e188f84266..0000000000
Binary files a/modules/game_cyclopedia/images/charms/4.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/5.png b/modules/game_cyclopedia/images/charms/5.png
deleted file mode 100644
index f054aac439..0000000000
Binary files a/modules/game_cyclopedia/images/charms/5.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/6.png b/modules/game_cyclopedia/images/charms/6.png
deleted file mode 100644
index 2b5d649be2..0000000000
Binary files a/modules/game_cyclopedia/images/charms/6.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/7.png b/modules/game_cyclopedia/images/charms/7.png
deleted file mode 100644
index 3be5f38fde..0000000000
Binary files a/modules/game_cyclopedia/images/charms/7.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/8.png b/modules/game_cyclopedia/images/charms/8.png
deleted file mode 100644
index d5f32bcece..0000000000
Binary files a/modules/game_cyclopedia/images/charms/8.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/9.png b/modules/game_cyclopedia/images/charms/9.png
deleted file mode 100644
index 9d57bcb226..0000000000
Binary files a/modules/game_cyclopedia/images/charms/9.png and /dev/null differ
diff --git a/modules/game_cyclopedia/images/charms/border/backdrop_charmgrade1.png b/modules/game_cyclopedia/images/charms/border/backdrop_charmgrade1.png
new file mode 100644
index 0000000000..0febc84a02
Binary files /dev/null and b/modules/game_cyclopedia/images/charms/border/backdrop_charmgrade1.png differ
diff --git a/modules/game_cyclopedia/images/charms/border/backdrop_charmgrade2.png b/modules/game_cyclopedia/images/charms/border/backdrop_charmgrade2.png
new file mode 100644
index 0000000000..07347c3b24
Binary files /dev/null and b/modules/game_cyclopedia/images/charms/border/backdrop_charmgrade2.png differ
diff --git a/modules/game_cyclopedia/images/charms/border/backdrop_charmgrade3.png b/modules/game_cyclopedia/images/charms/border/backdrop_charmgrade3.png
new file mode 100644
index 0000000000..5ede595022
Binary files /dev/null and b/modules/game_cyclopedia/images/charms/border/backdrop_charmgrade3.png differ
diff --git a/modules/game_cyclopedia/images/charms/border/border_charmgrades.png b/modules/game_cyclopedia/images/charms/border/border_charmgrades.png
new file mode 100644
index 0000000000..a7f48cd07f
Binary files /dev/null and b/modules/game_cyclopedia/images/charms/border/border_charmgrades.png differ
diff --git a/modules/game_cyclopedia/images/charms/icon-charms-major.png b/modules/game_cyclopedia/images/charms/icon-charms-major.png
new file mode 100644
index 0000000000..21570bd714
Binary files /dev/null and b/modules/game_cyclopedia/images/charms/icon-charms-major.png differ
diff --git a/modules/game_cyclopedia/images/charms/icon-charms-minor.png b/modules/game_cyclopedia/images/charms/icon-charms-minor.png
new file mode 100644
index 0000000000..28af33a13d
Binary files /dev/null and b/modules/game_cyclopedia/images/charms/icon-charms-minor.png differ
diff --git a/modules/game_cyclopedia/images/charms/monster-bonus-effects.png b/modules/game_cyclopedia/images/charms/monster-bonus-effects.png
new file mode 100644
index 0000000000..6fd8b49dd6
Binary files /dev/null and b/modules/game_cyclopedia/images/charms/monster-bonus-effects.png differ
diff --git a/modules/game_cyclopedia/images/minor-charm-echoes.png b/modules/game_cyclopedia/images/minor-charm-echoes.png
new file mode 100644
index 0000000000..a3c9e1918e
Binary files /dev/null and b/modules/game_cyclopedia/images/minor-charm-echoes.png differ
diff --git a/modules/game_cyclopedia/images/monster-icon-bonuspoints.png b/modules/game_cyclopedia/images/monster-icon-bonuspoints.png
new file mode 100644
index 0000000000..1b1997f372
Binary files /dev/null and b/modules/game_cyclopedia/images/monster-icon-bonuspoints.png differ
diff --git a/modules/game_cyclopedia/tab/bestiary/bestiary.lua b/modules/game_cyclopedia/tab/bestiary/bestiary.lua
index 971181ec26..812a90fbbc 100644
--- a/modules/game_cyclopedia/tab/bestiary/bestiary.lua
+++ b/modules/game_cyclopedia/tab/bestiary/bestiary.lua
@@ -34,6 +34,9 @@ function showBestiary()
controllerCyclopedia.ui.CharmsBase:setVisible(true)
controllerCyclopedia.ui.GoldBase:setVisible(true)
controllerCyclopedia.ui.BestiaryTrackerButton:setVisible(true)
+ if g_game.getClientVersion() >= 1410 then
+ controllerCyclopedia.ui.CharmsBase1410:hide()
+ end
g_game.requestBestiary()
end
diff --git a/modules/game_cyclopedia/tab/boss_slots/boss_slots.lua b/modules/game_cyclopedia/tab/boss_slots/boss_slots.lua
index 11976dcb2b..459ddb0056 100644
--- a/modules/game_cyclopedia/tab/boss_slots/boss_slots.lua
+++ b/modules/game_cyclopedia/tab/boss_slots/boss_slots.lua
@@ -8,6 +8,9 @@ function showBossSlot()
controllerCyclopedia.ui.CharmsBase:setVisible(false)
controllerCyclopedia.ui.GoldBase:setVisible(true)
controllerCyclopedia.ui.BestiaryTrackerButton:setVisible(false)
+ if g_game.getClientVersion() >= 1410 then
+ controllerCyclopedia.ui.CharmsBase1410:hide()
+ end
Cyclopedia.BossSlots.UnlockBosses = {}
end
diff --git a/modules/game_cyclopedia/tab/bosstiary/bosstiary.lua b/modules/game_cyclopedia/tab/bosstiary/bosstiary.lua
index 0de6da0826..7700f5cc6b 100644
--- a/modules/game_cyclopedia/tab/bosstiary/bosstiary.lua
+++ b/modules/game_cyclopedia/tab/bosstiary/bosstiary.lua
@@ -23,6 +23,9 @@ function showBosstiary()
controllerCyclopedia.ui.CharmsBase:setVisible(false)
controllerCyclopedia.ui.GoldBase:setVisible(false)
controllerCyclopedia.ui.BestiaryTrackerButton:setVisible(false)
+ if g_game.getClientVersion() >= 1410 then
+ controllerCyclopedia.ui.CharmsBase1410:setVisible(false)
+ end
end
Cyclopedia.Bosstiary = {}
diff --git a/modules/game_cyclopedia/tab/character/character.lua b/modules/game_cyclopedia/tab/character/character.lua
index 797f50d781..d0d3effcd6 100644
--- a/modules/game_cyclopedia/tab/character/character.lua
+++ b/modules/game_cyclopedia/tab/character/character.lua
@@ -100,6 +100,9 @@ function showCharacter()
controllerCyclopedia.ui.CharmsBase:setVisible(true)
controllerCyclopedia.ui.GoldBase:setVisible(true)
controllerCyclopedia.ui.BestiaryTrackerButton:setVisible(false)
+ if g_game.getClientVersion() >= 1410 then
+ controllerCyclopedia.ui.CharmsBase1410:setVisible(true)
+ end
end
Cyclopedia.Character = {}
@@ -966,18 +969,41 @@ function Cyclopedia.configureCharacterCategories()
{
text = "General Stats",
icon = "/game_cyclopedia/images/character_icons/icon_generalstats",
- subCategories = {
- {
- text = "Character Stats",
- icon = "/game_cyclopedia/images/character_icons/icon-character-generalstats-overview",
- open = "CharacterStats"
- },
- {
- text = "Combat Stats",
- icon = "/game_cyclopedia/images/character_icons/icon-character-generalstats-combatstats",
- open = "CombatStats"
+ subCategories = function()
+ local categories = {
+ {
+ text = "Character Stats",
+ icon = "/game_cyclopedia/images/character_icons/icon-character-generalstats-overview",
+ open = "CharacterStats"
+ }
}
- }
+
+ if g_game.getClientVersion() < 1410 then
+ table.insert(categories, {
+ text = "Combat Stats",
+ icon = "/game_cyclopedia/images/character_icons/icon-character-generalstats-combatstats",
+ open = "CombatStats"
+ })
+ else
+ table.insert(categories, {
+ text = "Offence Stats",
+ icon = "/game_cyclopedia/images/character_icons/icon-character-generalstats-combatstats",
+ open = "OffenceStats"
+ })
+ table.insert(categories, {
+ text = "Deffence Stats",
+ icon = "/game_cyclopedia/images/character_icons/icon-character-generalstats-defence",
+ open = "DeffenceStats"
+ })
+ table.insert(categories, {
+ text = "Misc. Stats",
+ icon = "/game_cyclopedia/images/character_icons/icon-character-generalstats-misc",
+ open = "MiscStats"
+ })
+ end
+
+ return categories
+ end
},
{
text = "Battle Results",
@@ -1033,11 +1059,16 @@ function Cyclopedia.configureCharacterCategories()
end
if button.subCategories ~= nil then
- widget.subCategories = button.subCategories
- widget.subCategoriesSize = #button.subCategories
+ local subCats = button.subCategories
+ if type(subCats) == "function" then
+ subCats = subCats()
+ end
+
+ widget.subCategories = subCats
+ widget.subCategoriesSize = #subCats
widget.Button.Arrow:setVisible(true)
- for subId, subButton in ipairs(button.subCategories) do
+ for subId, subButton in ipairs(subCats) do
local subWidget = g_ui.createWidget("CharacterCategoryItem", widget)
subWidget:setId(subId)
subWidget.Button.Icon:setIcon(subButton.icon)
@@ -1060,6 +1091,12 @@ function Cyclopedia.configureCharacterCategories()
g_game.requestCharacterInfo(0, CyclopediaCharacterInfoTypes.Badges)
elseif subWidget.open == "CombatStats" then
g_game.requestCharacterInfo(0, CyclopediaCharacterInfoTypes.CombatStats)
+ elseif subWidget.open == "OffenceStats" then
+ g_game.requestCharacterInfo(0, CyclopediaCharacterInfoTypes.Offencestats)
+ elseif subWidget.open == "DeffenceStats" then
+ g_game.requestCharacterInfo(0, CyclopediaCharacterInfoTypes.Defencestats)
+ elseif subWidget.open == "MiscStats" then
+ g_game.requestCharacterInfo(0, CyclopediaCharacterInfoTypes.Miscstats)
elseif subWidget.open == "RecentDeaths" then
g_game.requestCharacterInfo(0, CyclopediaCharacterInfoTypes.RecentDeaths, 23, 1)
elseif subWidget.open == "RecentKills" then
@@ -1084,11 +1121,11 @@ function Cyclopedia.configureCharacterCategories()
if id == 1 then
widget:addAnchor(AnchorTop, "parent", AnchorTop)
widget:addAnchor(AnchorHorizontalCenter, "parent", AnchorHorizontalCenter)
- widget:setMarginTop(10)
+ widget:setMarginTop(5)
else
widget:addAnchor(AnchorTop, "prev", AnchorBottom)
widget:addAnchor(AnchorHorizontalCenter, "parent", AnchorHorizontalCenter)
- widget:setMarginTop(10)
+ widget:setMarginTop(5)
end
function widget.Button.onClick(this)
@@ -1262,3 +1299,346 @@ function Cyclopedia.onParseCyclopediaStoreSummary(xpBoostTime, dailyRewardXpBoos
itemWidget:fill('parent')
end
end
+
+local function getWeaponSkillName(skillType)
+ local skillNames = {
+ [0] = "Fist Fighting",
+ [1] = "Club Fighting",
+ [2] = "Sword Fighting",
+ [3] = "Axe Fighting",
+ [4] = "Distance Fighting",
+ [5] = "Shielding",
+ [6] = "Fishing",
+ [7] = "Magic Level",
+ [8] = "Critical Hits",
+ [9] = "Life Leech",
+ [10] = "Mana Leech"
+ }
+
+ return skillNames[skillType] or "Fighting Skill"
+ end
+ function Cyclopedia.onCyclopediaCharacterOffenceStats(data)
+ UI.OffenceStats.rightPanel:destroyChildren()
+ UI.OffenceStats.leftPanel:destroyChildren()
+
+ local attackValue = data.weaponAttack + data.weaponFlatModifier + data.weaponDamage + data.weaponSkillLevel
+ local stats = {
+ {name = "Flat Damage and healing", value = data.flatDamage or 0, icon = false, percent = false},
+ {name = "Attack Value", value = attackValue, icon = true, weaponElement = data.weaponElement},
+ {name = "From Base Attack", value = data.weaponAttack or 0, align = "center", icon = false},
+ {name = "From Equipment", value = data.weaponFlatModifier or 0, align = "center", icon = false},
+
+ {name = getWeaponSkillName(data.weaponSkillType), value = data.weaponSkillLevel or 0, align = "center", icon = false},
+ {name = "From Combat Tactics", value = data.weaponDamage or 0, align = "center", icon = false},
+
+ {name = "Life Leech", value = data.lifeLeech or 0, icon = false, percent = true},
+ {name = "From Base", value = data.lifeLeechBase or 0, align = "center", percent = true, icon = false},
+ {name = "From Equipment", value = data.lifeLeechImbuement or 0, align = "center", percent = true, icon = false},
+ {name = "From Wheel", value = data.lifeLeechWheel or 0, align = "center", percent = true, icon = false},
+
+ {name = "Mana Leech", value = data.manaLeech or 0, icon = false, percent = true},
+ {name = "From Base", value = data.manaLeechBase or 0, align = "center", percent = true, icon = false},
+ {name = "From Equipment", value = data.manaLeechImbuement or 0, align = "center", percent = true, icon = false},
+ {name = "From Wheel", value = data.manaLeechWheel or 0, align = "center", percent = true, icon = false},
+
+ {name = "Onslaught", value = data.onslaught or 0, icon = false, percent = true},
+ {name = "From Base", value = data.onslaughtBase or 0, align = "center", percent = true, icon = false},
+ {name = "From Amplification", value = data.onslaughtBonus or 0, align = "center", percent = true, icon = false},
+
+ {name = "Critical Hit", parent = "right", value = "", icon = false},
+ {name = " Chance", parent = "right", value = data.critChance or 0, percent = true, icon = false},
+ {name = " Extra Damage", parent = "right", value = data.critDamage or 0, percent = true, icon = false},
+ {name = "From Base", parent = "right", value = data.critDamageBase or 0, align = "center", percent = true, icon = false},
+ {name = "From Equipment", parent = "right", value = data.critDamageImbuement or 0, align = "center", percent = true, icon = false},
+ {name = "From Wheel", parent = "right", value = data.critDamageWheel or 0, align = "center", percent = true, icon = false}
+ }
+
+ if data.perfectShotDamage then
+ for i = 1, 5 do
+ if data.perfectShotDamage[i] and data.perfectShotDamage[i] > 0 then
+ table.insert(stats, {
+ name = "Perfect Shot Damage Bonus",
+ parent = "right",
+ value = "",
+ icon = false
+ })
+ table.insert(stats, {
+ name = " +" .. data.perfectShotDamage[i] .. " from range " .. i,
+ parent = "right",
+ value = "",
+ align = "center",
+ icon = false
+ })
+ break
+ end
+ end
+ end
+
+ local function renderStat(stat)
+ local parent = stat.parent == "right" and UI.OffenceStats.rightPanel or UI.OffenceStats.leftPanel
+
+ if stat.align == "center" then
+ local widget = g_ui.createWidget("Label", parent)
+ local valueText = stat.value
+ if stat.percent then
+ local percentValue = math.floor(stat.value * 10000) / 100
+ local sign = percentValue > 0 and "+ " or ""
+ valueText = sign .. percentValue .. "%"
+ end
+ widget:setText(" " .. valueText .. " " .. stat.name)
+ widget:setMarginLeft(80)
+ return widget
+ else
+ local widget = g_ui.createWidget("CharacterSkillBase", parent)
+ local nameLabel = g_ui.createWidget("SkillNameLabel", widget)
+ nameLabel:setText(stat.name .. ":")
+ local valueLabel = g_ui.createWidget("SkillValueLabel", widget)
+ if stat.percent then
+ local percentValue = math.floor(stat.value * 10000) / 100
+ local sign = percentValue > 0 and "+ " or ""
+ valueLabel:setText(sign .. percentValue .. "%")
+ else
+ valueLabel:setText(tostring(stat.value))
+ end
+ if stat.icon then
+ valueLabel:setMarginRight(12)
+ local icon = g_ui.createWidget("SkillCharacterIcon", widget)
+ icon:setMarginTop(2)
+ icon:addAnchor(AnchorRight, "parent", AnchorRight)
+ local element = clientCombat[stat.weaponElement]
+ if element then
+ icon:setImageSource(element.path)
+ icon:setImageSize({
+ width = 9,
+ height = 9
+ })
+ end
+ end
+
+ return widget
+ end
+ end
+
+ for _, stat in ipairs(stats) do
+ if stat.align ~= "center" and stat.value == 0 and stat.value ~= "" then
+ -- Skip
+ else
+ renderStat(stat)
+ end
+ end
+ end
+ function Cyclopedia.onCyclopediaCharacterDefenceStats(data)
+ UI.DeffenceStats.rightPanel:destroyChildren()
+ UI.DeffenceStats.leftPanel:destroyChildren()
+
+ local stats = {
+ {name = "Defence Value", value = data.defense or 0, icon = false, percent = false},
+ {name = "From Equipment", value = data.defenseEquipment or 0, align = "center", icon = false},
+ {name = "From Wheel", value = data.defenseWheel or 0, align = "center", icon = false},
+ {name = getWeaponSkillName(data.defenseSkillType), value = data.shieldingSkill or 0, align = "center", icon = false},
+
+ {name = "Armor Value", value = data.armor or 0, icon = false, percent = false},
+
+ {name = "Mitigation", value = data.mitigation or 0, icon = false, percent = true},
+ {name = "From Shielding", value = data.mitigationShield or 0, align = "center", percent = true, icon = false},
+ {name = "From Combat Tactics", value = data.mitigationCombatTactics or 0, align = "center", percent = true, icon = false},
+ {name = "From Base", value = data.mitigationBase or 0, align = "center", percent = true, icon = false},
+ {name = "From Equipment", value = data.mitigationEquipment or 0, align = "center", percent = true, icon = false},
+ {name = "From Wheel", value = data.mitigationWheel or 0, align = "center", percent = true, icon = false},
+
+ {name = "Dodge", value = data.dodgeTotal or 0, icon = false, percent = true},
+ {name = "From Base", value = data.dodgeBase or 0, align = "center", percent = true, icon = false},
+ {name = "From Amplification", value = data.dodgeBonus or 0, align = "center", percent = true, icon = false},
+ {name = "From Wheel", value = data.dodgeWheel or 0, align = "center", percent = true, icon = false},
+
+ {name = "Magic Shield Capacity", value = data.magicShieldCapacity or 0, icon = false, percent = false},
+ {name = "Flat", value = data.magicShieldCapacityFlat or 0, align = "center", icon = false},
+ {name = "Percent", value = data.magicShieldCapacityPercent or 0, align = "center", percent = true, icon = false},
+
+ {name = "Reflect Physical", value = data.reflectPhysical or 0, icon = false, percent = false},
+
+ {name = "Resistances", parent = "right", value = "", icon = false}
+ }
+
+ local resistanceMap = {}
+ if data.resistances then
+ for _, resistance in ipairs(data.resistances) do
+ resistanceMap[resistance.element] = resistance.value
+ end
+ end
+
+ for elementId, elementInfo in pairs(Cyclopedia.clientCombat) do
+ local value = resistanceMap[elementId] or 0
+ local percentValue = value *100
+ local color = "#FFFFFF"
+
+ if percentValue > 0 then
+ color = "#44AD25"
+ elseif percentValue < 0 then
+ color = "#FF9900"
+ end
+
+ local sign = percentValue >= 0 and "+" or ""
+ table.insert(stats, {
+ name = " " .. elementInfo.id,
+ parent = "right",
+ value = sign .. string.format("%.2f", tonumber(percentValue)) .. "%",
+ percent = false,
+ element = elementId,
+ icon = true,
+ color = color
+ })
+ end
+
+ local function renderStat(stat)
+ local parent = stat.parent == "right" and UI.DeffenceStats.rightPanel or UI.DeffenceStats.leftPanel
+
+ if stat.align == "center" then
+ local widget = g_ui.createWidget("Label", parent)
+ local valueText = stat.value
+ if stat.percent then
+ local percentValue = math.floor(stat.value * 10000) / 100
+ local sign = percentValue > 0 and "+ " or ""
+ valueText = sign .. percentValue .. "%"
+ end
+ widget:setText(" " .. valueText .. " " .. stat.name)
+ widget:setMarginLeft(80)
+ return widget
+ else
+ local widget = g_ui.createWidget("CharacterSkillBase", parent)
+ local nameLabel = g_ui.createWidget("SkillNameLabel", widget)
+ nameLabel:setText(stat.name .. ":")
+ local valueLabel = g_ui.createWidget("SkillValueLabel", widget)
+ if stat.percent then
+ local percentValue = math.floor(stat.value * 10000) / 100
+ local sign = percentValue > 0 and "+ " or ""
+ valueLabel:setText(sign .. percentValue .. "%")
+ else
+ valueLabel:setText(tostring(stat.value))
+ end
+
+ if stat.color then
+ valueLabel:setColor(stat.color)
+ end
+
+ if stat.icon then
+ valueLabel:setMarginRight(12)
+ local icon = g_ui.createWidget("SkillCharacterIcon", widget)
+ icon:setMarginTop(2)
+ icon:addAnchor(AnchorRight, "parent", AnchorRight)
+ local element = Cyclopedia.clientCombat[stat.element]
+ if element then
+ icon:setImageSource(element.path)
+ icon:setImageSize({
+ width = 9,
+ height = 9
+ })
+ end
+ end
+
+ return widget
+ end
+ end
+
+ for _, stat in ipairs(stats) do
+ if stat.align ~= "center" and stat.value == 0 and stat.value ~= "" then
+ -- Skip
+ else
+ renderStat(stat)
+ end
+ end
+ end
+
+ function Cyclopedia.onCyclopediaCharacterMiscStats(data)
+ UI.MiscStats.leftPanel:destroyChildren()
+ UI.MiscStats.rightPanel:destroyChildren()
+
+ local stats = {
+ {name = "Momentum", value = data.momentumTotal or 0, icon = false, percent = true},
+ {name = "From Equipment", value = data.momentumBase or 0, align = "center", percent = true, icon = false},
+ {name = "From Amplification", value = data.momentumBonus or 0, align = "center", percent = true, icon = false},
+ {name = "From Wheel", value = data.momentumWheel or 0, align = "center", percent = true, icon = false},
+
+ {name = "Transcendence", value = data.dodgeTotal or 0, icon = false, percent = true},
+ {name = "From Base", value = data.dodgeBase or 0, align = "center", percent = true, icon = false},
+ {name = "From Amplification", value = data.dodgeBonus or 0, align = "center", percent = true, icon = false},
+ {name = "From Event Bonus", value = data.dodgeWheel or 0, align = "center", percent = true, icon = false},
+
+ {name = "Damage Reflection", value = data.damageReflectionTotal or 0, icon = false, percent = true},
+ {name = "From Base", value = data.damageReflectionBase or 0, align = "center", percent = true, icon = false},
+ {name = "From Bonus", value = data.damageReflectionBonus or 0, align = "center", percent = true, icon = false},
+
+ {name = "Blessings", value = (data.haveBlesses or 0) .. "/" .. (data.totalBlesses or 0), icon = false, percent = false},
+
+ }
+
+ if data.concoctions and #data.concoctions > 0 then
+ for _, concoction in ipairs(data.concoctions) do
+ table.insert(stats, {
+ name = " " .. concoction.name,
+ parent = "right",
+ value = concoction.value,
+ percent = true,
+ icon = false
+ })
+ end
+ end
+
+ local function renderStat(stat)
+ local parent = stat.parent == "right" and UI.MiscStats.rightPanel or UI.MiscStats.leftPanel
+
+ if stat.align == "center" then
+ local widget = g_ui.createWidget("Label", parent)
+ local valueText = stat.value
+ if stat.percent then
+ local percentValue = math.floor(stat.value * 10000) / 100
+ local sign = percentValue > 0 and "+ " or ""
+ valueText = sign .. percentValue .. "%"
+ end
+ widget:setText(" " .. valueText .. " " .. stat.name)
+ widget:setMarginLeft(60)
+ return widget
+ else
+ local widget = g_ui.createWidget("CharacterSkillBase", parent)
+ local nameLabel = g_ui.createWidget("SkillNameLabel", widget)
+ nameLabel:setText(stat.name .. ":")
+ local valueLabel = g_ui.createWidget("SkillValueLabel", widget)
+ if stat.percent then
+ local percentValue = math.floor(stat.value * 10000) / 100
+ local sign = percentValue > 0 and "+ " or ""
+
+
+ valueLabel:setText(sign .. percentValue .. "%")
+ else
+ valueLabel:setText(tostring(stat.value))
+ end
+
+ if stat.icon then
+ valueLabel:setMarginRight(12)
+ local icon = g_ui.createWidget("SkillCharacterIcon", widget)
+ icon:setMarginTop(2)
+ icon:addAnchor(AnchorRight, "parent", AnchorRight)
+ if stat.element then
+ local element = Cyclopedia.clientCombat[stat.element]
+ if element then
+ icon:setImageSource(element.path)
+ icon:setImageSize({
+ width = 9,
+ height = 9
+ })
+ end
+ end
+ end
+
+ return widget
+ end
+ end
+
+ for _, stat in ipairs(stats) do
+ if stat.align ~= "center" and stat.value == 0 and stat.value ~= "" then
+ -- Skip
+ else
+ renderStat(stat)
+ end
+ end
+ end
\ No newline at end of file
diff --git a/modules/game_cyclopedia/tab/character/character.otui b/modules/game_cyclopedia/tab/character/character.otui
index 549bdad63e..ae20c409dd 100644
--- a/modules/game_cyclopedia/tab/character/character.otui
+++ b/modules/game_cyclopedia/tab/character/character.otui
@@ -1791,4 +1791,126 @@ UIWidget
margin: 1
step: 80
- pixel-scroll: true
\ No newline at end of file
+ pixel-scroll: true
+
+ BorderBox
+ id: OffenceStats
+ anchors.top: CharacterBase.top
+ anchors.left: CharacterBase.right
+ anchors.bottom: OptionsBase.bottom
+ anchors.right: parent.right
+ margin-left: 10
+ visible: false
+
+ VerticalSeparator
+ id: separator
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ margin-top: 5
+ margin-bottom: 5
+
+
+ Panel
+ id: leftPanel
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: separator.right
+ margin-top: 5
+ margin-left: 5
+ height: 350
+ layout:
+ type: verticalBox
+ spacing: 2
+
+ Panel
+ id: rightPanel
+ anchors.top: prev.top
+ anchors.left: separator.left
+ anchors.right: parent.right
+ margin-top: 1
+ height: 350
+ layout:
+ type: verticalBox
+ spacing: 2
+
+
+ BorderBox
+ id: DeffenceStats
+ anchors.top: CharacterBase.top
+ anchors.left: CharacterBase.right
+ anchors.bottom: OptionsBase.bottom
+ anchors.right: parent.right
+ margin-left: 10
+ visible: false
+ VerticalSeparator
+ id: separator
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ margin-top: 5
+ margin-bottom: 5
+
+
+ Panel
+ id: leftPanel
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: separator.right
+ margin-top: 5
+ margin-left: 5
+ height: 350
+ layout:
+ type: verticalBox
+ spacing: 2
+
+ Panel
+ id: rightPanel
+ anchors.top: prev.top
+ anchors.left: separator.left
+ anchors.right: parent.right
+ margin-top: 1
+ height: 350
+ layout:
+ type: verticalBox
+ spacing: 2
+
+ BorderBox
+ id: MiscStats
+ anchors.top: CharacterBase.top
+ anchors.left: CharacterBase.right
+ anchors.bottom: OptionsBase.bottom
+ anchors.right: parent.right
+ margin-left: 10
+ visible: false
+ VerticalSeparator
+ id: separator
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ margin-top: 5
+ margin-bottom: 5
+
+
+ Panel
+ id: leftPanel
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: separator.right
+ margin-top: 5
+ margin-left: 5
+ height: 350
+ layout:
+ type: verticalBox
+ spacing: 2
+
+ Panel
+ id: rightPanel
+ anchors.top: prev.top
+ anchors.left: separator.left
+ anchors.right: parent.right
+ margin-top: 1
+ height: 350
+ layout:
+ type: verticalBox
+ spacing: 2
diff --git a/modules/game_cyclopedia/tab/charms/charms.lua b/modules/game_cyclopedia/tab/charms/charms.lua
index 9e34e1c671..681fef0b43 100644
--- a/modules/game_cyclopedia/tab/charms/charms.lua
+++ b/modules/game_cyclopedia/tab/charms/charms.lua
@@ -1,203 +1,614 @@
local UI = nil
+local TypeCharmRadioGroup = nil
+Cyclopedia.Charms = {}
+local charmCategory_t = {
+ CHARM_ALL = 0,
+ CHARM_MAJOR = 1,
+ CHARM_MINOR = 2
+};
+
+local charm_t = {
+ CHARM_UNDEFINED = 0,
+ CHARM_OFFENSIVE = 1,
+ CHARM_DEFENSIVE = 2,
+ CHARM_PASSIVE = 3
+};
+local charmRune_t = {
+ CHARM_WOUND = 0,
+ CHARM_ENFLAME = 1,
+ CHARM_POISON = 2,
+ CHARM_FREEZE = 3,
+ CHARM_ZAP = 4,
+ CHARM_CURSE = 5,
+ CHARM_CRIPPLE = 6,
+ CHARM_PARRY = 7,
+ CHARM_DODGE = 8,
+ CHARM_ADRENALINE = 9,
+ CHARM_NUMB = 10,
+ CHARM_CLEANSE = 11,
+ CHARM_BLESS = 12,
+ CHARM_SCAVENGE = 13,
+ CHARM_GUT = 14,
+ CHARM_LOW = 15,
+ CHARM_DIVINE = 16,
+ CHARM_VAMP = 17,
+ CHARM_VOID = 18,
+ CHARM_SAVAGE = 19,
+ CHARM_FATAL = 20,
+ CHARM_VOIDINVERSION = 21,
+ CHARM_CARNAGE = 22,
+ CHARM_OVERPOWER = 23,
+ CHARM_OVERFLUX = 24
+}
+
+local charms = {
+ [charmRune_t.CHARM_WOUND] = {
+ name = "Wound",
+ description = "Triggers on a creature with a chance to deal 5% of its initial HP as physical damage.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ percent = 5,
+ chance = {5, 10, 11},
+ points = {240, 360, 1200}
+ },
+ [charmRune_t.CHARM_ENFLAME] = {
+ name = "Enflame",
+ description = "Triggers on a creature with a chance to deal 5% of its initial HP as fire damage.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ percent = 5,
+ chance = {5, 10, 11},
+ points = {400, 600, 2000}
+ },
+ [charmRune_t.CHARM_POISON] = {
+ name = "Poison",
+ description = "Triggers on a creature with a chance to deal 5% of its initial HP as earth damage.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ percent = 5,
+ chance = {5, 10, 11},
+ points = {240, 360, 1200}
+ },
+ [charmRune_t.CHARM_FREEZE] = {
+ name = "Freeze",
+ description = "Triggers on a creature with a chance to deal 5% of its initial HP as ice damage.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ percent = 5,
+ chance = {5, 10, 11},
+ points = {320, 480, 1600}
+ },
+ [charmRune_t.CHARM_ZAP] = {
+ name = "Zap",
+ description = "Triggers on a creature with a chance to deal 5% of its initial HP as energy damage.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ percent = 5,
+ chance = {5, 10, 11},
+ points = {320, 480, 1600}
+ },
+ [charmRune_t.CHARM_CURSE] = {
+ name = "Curse",
+ description = "Triggers on a creature with a chance to deal 5% of its initial HP as death damage.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ percent = 5,
+ chance = {5, 10, 11},
+ points = {360, 540, 1800}
+ },
+ [charmRune_t.CHARM_CRIPPLE] = {
+ name = "Cripple",
+ description = "Cripples the creature and paralyzes it for 10 seconds.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ chance = {6, 9, 12},
+ messageCancel = "You crippled a monster. (cripple charm)",
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_PARRY] = {
+ name = "Parry",
+ description = "Reflects incoming damage back to the aggressor.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_DEFENSIVE,
+ chance = {5, 10, 11},
+ messageCancel = "You parried an attack. (parry charm)",
+ points = {400, 600, 2000}
+ },
+ [charmRune_t.CHARM_DODGE] = {
+ name = "Dodge",
+ description = "Dodges an attack with a chance, avoiding all damage.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_DEFENSIVE,
+ chance = {5, 10, 11},
+ messageCancel = "You dodged an attack. (dodge charm)",
+ points = {240, 360, 1200}
+ },
+ [charmRune_t.CHARM_ADRENALINE] = {
+ name = "Adrenaline Burst",
+ description = "Boosts movement speed for 10 seconds after being hit.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_DEFENSIVE,
+ chance = {6, 9, 12},
+ messageCancel = "Your movements where bursted. (adrenaline burst charm)",
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_NUMB] = {
+ name = "Numb",
+ description = "Numbs the creature and paralyzes it for 10 seconds.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_DEFENSIVE,
+ chance = {6, 9, 12},
+ messageCancel = "You numbed a monster. (numb charm)",
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_CLEANSE] = {
+ name = "Cleanse",
+ description = "Removes a negative status effect and grants temporary immunity.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_DEFENSIVE,
+ chance = {6, 9, 12},
+ messageCancel = "You purified an attack. (cleanse charm)",
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_BLESS] = {
+ name = "Bless",
+ description = "Reduces skill and XP loss by 10% when killed by the chosen creature.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_PASSIVE,
+ percent = 10,
+ chance = {6, 9, 12},
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_SCAVENGE] = {
+ name = "Scavenge",
+ description = "Enhances chances to successfully skin or dust a creature.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_PASSIVE,
+ chance = {60, 90, 120},
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_GUT] = {
+ name = "Gut",
+ description = "Increases creature product yields by 20%.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_PASSIVE,
+ chance = {6, 9, 12},
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_LOW] = {
+ name = "Low Blow",
+ description = "Adds 8% critical hit chance to attacks with critical hit weapons.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_PASSIVE,
+ chance = {4, 8, 9},
+ points = {800, 1200, 4000}
+ },
+ [charmRune_t.CHARM_DIVINE] = {
+ name = "Divine Wrath",
+ description = "Triggers on a creature and deals 5% of its initial HP as holy damage.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ percent = 5,
+ chance = {5, 10, 11},
+ points = {600, 900, 3000}
+ },
+ [charmRune_t.CHARM_VAMP] = {
+ name = "Vampiric Embrace",
+ description = "Adds 4% life leech to attacks if using life-leeching equipment.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_PASSIVE,
+ chance = {1.6, 2.4, 3.2},
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_VOID] = {
+ name = "Void's Call",
+ description = "Adds 2% mana leech to attacks if using mana-leeching equipment.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_PASSIVE,
+ chance = {0.8, 1.2, 1.6},
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_SAVAGE] = {
+ name = "Savage Blow",
+ description = "Adds extra critical damage to attacks with critical hit weapons.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_PASSIVE,
+ chance = {20, 40, 44},
+ points = {800, 1200, 4000}
+ },
+ [charmRune_t.CHARM_FATAL] = {
+ name = "Fatal Hold",
+ description = "Prevents creatures from fleeing due to low health for 30 seconds.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_PASSIVE,
+ chance = {30, 45, 60},
+ messageCancel = "Your enemy is not able to flee now for 30 seconds. (fatal hold charm)",
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_VOIDINVERSION] = {
+ name = "Void Inversion",
+ description = "Chance to gain mana instead of losing it when taking Mana Drain damage.",
+ category = charmCategory_t.CHARM_MINOR,
+ type = charm_t.CHARM_PASSIVE,
+ chance = {20, 30, 40},
+ points = {100, 150, 225}
+ },
+ [charmRune_t.CHARM_CARNAGE] = {
+ name = "Carnage",
+ description = "Killing a monster deals physical damage to others nearby.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ percent = 15,
+ chance = {10, 20, 22},
+ points = {600, 900, 3000}
+ },
+ [charmRune_t.CHARM_OVERPOWER] = {
+ name = "Overpower",
+ description = "Deals physical damage based on your maximum health.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ percent = 5,
+ chance = {5, 10, 11},
+ points = {600, 900, 3000}
+ },
+ [charmRune_t.CHARM_OVERFLUX] = {
+ name = "Overflux",
+ description = "Deals physical damage based on your maximum mana.",
+ category = charmCategory_t.CHARM_MAJOR,
+ type = charm_t.CHARM_OFFENSIVE,
+ percent = 2.5,
+ chance = {5, 10, 11},
+ points = {600, 900, 3000}
+ }
+}
+local isModernUI = false
function showCharms()
- UI = g_ui.loadUI("charms", contentContainer)
+ isModernUI = g_game.getClientVersion() >= 1410
+ local UIUX = isModernUI and "charms1410" or "charms"
+ UI = g_ui.loadUI(UIUX, contentContainer)
UI:show()
g_game.requestBestiary()
controllerCyclopedia.ui.CharmsBase:setVisible(true)
controllerCyclopedia.ui.GoldBase:setVisible(true)
controllerCyclopedia.ui.BestiaryTrackerButton:setVisible(false)
+ if isModernUI then
+ controllerCyclopedia.ui.CharmsBase1410:setVisible(true)
+ TypeCharmRadioGroup = UIRadioGroup.create()
+ TypeCharmRadioGroup:addWidget(UI.mainPanelCharmsType.typeCharmPanel.MajorCharms)
+ TypeCharmRadioGroup:addWidget(UI.mainPanelCharmsType.typeCharmPanel.MinorCharms)
+ TypeCharmRadioGroup:selectWidget(TypeCharmRadioGroup:getFirstWidget())
+ connect(TypeCharmRadioGroup, {
+ onSelectionChange = onTypeCharmRadioGroup
+ })
+ end
end
-Cyclopedia.Charms = {}
-
-local CHARMS = {
- { ID = 9, IMAGE = "/game_cyclopedia/images/charms/9", TYPE = 1 },
- { ID = 11, IMAGE = "/game_cyclopedia/images/charms/11", TYPE = 2 },
- { ID = 10, IMAGE = "/game_cyclopedia/images/charms/10", TYPE = 3 },
- { ID = 6, IMAGE = "/game_cyclopedia/images/charms/6", TYPE = 2 },
- { ID = 8, IMAGE = "/game_cyclopedia/images/charms/8", TYPE = 2 },
- { ID = 7, IMAGE = "/game_cyclopedia/images/charms/7", TYPE = 3 },
- { ID = 5, IMAGE = "/game_cyclopedia/images/charms/5", TYPE = 4 },
- { ID = 1, IMAGE = "/game_cyclopedia/images/charms/1", TYPE = 4 },
- { ID = 3, IMAGE = "/game_cyclopedia/images/charms/3", TYPE = 4 },
- { ID = 2, IMAGE = "/game_cyclopedia/images/charms/2", TYPE = 4 },
- { ID = 0, IMAGE = "/game_cyclopedia/images/charms/0", TYPE = 4 },
- { ID = 4, IMAGE = "/game_cyclopedia/images/charms/4", TYPE = 4 },
- { ID = 16, IMAGE = "/game_cyclopedia/images/charms/16", TYPE = 5 },
- { ID = 15, IMAGE = "/game_cyclopedia/images/charms/15", TYPE = 6 },
- { ID = 17, IMAGE = "/game_cyclopedia/images/charms/17", TYPE = 6 },
- { ID = 13, IMAGE = "/game_cyclopedia/images/charms/13", TYPE = 6 },
- { ID = 12, IMAGE = "/game_cyclopedia/images/charms/12", TYPE = 6 },
- { ID = 14, IMAGE = "/game_cyclopedia/images/charms/14", TYPE = 6 },
- { ID = 18, IMAGE = "/game_cyclopedia/images/charms/18", TYPE = 6 },
-}
-
-function Cyclopedia.UpdateCharmsBalance(Value)
- for i, child in pairs(UI.Bottombase:getChildren()) do
- if child.CharmsBase then
- child.CharmsBase.Value:setText(Value)
- end
+function onTerminateCharm()
+ if TypeCharmRadioGroup then
+ disconnect(TypeCharmRadioGroup, {
+ onSelectionChange = onTypeCharmRadioGroup
+ })
+ TypeCharmRadioGroup:destroy()
+ TypeCharmRadioGroup = nil
end
end
function Cyclopedia.CreateCharmItem(data)
- local widget = g_ui.createWidget("CharmItem", UI.CharmList)
+ local CharmList = isModernUI and UI.mainPanelCharmsType.panelCharmList.CharmList or UI.CharmList
+ local widget = g_ui.createWidget("CharmItem", CharmList)
local value = widget.PriceBase.Value
widget:setId(data.id)
+ widget.charmBase.image:setImageSource("/game_cyclopedia/images/charms/monster-bonus-effects")
if data.id ~= nil then
- widget.charmBase.image:setImageSource("/game_cyclopedia/images/charms/" .. data.id)
+ widget.charmBase.image:setImageClip((data.id * 32) .. ' 0 32 32')
else
g_logger.error(string.format("Cyclopedia.CreateCharmItem - charm %s is nil", data.id))
return
end
- widget:setText(data.name)
+ local charmData = charms[data.id]
+ widget:setText(isModernUI and charmData.name or data.name)
widget.data = data
if data.asignedStatus then
if data.raceId then
- widget.InfoBase.Sprite:setOutfit(g_things.getRaceData(data.raceId).outfit)
+ local raceData = g_things.getRaceData(data.raceId)
+ widget.InfoBase.Sprite:setOutfit(raceData.outfit)
widget.InfoBase.Sprite:getCreature():setStaticWalking(1000)
else
g_logger.error("Cyclopedia.CreateCharmItem - no race id provided")
end
end
- if data.unlocked then
- widget.PriceBase.Charm:setVisible(false)
- widget.PriceBase.Gold:setVisible(true)
- widget.charmBase.lockedMask:setVisible(false)
- widget.icon = 1
- if data.asignedStatus then
- widget.PriceBase.Value:setText(comma_value(data.removeRuneCost))
- else
- widget.PriceBase.Value:setText(0)
- end
+ local isUnlocked = data.tier > 0 or data.unlocked
+ if not isModernUI then
+ widget.PriceBase.Charm:setVisible(not isUnlocked)
+ widget.PriceBase.Gold:setVisible(isUnlocked)
+ end
+ widget.charmBase.lockedMask:setVisible(not isUnlocked)
+ widget.icon = isUnlocked and 1 or 0
+
+ if isUnlocked then
+ widget.PriceBase.Value:setText(data.asignedStatus and comma_value(data.removeRuneCost) or 0)
else
- widget.PriceBase.Charm:setVisible(true)
- widget.PriceBase.Gold:setVisible(false)
- widget.charmBase.lockedMask:setVisible(true)
widget.PriceBase.Value:setText(comma_value(data.unlockPrice))
- widget.icon = 0
end
- if widget.icon == 1 and g_game.getLocalPlayer():getResourceBalance(1) then
- if data.removeRuneCost > g_game.getLocalPlayer():getResourceBalance(1) then
- value:setColor("#D33C3C")
- else
- value:setColor("#C0C0C0")
- end
+ local player = g_game.getLocalPlayer()
+ if widget.icon == 1 and player:getResourceBalance(ResourceTypes.GOLD_EQUIPPED) then
+ local canAfford = data.removeRuneCost <= player:getResourceBalance(ResourceTypes.GOLD_EQUIPPED)
+ value:setColor(canAfford and "#C0C0C0" or "#D33C3C")
+ elseif widget.icon == 0 then
+ local canAfford = data.unlockPrice <= UI.CharmsPoints
+ value:setColor(canAfford and "#C0C0C0" or "#D33C3C")
end
- if widget.icon == 0 then
- if data.unlockPrice > UI.CharmsPoints then
- value:setColor("#D33C3C")
- else
- value:setColor("#C0C0C0")
- end
+ widget.category = charmData.category
+
+ if isModernUI and data.tier > 0 then
+ widget.charmBase.border:setImageSource("/game_cyclopedia/images/charms/border/backdrop_charmgrade" .. data.tier)
end
end
function Cyclopedia.loadCharms(charmsData)
- controllerCyclopedia.ui.CharmsBase.Value:setText(Cyclopedia.formatGold(charmsData.points))
-
- if UI == nil or UI.CharmList == nil then -- I know, don't change it
+ if not UI then
+ return
+ end
+ if isModernUI and not UI.mainPanelCharmsType then
return
end
+ local CharmList = isModernUI and UI.mainPanelCharmsType.panelCharmList.CharmList or UI.CharmList
+ local player = g_game.getLocalPlayer()
+
+ if isModernUI then
+ local formatResourceBalance = function(resourceType, maxResourceType)
+ return string.format("%d/%d", player:getResourceBalance(resourceType),
+ player:getResourceBalance(maxResourceType))
+ end
+
+ controllerCyclopedia.ui.CharmsBase.Value:setText(formatResourceBalance(ResourceTypes.CHARM,
+ ResourceTypes.MAX_CHARM))
+ controllerCyclopedia.ui.CharmsBase1410.Value:setText(
+ formatResourceBalance(ResourceTypes.MINOR_CHARM, ResourceTypes.MAX_MINOR_CHARM))
+ else
+ controllerCyclopedia.ui.CharmsBase.Value:setText(Cyclopedia.formatGold(charmsData.points))
+ end
UI.CharmsPoints = charmsData.points
+ Cyclopedia.Charms.Monsters = {}
local raceIdNamePairs = {}
for _, raceId in ipairs(charmsData.finishedMonsters) do
- local raceName = g_things.getRaceData(raceId).name
- if #raceName == 0 then
- raceName = string.format("unnamed_%d", raceId)
- end
-
+ local raceData = g_things.getRaceData(raceId)
+ local raceName = raceData.name ~= "" and raceData.name or string.format("unnamed_%d", raceId)
table.insert(raceIdNamePairs, {
raceId = raceId,
name = raceName
})
end
- local function compareByName(a, b)
+ table.sort(raceIdNamePairs, function(a, b)
return a.name:lower() < b.name:lower()
- end
-
- table.sort(raceIdNamePairs, compareByName)
-
- Cyclopedia.Charms.Monsters = {}
+ end)
for _, pair in ipairs(raceIdNamePairs) do
table.insert(Cyclopedia.Charms.Monsters, pair.raceId)
end
- UI.CharmList:destroyChildren()
-
- local formatedData = {}
+ CharmList:destroyChildren()
+ local formattedData = {}
for _, charmData in pairs(charmsData.charms) do
- local internalId = (charmData.id)
- if internalId then
+ local internalId = charmData.id
+ if internalId and charms[internalId] then
+ local charm = charms[internalId]
+ charmData.name = charmData.name ~= "" and charmData.name or charm.name
+ charmData.description = charmData.description ~= "" and charmData.description or charm.description
charmData.internalId = internalId
- charmData.typePriority = CHARMS[internalId + 1].TYPE
- table.insert(formatedData, charmData)
+ charmData.typePriority = charm.type
+ charmData.category = charm.category
+ table.insert(formattedData, charmData)
end
end
- table.sort(formatedData, function(a, b)
- if a.unlocked == b.unlocked then
- if a.typePriority == b.typePriority then
- return a.name < b.name
- else
- return a.typePriority < b.typePriority
+ if isModernUI then
+ table.sort(formattedData, function(a, b)
+ local tierA, tierB = a.tier or 0, b.tier or 0
+ if tierA ~= tierB then
+ return tierA > tierB
+ end
+ return a.name:lower() < b.name:lower()
+ end)
+ else
+ table.sort(formattedData, function(a, b)
+ if a.unlocked ~= b.unlocked then
+ return a.unlocked and not b.unlocked
+ end
+ return a.name:lower() < b.name:lower()
+ end)
+ end
+
+ for _, value in ipairs(formattedData) do
+ if value and value.name and value.description and value.internalId and value.typePriority then
+ local success, error = pcall(Cyclopedia.CreateCharmItem, value)
+ if not success then
+ g_logger.error(string.format("Error creating charm item: %s for charm ID: %s (%s)", error,
+ tostring(value.internalId), tostring(value.name)))
end
else
- return a.unlocked and not b.unlocked
+ g_logger.error(string.format("Incomplete charm data: ID: %s",
+ value and tostring(value.internalId or "unknown") or "nil"))
end
- end)
+ end
+
+ if isModernUI then
+ local selectedWidget = TypeCharmRadioGroup:getSelectedWidget()
+ if selectedWidget then
+ local charmCategory = selectedWidget:getId() == "MajorCharms" and charmCategory_t.CHARM_MAJOR or
+ charmCategory_t.CHARM_MINOR
+
+ for _, widget in ipairs(CharmList:getChildren()) do
+ widget:setVisible(widget.category == charmCategory)
+ end
- for _, value in ipairs(formatedData) do
- Cyclopedia.CreateCharmItem(value)
+ CharmList:getLayout():update()
+ end
end
- if Cyclopedia.Charms.redirect then
- Cyclopedia.selectCharm(UI.CharmList:getChildById(Cyclopedia.Charms.redirect),
- UI.CharmList:getChildById(Cyclopedia.Charms.redirect):isChecked())
+ local firstCharm = Cyclopedia.Charms.redirect and CharmList:getChildById(Cyclopedia.Charms.redirect) or
+ CharmList:getChildByIndex(1)
+
+ if firstCharm then
+ Cyclopedia.selectCharm(firstCharm, firstCharm:isChecked())
Cyclopedia.Charms.redirect = nil
+ end
+end
+
+local function getUIBase()
+ if isModernUI then
+ return {
+ CreatureList = UI.InformationBase.PanelCreatureList.CreaturesBase.CreatureList,
+ InfoBase = UI.InformationBase.panelSelectCreature.InfoBase,
+ TextBase = UI.InformationBase.TextBase,
+ ItemBase = UI.InformationBase.ItemBase,
+ PriceBase = UI.InformationBase.verticalPanelUnLockClearChram.PriceBaseGold,
+ UnlockButton = UI.InformationBase.verticalPanelUnLockClearChram.UnlockButton,
+ SearchEdit = UI.InformationBase.PanelCreatureList.SearchEdit.SearchEdit,
+ SearchLabel = UI.InformationBase.SearchLabel,
+ CreaturesBase = UI.InformationBase.PanelCreatureList.CreaturesBase,
+ CreaturesLabel = UI.InformationBase.panelSelectCreature.CreaturesLabel
+ }
else
- Cyclopedia.selectCharm(UI.CharmList:getChildByIndex(1), UI.CharmList:getChildByIndex(1):isChecked())
+ return {
+ CreatureList = UI.InformationBase.CreaturesBase.CreatureList,
+ InfoBase = UI.InformationBase.InfoBase,
+ TextBase = UI.InformationBase.TextBase,
+ ItemBase = UI.InformationBase.ItemBase,
+ PriceBase = UI.InformationBase.PriceBase,
+ UnlockButton = UI.InformationBase.UnlockButton,
+ SearchEdit = UI.InformationBase.SearchEdit,
+ SearchLabel = UI.InformationBase.SearchLabel,
+ CreaturesBase = UI.InformationBase.CreaturesBase,
+ CreaturesLabel = UI.InformationBase.CreaturesLabel
+ }
end
end
-function Cyclopedia.selectCharm(widget, isChecked)
- UI.InformationBase.CreaturesBase.CreatureList:destroyChildren()
+local function formatCreatureName(text)
+ local capitalizedText = text:gsub("(%l)(%w*)", function(first, rest)
+ return first:upper() .. rest
+ end)
+ return #capitalizedText > 19 and capitalizedText:sub(1, 16) .. "..." or capitalizedText
+end
- local parent = widget:getParent()
- local button = UI.InformationBase.UnlockButton
- local value = UI.InformationBase.PriceBase.Value
+local function updateUIColors(widget, UI_BASE)
+ local player = g_game.getLocalPlayer()
+ local priceValue = UI_BASE.PriceBase.Value
+ if isModernUI then
+ local charmEntry = charms[widget.data.id]
+ if charmEntry and charmEntry.points and charmEntry.points[widget.data.tier + 1] then
+ local selectedWidget = TypeCharmRadioGroup:getSelectedWidget()
+ if selectedWidget then
+ local charmCategory = selectedWidget:getId() == "MajorCharms" and ResourceTypes.CHARM or
+ ResourceTypes.MINOR_CHARM
+ local pointsValue = charmEntry.points[widget.data.tier + 1]
+ local canAfford = pointsValue <= player:getResourceBalance(charmCategory)
+ UI.InformationBase.verticalPanelUnLockClearChram.PriceBaseCharm.Value:setColor(
+ canAfford and "#C0C0C0" or "#D33C3C")
+ UI.InformationBase.verticalPanelUnLockClearChram.UnlockButton:setEnabled(canAfford)
+ end
+ else
+ UI.InformationBase.verticalPanelUnLockClearChram.PriceBaseCharm.Value:setColor("#C0C0C0")
+ UI.InformationBase.verticalPanelUnLockClearChram.UnlockButton:setEnabled(false)
+ end
+ if not widget.data.asignedStatus then
+ priceValue:setText(0)
+ end
+ else
+ if widget.icon == 1 and player:getResourceBalance(ResourceTypes.GOLD_EQUIPPED) then
+ local canAfford = widget.data.removeRuneCost <= player:getResourceBalance(ResourceTypes.GOLD_EQUIPPED)
+ priceValue:setColor(canAfford and "#C0C0C0" or "#D33C3C")
+ UI_BASE.UnlockButton:setEnabled(canAfford)
+
+ local priceText = (widget.data.unlocked and not widget.data.asignedStatus) and 0 or
+ comma_value(widget.data.removeRuneCost)
+ priceValue:setText(priceText)
+ elseif widget.icon == 0 then
+ local canAfford = widget.data.unlockPrice <= UI.CharmsPoints
+ priceValue:setColor(canAfford and "#C0C0C0" or "#D33C3C")
+ UI_BASE.UnlockButton:setEnabled(canAfford)
+ priceValue:setText(widget.data.unlockPrice)
+ end
+ end
+end
- UI.InformationBase.data = widget.data
+local function setupCreatureList(widget, UI_BASE)
+ if (widget.data.unlocked and not widget.data.asignedStatus) or isModernUI then
+ UI_BASE.UnlockButton:setText("Select")
- local function format(text)
- local capitalizedText = text:gsub("(%l)(%w*)", function(first, rest)
- return first:upper() .. rest
- end)
+ local color = "#484848"
+ for index, raceId in ipairs(Cyclopedia.Charms.Monsters) do
+ local creatureWidget = g_ui.createWidget("CharmCreatureName", UI_BASE.CreatureList)
+ creatureWidget:setId(index)
+ creatureWidget:setText(formatCreatureName(g_things.getRaceData(raceId).name))
+ creatureWidget.raceId = raceId
+ creatureWidget:setBackgroundColor(color)
+ creatureWidget.color = color
+ color = color == "#484848" and "#414141" or "#484848"
+ end
- if #capitalizedText > 19 then
- return capitalizedText:sub(1, 16) .. "..."
- else
- return capitalizedText
+ UI_BASE.UnlockButton:setEnabled(false)
+ UI_BASE.SearchEdit:setEnabled(true)
+ if UI_BASE.SearchLabel then
+ UI_BASE.SearchLabel:setEnabled(true)
end
+ UI_BASE.CreaturesLabel:setEnabled(true)
end
+end
+
+local function setupModernVersionUpgrade(widget, UI_BASE)
+ if not isModernUI then
+ return
+ end
+
+ local charmId = widget.data.id
+ local tier = widget.data.tier or 0
+ local charmEntry = charms[charmId]
+
+ if charmEntry and charmEntry.points and charmEntry.points[tier + 1] then
+ local pointsValue = charmEntry.points[tier + 1]
+ local chanceValue = charmEntry.chance and charmEntry.chance[tier + 1] or 0
+
+ UI.InformationBase.verticalPanelUnLockClearChram.PriceBaseCharm.Value:setText(comma_value(pointsValue))
+ local tierButtons = {
+ [3] = "Fully Unlocked",
+ [2] = string.format("Upgrade to %d%%", charmEntry.chance[3] or 0),
+ [1] = string.format("Upgrade to %d%%", charmEntry.chance[2] or 0),
+ [0] = "Unlock"
+ }
+ UI_BASE.UnlockButton:setText(tierButtons[tier])
+ if tier >= 0 and tier < 3 then
+ UI_BASE.UnlockButton:setEnabled(true)
+ end
+ UI_BASE.UnlockButton:getParent().data = widget.data
+ else
+ UI_BASE.UnlockButton:setText("Fully Unlocked")
+ UI.InformationBase.verticalPanelUnLockClearChram.PriceBaseCharm.Value:setText(comma_value(0))
+ end
+end
+
+function Cyclopedia.selectCharm(widget, isChecked)
+ local UI_BASE = getUIBase()
+ UI_BASE.CreatureList:destroyChildren()
+ local parent = widget:getParent()
+ UI.InformationBase.data = widget.data
for i = 1, parent:getChildCount() do
local internalWidget = parent:getChildByIndex(i)
-
if internalWidget:isChecked() and widget:getId() ~= internalWidget:getId() then
internalWidget:setChecked(false)
end
@@ -207,96 +618,79 @@ function Cyclopedia.selectCharm(widget, isChecked)
widget:setChecked(true)
end
- UI.InformationBase.TextBase:setText(widget.data.description)
- UI.InformationBase.ItemBase.image:setImageSource(widget.charmBase.image:getImageSource())
-
- if widget.data.asignedStatus then
- UI.InformationBase.InfoBase.sprite:setVisible(true)
- UI.InformationBase.InfoBase.sprite:setOutfit(g_things.getRaceData(widget.data.raceId).outfit)
- UI.InformationBase.InfoBase.sprite:getCreature():setStaticWalking(1000)
- UI.InformationBase.InfoBase.sprite:setOpacity(1)
- else
- UI.InformationBase.InfoBase.sprite:setVisible(false)
- end
-
- if widget.icon == 1 then
- UI.InformationBase.PriceBase.Gold:setVisible(true)
- UI.InformationBase.PriceBase.Charm:setVisible(false)
- else
- UI.InformationBase.PriceBase.Gold:setVisible(false)
- UI.InformationBase.PriceBase.Charm:setVisible(true)
- end
-
- if widget.icon == 1 and g_game.getLocalPlayer():getResourceBalance(1) then
- if widget.data.removeRuneCost > g_game.getLocalPlayer():getResourceBalance(1) then
- value:setColor("#D33C3C")
- button:setEnabled(false)
- else
- value:setColor("#C0C0C0")
- button:setEnabled(true)
- end
+ UI_BASE.TextBase:setText(widget.data.description)
+ UI_BASE.ItemBase.image:setImageSource(widget.charmBase.image:getImageSource())
+ UI_BASE.ItemBase.image:setImageClip(widget.charmBase.image:getImageClip())
- if widget.data.unlocked and not widget.data.asignedStatus then
- value:setText(0)
+ if isModernUI then
+ UI.InformationBase:setText(widget:getText())
+ if widget.data.tier > 0 then
+ UI_BASE.ItemBase.border:setImageSource("/game_cyclopedia/images/charms/border/backdrop_charmgrade" ..
+ widget.data.tier)
+ UI_BASE.ItemBase.lockedMask:setVisible(false)
else
- value:setText(comma_value(widget.data.removeRuneCost))
+ UI_BASE.ItemBase.lockedMask:setVisible(true)
+ UI_BASE.ItemBase.border:setImageSource("")
end
end
- if widget.icon == 0 then
- if widget.data.unlockPrice > UI.CharmsPoints then
- value:setColor("#D33C3C")
- button:setEnabled(false)
- else
- value:setColor("#C0C0C0")
- button:setEnabled(true)
- end
-
- value:setText(widget.data.unlockPrice)
+ if widget.data.asignedStatus then
+ local sprite = UI_BASE.InfoBase.sprite
+ sprite:setVisible(true)
+ sprite:setOutfit(g_things.getRaceData(widget.data.raceId).outfit)
+ sprite:getCreature():setStaticWalking(1000)
+ sprite:setOpacity(1)
+ else
+ UI_BASE.InfoBase.sprite:setVisible(false)
end
- if widget.data.unlocked and not widget.data.asignedStatus then
- button:setText("Select")
-
- local color = "#484848"
+ if not isModernUI then
+ UI_BASE.PriceBase.Gold:setVisible(widget.icon == 1)
+ UI_BASE.PriceBase.Charm:setVisible(widget.icon == 0)
+ end
- for index, raceId in ipairs(Cyclopedia.Charms.Monsters) do
- local internalWidget = g_ui.createWidget("CharmCreatureName", UI.InformationBase.CreaturesBase.CreatureList)
- internalWidget:setId(index)
- internalWidget:setText(format(g_things.getRaceData(raceId).name))
- internalWidget.raceId = raceId
- internalWidget:setBackgroundColor(color)
- internalWidget.color = color
- color = color == "#484848" and "#414141" or "#484848"
- end
+ updateUIColors(widget, UI_BASE)
- button:setEnabled(false)
- UI.InformationBase.SearchEdit:setEnabled(true)
- UI.InformationBase.SearchLabel:setEnabled(true)
- UI.InformationBase.CreaturesLabel:setEnabled(true)
- end
+ setupCreatureList(widget, UI_BASE)
if widget.data.asignedStatus then
- button:setText("Remove")
-
- local internalWidget = g_ui.createWidget("CharmCreatureName", UI.InformationBase.CreaturesBase.CreatureList)
- internalWidget:setText(format(g_things.getRaceData(widget.data.raceId).name))
- internalWidget:setEnabled(false)
- internalWidget:setColor("#707070")
- UI.InformationBase.SearchEdit:setEnabled(false)
- UI.InformationBase.SearchLabel:setEnabled(false)
- UI.InformationBase.CreaturesLabel:setEnabled(false)
+ UI_BASE.UnlockButton:setText("Remove")
+ local creatureWidget = g_ui.createWidget("CharmCreatureName", UI_BASE.CreatureList)
+ creatureWidget:setText(formatCreatureName(g_things.getRaceData(widget.data.raceId).name))
+ creatureWidget:setEnabled(false)
+ creatureWidget:setColor("#707070")
+
+ UI_BASE.SearchEdit:setEnabled(false)
+ if UI_BASE.SearchLabel then
+ UI_BASE.SearchLabel:setEnabled(false)
+ end
+ UI_BASE.CreaturesLabel:setEnabled(false)
end
if not widget.data.unlocked then
- button:setText("Unlock")
- UI.InformationBase.SearchEdit:setEnabled(false)
- UI.InformationBase.SearchLabel:setEnabled(false)
- UI.InformationBase.CreaturesLabel:setEnabled(false)
+ UI_BASE.UnlockButton:setText("Unlock")
+ UI_BASE.SearchEdit:setEnabled(false)
+ if UI_BASE.SearchLabel then
+ UI_BASE.SearchLabel:setEnabled(false)
+ end
+ if not isModernUI then
+ UI_BASE.CreaturesLabel:setEnabled(false)
+ end
end
+
+ setupModernVersionUpgrade(widget, UI_BASE)
end
function Cyclopedia.selectCreatureCharm(widget, isChecked)
+ local UI_BASE = {}
+
+ if isModernUI then
+ UI_BASE.InfoBase = UI.InformationBase.panelSelectCreature.InfoBase
+ UI_BASE.UnlockButton = UI.InformationBase.verticalPanelUnLockClearChram.UnlockButton
+ else
+ UI_BASE.InfoBase = UI.InformationBase.InfoBase
+ UI_BASE.UnlockButton = UI.InformationBase.UnlockButton
+ end
local parent = widget:getParent()
for i = 1, parent:getChildCount() do
@@ -312,17 +706,25 @@ function Cyclopedia.selectCreatureCharm(widget, isChecked)
widget:setChecked(true)
end
- UI.InformationBase.InfoBase.sprite:setVisible(true)
- UI.InformationBase.InfoBase.sprite:setOutfit(g_things.getRaceData(widget.raceId).outfit)
- UI.InformationBase.InfoBase.sprite:getCreature():setStaticWalking(1000)
- UI.InformationBase.InfoBase.sprite:setOpacity(0.5)
- UI.InformationBase.UnlockButton:setEnabled(true)
+ UI_BASE.InfoBase.sprite:setVisible(true)
+ UI_BASE.InfoBase.sprite:setOutfit(g_things.getRaceData(widget.raceId).outfit)
+ UI_BASE.InfoBase.sprite:getCreature():setStaticWalking(1000)
+ UI_BASE.InfoBase.sprite:setOpacity(0.5)
+ UI_BASE.UnlockButton:setEnabled(true)
Cyclopedia.Charms.SelectedCreature = widget.raceId
end
function Cyclopedia.searchCharmMonster(text)
- UI.InformationBase.CreaturesBase.CreatureList:destroyChildren()
+ local UI_BASE = {}
+
+ if isModernUI then
+ UI_BASE.CreaturesBase = UI.InformationBase.PanelCreatureList.CreaturesBase
+ else
+ UI_BASE.CreaturesBase = UI.InformationBase.CreaturesBase
+ end
+
+ UI_BASE.CreaturesBase.CreatureList:destroyChildren()
local function format(string)
local capitalizedText = string:gsub("(%l)(%w*)", function(first, rest)
@@ -356,7 +758,7 @@ function Cyclopedia.searchCharmMonster(text)
local color = "#484848"
for _, raceId in ipairs(searchedMonsters) do
- local internalWidget = g_ui.createWidget("CharmCreatureName", UI.InformationBase.CreaturesBase.CreatureList)
+ local internalWidget = g_ui.createWidget("CharmCreatureName", UI_BASE.CreaturesBase.CreatureList)
internalWidget:setId(raceId)
internalWidget:setText(format(g_things.getRaceData(raceId).name))
internalWidget.raceId = raceId
@@ -373,7 +775,11 @@ function Cyclopedia.actionCharmButton(widget)
if type == "Unlock" then
local function yesCallback()
- g_game.BuyCharmRune(data.id)
+ if isModernUI then
+ g_game.BuyCharmRune(0, data.id, 0)
+ else
+ g_game.BuyCharmRune(data.id)
+ end
if confirmWindow then
confirmWindow:destroy()
confirmWindow = nil
@@ -403,14 +809,16 @@ function Cyclopedia.actionCharmButton(widget)
callback = noCallback
},
anchor = AnchorHorizontalCenter
- }, yesCallback, noCallback
- )
+ }, yesCallback, noCallback)
end
end
-
- if type == "Select" then
+ if type == "Select" or type == "Select Creature" then
local function yesCallback()
- g_game.BuyCharmRune(data.id, 1, Cyclopedia.Charms.SelectedCreature)
+ if isModernUI then
+ g_game.BuyCharmRune(1, data.id, Cyclopedia.Charms.SelectedCreature)
+ else
+ g_game.BuyCharmRune(data.id, 1, Cyclopedia.Charms.SelectedCreature)
+ end
if confirmWindow then
confirmWindow:destroy()
confirmWindow = nil
@@ -437,8 +845,7 @@ function Cyclopedia.actionCharmButton(widget)
callback = noCallback
},
anchor = AnchorHorizontalCenter
- }, yesCallback, noCallback
- )
+ }, yesCallback, noCallback)
end
end
@@ -473,8 +880,104 @@ function Cyclopedia.actionCharmButton(widget)
callback = noCallback
},
anchor = AnchorHorizontalCenter
- }, yesCallback, noCallback
- )
+ }, yesCallback, noCallback)
+ end
+ end
+ if isModernUI and type:match("^Upgrade") then
+ local function yesCallback()
+ g_game.BuyCharmRune(0, data.id, 0)
+ if confirmWindow then
+ confirmWindow:destroy()
+ confirmWindow = nil
+ end
+ Cyclopedia.Charms.redirect = data.id
+ end
+
+ local function noCallback()
+ if confirmWindow then
+ confirmWindow:destroy()
+ confirmWindow = nil
+ end
+ end
+
+ if not confirmWindow then
+ confirmWindow = displayGeneralBox(tr("Confirm Unlocking of Charm"), tr(
+ "Do you want to upgrade the Charm %s? This will cost you %d Charm Points?", data.name, data.unlockPrice),
+ {
+ {
+ text = tr("Yes"),
+ callback = yesCallback
+ },
+ {
+ text = tr("No"),
+ callback = noCallback
+ },
+ anchor = AnchorHorizontalCenter
+ }, yesCallback, noCallback)
+ end
+ end
+end
+
+function onTypeCharmRadioGroup(radioGroup, selectedWidget)
+ local charmCategory = selectedWidget:getId() == "MajorCharms" and charmCategory_t.CHARM_MAJOR or
+ charmCategory_t.CHARM_MINOR
+ local CharmList = UI.mainPanelCharmsType.panelCharmList.CharmList
+ if charmCategory == charmCategory_t.CHARM_MAJOR then
+ UI.InformationBase.verticalPanelUnLockClearChram.PriceBaseCharm.Charm:setImageSource(
+ "/game_cyclopedia/images/monster-icon-bonuspoints")
+ else
+ UI.InformationBase.verticalPanelUnLockClearChram.PriceBaseCharm.Charm:setImageSource(
+ "/game_cyclopedia/images/minor-charm-echoes")
+ end
+ for _, widget in ipairs(CharmList:getChildren()) do
+ if widget.category == charmCategory then
+ widget:setVisible(true)
+ else
+ widget:setVisible(false)
+ end
+ end
+
+ CharmList:getLayout():update()
+end
+
+function Cyclopedia.actionSelectCharmButton(widget)
+ local confirmWindow
+ local type = widget:getText()
+ local data = UI.InformationBase.data
+ if type == "Select" or type == "Select Creature" then
+ local function yesCallback()
+ if isModernUI then
+ g_game.BuyCharmRune(1, data.id, Cyclopedia.Charms.SelectedCreature)
+ else
+ g_game.BuyCharmRune(data.id, 1, Cyclopedia.Charms.SelectedCreature)
+ end
+ if confirmWindow then
+ confirmWindow:destroy()
+ confirmWindow = nil
+ end
+ Cyclopedia.Charms.redirect = data.id
+ end
+
+ local function noCallback()
+ if confirmWindow then
+ confirmWindow:destroy()
+ confirmWindow = nil
+ end
+ end
+
+ if not confirmWindow then
+ confirmWindow = displayGeneralBox(tr("Confirm Selected Charm"),
+ tr("Do you want to use the Charm %s for this creature?", data.name), {
+ {
+ text = tr("Yes"),
+ callback = yesCallback
+ },
+ {
+ text = tr("No"),
+ callback = noCallback
+ },
+ anchor = AnchorHorizontalCenter
+ }, yesCallback, noCallback)
end
end
end
diff --git a/modules/game_cyclopedia/tab/charms/charms1410.otui b/modules/game_cyclopedia/tab/charms/charms1410.otui
new file mode 100644
index 0000000000..4ea788962c
--- /dev/null
+++ b/modules/game_cyclopedia/tab/charms/charms1410.otui
@@ -0,0 +1,327 @@
+UIWidget
+ id: Cat3
+ anchors.fill: parent
+ visible: false
+ MiniPanel
+ id: InformationBase
+ text: carnage
+ anchors.top: parent.top
+ anchors.left: parent.left
+ size: 551 122
+ layout:
+ type: horizontalBox
+ spacing: -1
+
+ Label
+ id: TextBase
+ text: "killin a monster has 10% chance to deal physical damage equal to 15& of its maximum health to all monsters in a small rabius"
+ width: 165
+ text-wrap: true
+ margin-top: 25
+ margin-right: 5
+ BorderBox
+ id: ItemBase
+ image-source: /images/ui/panel_flat
+ image-border: 10
+ size: 46 46
+ margin-top: 23
+ margin-bottom: 22
+ phantom: true
+ border-width-top: 1
+ border-color-top: black
+ border-width-left: 1
+ border-color-left: black
+ border-width-bottom: 1
+ border-color-bottom: gray
+ border-width-right: 1
+ border-color-right: gray
+ UIWidget
+ size: 32 32
+ id: image
+ anchors.centerIn: parent
+ phantom: true
+ UIWidget
+ id: lockedMask
+ size: 42 42
+ anchors.centerIn: parent
+ image-source: /images/ui/ditherpattern
+ phantom: true
+ UIWidget
+ id: border
+ anchors.centerIn: parent
+ phantom: true
+
+ Panel
+ id: verticalPanelUnLockClearChram
+ width: 92
+ margin-left: 5
+ QtButton
+ id: UnlockButton
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ text: UnLock
+ @onClick: modules.game_cyclopedia.Cyclopedia.actionCharmButton(self)
+ UIWidget
+ id: PriceBaseCharm
+ anchors.top: prev.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ margin-top: 3
+ height: 20
+ image-source: /images/ui/item
+ image-border: 10
+ phantom: true
+ Label
+ id: Value
+ anchors.centerIn: parent
+ !text: tr('0')
+ text-auto-resize: true
+ UIWidget
+ id: Charm
+ anchors.left: Value.right
+ anchors.verticalCenter: parent.verticalCenter
+ margin-left: 2
+ image-source: /game_cyclopedia/images/monster-icon-bonuspoints
+
+ QtButton
+ id: check
+ anchors.top: prev.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ text: Addon 1
+ margin-top:3
+ text: Clear Charm
+
+
+ UIWidget
+ id: PriceBaseGold
+ anchors.top: prev.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ margin-top: 3
+ height: 20
+ image-source: /images/ui/item
+ image-border: 10
+ phantom: true
+ Label
+ id: Value
+ anchors.centerIn: parent
+ !text: tr('0')
+ text-auto-resize: true
+ UIWidget
+ id: Gold
+ anchors.left: Value.right
+ anchors.verticalCenter: parent.verticalCenter
+ margin-left: 2
+ image-source: /game_cyclopedia/images/icon_gold
+ visible: true
+ Panel
+ id: PanelCreatureList
+ width: 140
+ margin-left: 5
+ TextEditQuestLog
+ id:SearchEdit
+ height: 20
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ BorderBox
+ id: CreaturesBase
+ anchors.top: prev.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: 68
+ margin-left: 5
+ margin-top: 5
+ image-source: /images/ui/panel_flat
+ image-border: 10
+ phantom: true
+ UIScrollArea
+ id: CreatureList
+ vertical-scrollbar: CreatureListScrollbar
+ anchors.fill: parent
+ margin-right: 13
+ padding-left: 3
+ padding-top: 3
+ padding-bottom: 2
+ layout:
+ type: grid
+ cell-size: 125 15
+ cell-spacing: 2
+ flow: true
+ CharmCreatureName
+ background-color: #484848
+ CharmCreatureName
+ background-color: #414141
+ VerticalQtScrollBar
+ id: CreatureListScrollbar
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ step: 80
+ pixel-scroll: true
+
+ Panel
+ id: panelSelectCreature
+ width: 80
+ margin-left: 5
+ QtButton
+ id: CreaturesLabel
+ height: 20
+ text: Select Creature
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ @onClick: modules.game_cyclopedia.Cyclopedia.actionSelectCharmButton(self)
+ BorderBox
+ id: InfoBase
+ anchors.top: prev.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: 68
+ margin-top: 5
+ image-source: /images/ui/panel_flat
+ image-border: 10
+ phantom: true
+ UICreature
+ id: sprite
+ anchors.centerIn: parent
+ size: 64 64
+
+ QtPanel
+ id: anotherPanel
+ size: 112 122
+ anchors.top: parent.top
+ anchors.left: prev.right
+ margin-left: 5
+
+ QtButton
+ text: Reset all charms
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ margin-top: -15
+
+ UIWidget
+ id: CharmsBase1410
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ image-source: /images/ui/item
+ image-border: 10
+ margin-top: 15
+ size: 100 20
+ !tooltip: tr("Minor Charm echoes\nEarned by unlocking or upgrading major charm and promotion your character")
+ UIWidget
+ id: Icon
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ margin-right: 4
+ image-source: /game_cyclopedia/images/icon_gold
+ Label
+ id: Value
+ anchors.right: Icon.left
+ anchors.verticalCenter: parent.verticalCenter
+ margin-right: 4
+ !text: tr('0')
+ text-auto-resize: true
+ color: #BDBDBD
+
+ QtPanel
+ id: mainPanelCharmsType
+ height: 299
+ anchors.top: prev.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ margin-top: 5
+
+ Panel
+ id: typeCharmPanel
+ height: 38
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ QtButton
+ id: MajorCharms
+ !text: tr('Major Charms')
+ font: verdana-11px-antialised
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: 322
+ margin: 1
+ $on:
+ margin-top: -1
+ $checked:
+ image-clip: 0 46 22 23
+ text-offset: 1 1
+
+ UIWidget
+ image-source: /game_cyclopedia/images/charms/icon-charms-major
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ phantom: true
+ QtButton
+ id: MinorCharms
+ !text: tr('Minor Charms')
+ font: verdana-11px-antialised
+ width: 322
+ anchors.top: parent.top
+ anchors.left: prev.right
+ anchors.bottom: parent.bottom
+ margin: 1
+ $on:
+ margin-top: -1
+ $checked:
+ image-clip: 0 46 22 23
+ text-offset: 1 1
+
+
+ UIWidget
+ image-source: /game_cyclopedia/images/charms/icon-charms-minor
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ phantom: true
+
+ Panel
+ id: panelCharmList
+ font: verdana-11px-antialised
+ text-offset: 24 5
+ text-align: topLeft
+ border-width: 1
+ border-color: #00000077
+ anchors.top: prev.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: 232
+ margin-top: 10
+ image-border: 4
+ image-border-top: 23
+ image-border-bottom: 4
+ focusable: true
+ UIScrollArea
+ id: CharmList
+ vertical-scrollbar: spellsScrollBar
+ anchors.fill: parent
+ padding: 1
+ width: 10
+ focusable: true
+ background-color: #404040
+ border-width: 1
+ border-color: #272727
+ padding: 1
+ auto-focus: first
+ layout:
+ type: grid
+ cell-size: 158 100
+ cell-spacing: 2
+ flow: true
+ VerticalQtScrollBar
+ id: spellsScrollBar
+ anchors.top: prev.top
+ anchors.bottom: prev.bottom
+ anchors.right: prev.right
+ step: 50
+ pixels-scroll: true
+ margin-right: -2
diff --git a/modules/game_cyclopedia/tab/house/house.lua b/modules/game_cyclopedia/tab/house/house.lua
index 480e101c2a..022fd773ea 100644
--- a/modules/game_cyclopedia/tab/house/house.lua
+++ b/modules/game_cyclopedia/tab/house/house.lua
@@ -25,7 +25,9 @@ function showHouse()
controllerCyclopedia.ui.CharmsBase:setVisible(false)
controllerCyclopedia.ui.GoldBase:setVisible(true)
controllerCyclopedia.ui.BestiaryTrackerButton:setVisible(false)
-
+ if g_game.getClientVersion() >= 1410 then
+ controllerCyclopedia.ui.CharmsBase1410:setVisible(false)
+ end
-- Cyclopedia.House.Data = json_data
if not Cyclopedia.House.Loaded then
diff --git a/modules/game_cyclopedia/tab/items/items.lua b/modules/game_cyclopedia/tab/items/items.lua
index 198736a6fd..466fefb348 100644
--- a/modules/game_cyclopedia/tab/items/items.lua
+++ b/modules/game_cyclopedia/tab/items/items.lua
@@ -60,7 +60,9 @@ function showItems()
controllerCyclopedia.ui.CharmsBase:setVisible(false)
controllerCyclopedia.ui.GoldBase:setVisible(false)
controllerCyclopedia.ui.BestiaryTrackerButton:setVisible(false)
-
+ if g_game.getClientVersion() >= 1410 then
+ controllerCyclopedia.ui.CharmsBase1410:setVisible(false)
+ end
local CategoryColor = "#484848"
for _, data in ipairs(Cyclopedia.CategoryItems) do
diff --git a/modules/game_cyclopedia/tab/magicalArchives/magicalArchives.lua b/modules/game_cyclopedia/tab/magicalArchives/magicalArchives.lua
new file mode 100644
index 0000000000..dc876aee4d
--- /dev/null
+++ b/modules/game_cyclopedia/tab/magicalArchives/magicalArchives.lua
@@ -0,0 +1,13 @@
+local UI = nil
+
+function showMagicalArchives()
+ UI = g_ui.loadUI("magicalArchives", contentContainer)
+ UI:show()
+ controllerCyclopedia.ui.CharmsBase:setVisible(false)
+ controllerCyclopedia.ui.GoldBase:setVisible(false)
+ controllerCyclopedia.ui.BestiaryTrackerButton:setVisible(false)
+ if g_game.getClientVersion() >= 1410 then
+ controllerCyclopedia.ui.CharmsBase1410:setVisible(false)
+ end
+end
+
diff --git a/modules/game_cyclopedia/tab/magicalArchives/magicalArchives.otui b/modules/game_cyclopedia/tab/magicalArchives/magicalArchives.otui
new file mode 100644
index 0000000000..037b1c8445
--- /dev/null
+++ b/modules/game_cyclopedia/tab/magicalArchives/magicalArchives.otui
@@ -0,0 +1,51 @@
+UIWidget
+ id: Cat5
+ anchors.fill: parent
+ visible: false
+ UIWidget
+ id: InformationBase
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.bottom: parent.bottom
+ margin-bottom: 8
+ margin-top: 10
+ width: 180
+ MiniPanel
+ id: selectSpell
+ anchors.fill: parent
+ image-border: 10
+ padding-right: 16
+ margin-bottom: 25
+ !text: tr('Select Spell')
+ layout:
+ type: verticalBox
+ spacing: 6
+
+ ComboBox
+ TextListQuestLog
+ height: 330
+ TextEditQuestLog
+
+ MiniPanel
+ id: spellAndRune
+ text: Spell / Rune Information
+ anchors.top: InformationBase.top
+ anchors.left: InformationBase.right
+ anchors.right: parent.right
+ anchors.bottom: InformationBase.bottom
+ margin-left: 10
+ margin-bottom: 25
+
+ Label
+ anchors.centerIn: parent
+ text: select a spell to view detailed information.
+ Label
+ id: deleteme
+ anchors.centerIn: parent
+ !text: tr("\nis this in the protobuf ?\n in http post ? \nor modify gamelib/spells.lua?")
+ font-scale: 2
+ margin-top: 32
+ text-auto-resize: true
+
+
+
diff --git a/modules/game_cyclopedia/tab/map/map.lua b/modules/game_cyclopedia/tab/map/map.lua
index 46814ac14d..3e74e9141b 100644
--- a/modules/game_cyclopedia/tab/map/map.lua
+++ b/modules/game_cyclopedia/tab/map/map.lua
@@ -14,6 +14,9 @@ function showMap()
controllerCyclopedia.ui.CharmsBase:setVisible(false)
controllerCyclopedia.ui.GoldBase:setVisible(true)
controllerCyclopedia.ui.BestiaryTrackerButton:setVisible(false)
+ if g_game.getClientVersion() >= 1410 then
+ controllerCyclopedia.ui.CharmsBase1410:setVisible(false)
+ end
end
function Cyclopedia.loadMap()
diff --git a/modules/game_cyclopedia/utils.lua b/modules/game_cyclopedia/utils.lua
index 57c02de244..644097026b 100644
--- a/modules/game_cyclopedia/utils.lua
+++ b/modules/game_cyclopedia/utils.lua
@@ -709,6 +709,6 @@ Cyclopedia.clientCombat[combatStates.CLIENT_COMBAT_ICE] = { path = '/game_cyclop
Cyclopedia.clientCombat[combatStates.CLIENT_COMBAT_HOLY] = {path = '/game_cyclopedia/images/bestiary/icons/monster-icon-holy-resist', id = 'Holy' }
Cyclopedia.clientCombat[combatStates.CLIENT_COMBAT_DEATH] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-death-resist', id = 'Death' }
Cyclopedia.clientCombat[combatStates.CLIENT_COMBAT_HEALING] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-healing-resist', id = 'Healing' }
-Cyclopedia.clientCombat[combatStates.CLIENT_COMBAT_DROWN] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-drown-resist', id = 'Drown' }
+Cyclopedia.clientCombat[combatStates.CLIENT_COMBAT_DROWN] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-drowning-resist', id = 'Drown' }
Cyclopedia.clientCombat[combatStates.CLIENT_COMBAT_LIFEDRAIN] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-lifedrain-resist', id = 'Lifedrain ' }
Cyclopedia.clientCombat[combatStates.CLIENT_COMBAT_MANADRAIN] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-manadrain-resist', id = 'Manadrain' }
diff --git a/modules/game_features/features.lua b/modules/game_features/features.lua
index 121f3ec59d..05a79d49c0 100644
--- a/modules/game_features/features.lua
+++ b/modules/game_features/features.lua
@@ -210,6 +210,7 @@ controller:registerEvents(g_game, {
end
if version >= 1281 then
+ g_game.enableFeature(GameForgeSkillStats)
g_game.enableFeature(GamePlayerFamiliars)
g_game.disableFeature(GameEnvironmentEffect)
g_game.disableFeature(GameItemAnimationPhase)
@@ -252,7 +253,13 @@ controller:registerEvents(g_game, {
end
if version >= 1332 then
- g_game.enableFeature(GameForgeConvergence);
+ g_game.enableFeature(GameForgeConvergence)
+ end
+
+ if version >= 1410 then
+ g_game.disableFeature(GameAdditionalSkills)
+ g_game.disableFeature(GameForgeSkillStats)
+ g_game.enableFeature(GameCharacterSkillStats)
end
end
})
diff --git a/modules/game_mainpanel/mainoptionspanel.otui b/modules/game_mainpanel/mainoptionspanel.otui
index 53772bbdca..851bd0a0c6 100644
--- a/modules/game_mainpanel/mainoptionspanel.otui
+++ b/modules/game_mainpanel/mainoptionspanel.otui
@@ -75,14 +75,12 @@ PhantomMiniWindow
cell-spacing: 2
flow: true
- UIWidget
- image-source: /images/ui/vertical_line_bright
+ VerticalSeparator
anchors.top: options.top
anchors.bottom: options.bottom
anchors.left: store.right
margin-left: 5
phantom: true
- width: 1
UIWidget
id: specials
diff --git a/modules/game_questlog/game_questlog.lua b/modules/game_questlog/game_questlog.lua
index 1223959058..d2147eb7ef 100644
--- a/modules/game_questlog/game_questlog.lua
+++ b/modules/game_questlog/game_questlog.lua
@@ -476,16 +476,16 @@ local function onQuestTracker(remainingQuests, missions)
end
trackerMiniWindow.contentsPanel.list:destroyChildren()
for index, mission in ipairs(missions) do
- local missionId, questName, questIsCompleted, missionName, missionDesc = unpack(mission)
+ local questId, missionId, questName, missionName, missionDesc = unpack(mission)
local trackerLabel = g_ui.createWidget('QuestTrackerLabel', trackerMiniWindow.contentsPanel.list)
trackerLabel:setId(missionId)
trackerLabel.description:setText(missionDesc)
end
end
-local function onUpdateQuestTracker(missionId, missionName, questIsCompleted, missionDesc)
+local function onUpdateQuestTracker(questId, missionId, questName, missionName, missionDesc)
-- untest
- -- print(missionId, missionName, questIsCompleted, missionDesc)
+ -- print(questId, missionId, questName, missionName, missionDesc)
local trackerLabel = trackerMiniWindow.contentsPanel.list:getChildById(missionId)
if trackerLabel then
trackerLabel.description:setText(missionDesc)
diff --git a/modules/game_skills/skills.lua b/modules/game_skills/skills.lua
index 5921190b12..eaf7cf672f 100644
--- a/modules/game_skills/skills.lua
+++ b/modules/game_skills/skills.lua
@@ -1,6 +1,7 @@
skillsWindow = nil
skillsButton = nil
skillsSettings = nil
+local ExpRating = {}
function init()
connect(LocalPlayer, {
@@ -19,7 +20,15 @@ function init()
onMagicLevelChange = onMagicLevelChange,
onBaseMagicLevelChange = onBaseMagicLevelChange,
onSkillChange = onSkillChange,
- onBaseSkillChange = onBaseSkillChange
+ onBaseSkillChange = onBaseSkillChange,
+ onFlatDamageHealingChange = onFlatDamageHealingChange,
+ onAttackInfoChange = onAttackInfoChange,
+ onConvertedDamageChange = onConvertedDamageChange,
+ onImbuementsChange = onImbuementsChange,
+ onDefenseInfoChange = onDefenseInfoChange,
+ onCombatAbsorbValuesChange = onCombatAbsorbValuesChange,
+ onForgeBonusesChange = onForgeBonusesChange,
+ onExperienceRateChange = onExperienceRateChange
})
connect(g_game, {
onGameStart = online,
@@ -68,7 +77,15 @@ function terminate()
onMagicLevelChange = onMagicLevelChange,
onBaseMagicLevelChange = onBaseMagicLevelChange,
onSkillChange = onSkillChange,
- onBaseSkillChange = onBaseSkillChange
+ onBaseSkillChange = onBaseSkillChange,
+ onFlatDamageHealingChange = onFlatDamageHealingChange,
+ onAttackInfoChange = onAttackInfoChange,
+ onConvertedDamageChange = onConvertedDamageChange,
+ onImbuementsChange = onImbuementsChange,
+ onDefenseInfoChange = onDefenseInfoChange,
+ onCombatAbsorbValuesChange = onCombatAbsorbValuesChange,
+ onForgeBonusesChange = onForgeBonusesChange,
+ onExperienceRateChange = onExperienceRateChange
})
disconnect(g_game, {
onGameStart = online,
@@ -230,6 +247,15 @@ function update()
else
regenerationTime:show()
end
+ local xpBoostButton = skillsWindow:recursiveGetChildById('xpBoostButton')
+ local xpGainRate = skillsWindow:recursiveGetChildById('xpGainRate')
+ if g_game.getFeature(GameExperienceBonus) then
+ xpBoostButton:show()
+ xpGainRate:show()
+ else
+ xpBoostButton:hide()
+ xpGainRate:hide()
+ end
end
function online()
@@ -271,22 +297,21 @@ function refresh()
local ativedAdditionalSkills = hasAdditionalSkills
if ativedAdditionalSkills then
if g_game.getClientVersion() >= 1281 then
- if i == Skill.LifeLeechAmount or i == Skill.ManaLeechAmount then
+ if i == Skill.LifeLeechAmount or i == Skill.ManaLeechAmount then
ativedAdditionalSkills = false
elseif g_game.getClientVersion() < 1332 and Skill.Transcendence then
ativedAdditionalSkills = false
elseif i >= Skill.Fatal and player:getSkillLevel(i) <= 0 then
ativedAdditionalSkills = false
end
- elseif g_game.getClientVersion() < 1281 and i >= Skill.Fatal then
+ elseif g_game.getClientVersion() < 1281 and i >= Skill.Fatal then
ativedAdditionalSkills = false
- end
+ end
end
-
toggleSkill('skillId' .. i, ativedAdditionalSkills)
end
end
-
+-- todo reload skills 14.12
update()
updateHeight()
end
@@ -435,12 +460,12 @@ function onLevelChange(localPlayer, value, percent)
end
function onHealthChange(localPlayer, health, maxHealth)
- setSkillValue('health', health)
+ setSkillValue('health', comma_value(health))
checkAlert('health', health, maxHealth, 30)
end
function onManaChange(localPlayer, mana, maxMana)
- setSkillValue('mana', mana)
+ setSkillValue('mana', comma_value(mana))
checkAlert('mana', mana, maxMana, 30)
end
@@ -449,7 +474,7 @@ function onSoulChange(localPlayer, soul)
end
function onFreeCapacityChange(localPlayer, freeCapacity)
- setSkillValue('capacity', freeCapacity)
+ setSkillValue('capacity', comma_value(freeCapacity))
checkAlert('capacity', freeCapacity, localPlayer:getTotalCapacity(), 20)
end
@@ -540,7 +565,7 @@ function onRegenerationChange(localPlayer, regenerationTime)
end
function onSpeedChange(localPlayer, speed)
- setSkillValue('speed', speed)
+ setSkillValue('speed', comma_value(speed))
onBaseSpeedChange(localPlayer, localPlayer:getBaseSpeed())
end
@@ -574,3 +599,199 @@ end
function onBaseSkillChange(localPlayer, id, baseLevel)
setSkillBase('skillId' .. id, localPlayer:getSkillLevel(id), baseLevel)
end
+
+-- 14.12
+local function updateExperienceRate(localPlayer)
+ local baseRate = ExpRating[ExperienceRate.BASE] or 100
+ local expRateTotal = baseRate
+
+ for type, value in pairs(ExpRating) do
+ if type ~= ExperienceRate.BASE and type ~= ExperienceRate.STAMINA_MULTIPLIER then
+ expRateTotal = expRateTotal + (value or 0)
+ end
+ end
+
+ local staminaMultiplier = ExpRating[ExperienceRate.STAMINA_MULTIPLIER] or 100
+ expRateTotal = expRateTotal * staminaMultiplier / 100
+
+ local xpgainrate = skillsWindow:recursiveGetChildById("xpGainRate")
+ if not xpgainrate then
+ return
+ end
+
+ local widget = xpgainrate:getChildById("value")
+ if not widget then
+ return
+ end
+
+ widget:setText(math.floor(expRateTotal) .. "%")
+
+ local tooltip = string.format("Your current XP gain rate amounts to %d%%.", math.floor(expRateTotal))
+ tooltip = tooltip ..
+ string.format("\nYour XP gain rate is calculated as follows:\n- Base XP gain rate %d%%", baseRate)
+
+ if (ExpRating[ExperienceRate.VOUCHER] or 0) > 0 then
+ tooltip = tooltip .. string.format("\n- Voucher: %d%%", ExpRating[ExperienceRate.VOUCHER])
+ end
+
+ if (ExpRating[ExperienceRate.XP_BOOST] or 0) > 0 then
+ tooltip = tooltip .. string.format("\n- XP Boost: %d%% (%s h remaining)", ExpRating[ExperienceRate.XP_BOOST],
+ formatTimeBySeconds(localPlayer:getStoreExpBoostTime()))
+ end
+ tooltip = tooltip .. string.format("\n- Stamina multiplier: x%.1f (%s h remaining)", staminaMultiplier / 100,
+ formatTimeByMinutes(localPlayer:getStamina() - 2340))
+
+ xpgainrate:setTooltip(tooltip)
+
+ if expRateTotal == 0 then
+ widget:setColor("#ff4a4a")
+ elseif expRateTotal > 100 then
+ widget:setColor("#00cc00")
+ elseif expRateTotal < 100 then
+ widget:setColor("#ff9429")
+ else
+ widget:setColor("#ffffff")
+ end
+end
+
+function onExperienceRateChange(localPlayer, type, value)
+ ExpRating[type] = value
+ updateExperienceRate(localPlayer)
+end
+
+local function setSkillValueWithTooltips(id, value, tooltip, showPercentage, color)
+ local skill = skillsWindow:recursiveGetChildById(id)
+ if not skill then
+ return
+ end
+ if value and value ~= 0 then
+ skill:show()
+ local widget = skill:getChildById('value')
+ if not widget then
+ return
+ end
+ if color then
+ widget:setColor(color)
+ end
+ if showPercentage then
+ local percentValue = math.floor(value * 10000) / 100
+ local sign = percentValue > 0 and "+ " or ""
+ widget:setText(sign .. percentValue .. "%")
+ if percentValue < 0 then
+ widget:setColor("#FF9854")
+ end
+ else
+ widget:setText(tostring(value))
+ end
+ if tooltip then
+ skill:setTooltip(tooltip)
+ end
+ else
+ skill:hide()
+ end
+end
+
+function onFlatDamageHealingChange(localPlayer, flatBonus)
+ local tooltips =
+ "This flat bonus is the main source of your character's power, added \nto most of the damage and healing values you cause."
+ setSkillValueWithTooltips('damageHealing', flatBonus, tooltips, false)
+end
+
+function onAttackInfoChange(localPlayer, attackValue, attackElement)
+ local tooltips =
+ "This is your character's basic attack power whenever you enter a \nfight with a weapon or your fists. It does not apply to any spells \nyou cast. The attack value is calculated from the weapon's attack\n value, the corresponding weapon skill, combat tactics, the bonus \nreceived from the Revelation Perks and the player's level. The \nvalue represents the average damage you would inflict on a\ncreature which had no kind of defence or protection."
+ setSkillValueWithTooltips('attackValue', attackValue, tooltips, false)
+ local skill = skillsWindow:recursiveGetChildById("attackValue")
+ if skill then
+ local element = clientCombat[attackElement]
+ if element then
+ skill:getChildById('icon'):setImageSource(element.path)
+ skill:getChildById('icon'):setImageSize({
+ width = 9,
+ height = 9
+ })
+ end
+ end
+end
+
+function onConvertedDamageChange(localPlayer, convertedDamage, convertedElement)
+ setSkillValueWithTooltips('convertedDamage', convertedDamage, false, true)
+ setSkillValueWithTooltips('convertedElement', convertedElement, false, true)
+end
+
+function onImbuementsChange(localPlayer, lifeLeech, manaLeech, critChance, critDamage, onslaught)
+ local lifeLeechTooltips =
+ "You have a +11.4% chance to trigger Onslaught, granting you 60% increased damage for all attacks."
+ local manaLeechTooltips = "You have a +1% chance to cause +1% extra damage."
+ local critChanceTooltips =
+ "Critical Hits deal more damage than normal attacks. They have a chance to be \ntriggered during combat, inflicting additional damage beyond the standard amount."
+ local critDamageTooltips = "You get +1% of the damage dealt as mana"
+ local onslaughtTooltips = "You get +1% of the damage dealt as hit points"
+ skillsWindow:recursiveGetChildById("criticalHit"):setVisible(true)
+ setSkillValueWithTooltips('lifeLeech', lifeLeech, lifeLeechTooltips, true)
+ setSkillValueWithTooltips('manaLeech', manaLeech, manaLeechTooltips, true)
+ setSkillValueWithTooltips('criticalChance', critChance, critChanceTooltips, true)
+ setSkillValueWithTooltips('criticalExtraDamage', critDamage, critDamageTooltips, true)
+ setSkillValueWithTooltips('onslaught', onslaught, onslaughtTooltips, true)
+end
+
+local combatIdToWidgetId = {
+ [0] = "physicalResist",
+ [1] = "fireResist",
+ [2] = "earthResist",
+ [3] = "energyResist",
+ [4] = "IceResist",
+ [5] = "HolyResist",
+ [6] = "deathResist",
+ [7] = "HealingResist",
+ [8] = "drowResist",
+ [9] = "lifedrainResist",
+ [10] = "manadRainResist"
+}
+
+function onCombatAbsorbValuesChange(localPlayer, absorbValues)
+ for id, widgetId in pairs(combatIdToWidgetId) do
+ local skill = skillsWindow:recursiveGetChildById(widgetId)
+ if skill then
+ local value = absorbValues[id]
+ if value then
+ setSkillValueWithTooltips(widgetId, value, false, true, "#44AD25")
+ else
+ skill:hide()
+ end
+ end
+ end
+end
+function onDefenseInfoChange(localPlayer, defense, armor, mitigation, dodge, damageReflection)
+ skillsWindow:recursiveGetChildById("separadorOnDefenseInfoChange"):setVisible(true)
+ local defenseToolstip =
+ "When attacked, you have a +9.6% chance to trigger Dodge, which \nwill fully mitigate the damage."
+ local armorToolstip =
+ "Mitigation reduces most of the damage you take and varies based\non your shielding skill, equipped weapon, chosen combat tactics \nand any mitigation multipliers acquired in your Wheel of Destiny."
+ local mitigationToolstip = "This shows how well your armor protects you from all physical\nattacks."
+ local dodgetToolstip =
+ "This is your protection against all physical attacks in close combat \nas well as all distance physical attacks. The higher the defence value, the less damage you will take from melee physical hits. The defence\n value is calculated from your shield and/or weapon\n defence and the corresponding skill. Careful! \nYour defence value protects you only from hits of two creatures in a single round."
+ setSkillValueWithTooltips('defenceValue', defense, defenseToolstip, false)
+ setSkillValueWithTooltips('armorValue', armor, armorToolstip, false)
+ setSkillValueWithTooltips('mitigation', mitigation, mitigationToolstip, true)
+ setSkillValueWithTooltips('dodge', dodge, dodgetToolstip, true)
+ setSkillValueWithTooltips('damageReflection', damageReflection, false, true)
+
+end
+
+function onForgeBonusesChange(localPlayer, momentum, transcendence, amplification)
+ skillsWindow:recursiveGetChildById("separadorOnForgeBonusesChange"):setVisible(true)
+ local momentumTooltip = "During combat, you have a +" .. math.floor(momentum * 10000) / 100 ..
+ "% chance to trigger Momentum\n, which reduces all spell cooldowns by 2 seconds."
+
+ local transcendenceTooltip = "During combat, you have a +" .. math.floor(transcendence * 10000) / 100 ..
+ "% chance to trigger\nTranscendence, which transforms your character into a vocation-\nspecific avatar for 7 seconds. " ..
+ "While in this form, you will benefit\nfrom a 15% damage reduction and guaranteed critical hits that \ndeal an additional 15% damage."
+
+ local amplificationTooltip =
+ "Effects of tiered items are amplified by +" .. math.floor(amplification * 10000) / 100 .. "%."
+
+ setSkillValueWithTooltips('momentum', momentum, momentumTooltip, true)
+ setSkillValueWithTooltips('transcendence', transcendence, transcendenceTooltip, true)
+ setSkillValueWithTooltips('amplification', amplification, amplificationTooltip, true)
+end
diff --git a/modules/game_skills/skills.otui b/modules/game_skills/skills.otui
index eb66b77821..3d9b162e02 100644
--- a/modules/game_skills/skills.otui
+++ b/modules/game_skills/skills.otui
@@ -13,6 +13,7 @@ SkillNameLabel < GameLabel
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
+ color: #C0C0C0
SkillValueLabel < GameLabel
id: value
@@ -22,6 +23,7 @@ SkillValueLabel < GameLabel
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: prev.left
+ color: #C0C0C0
ImageSkill < UIWidget
id: icon
@@ -32,6 +34,15 @@ ImageSkill < UIWidget
anchors.top: parent.top
phantom: false
+ImageAbsorb < UIWidget
+ id: icon
+ size: 9 9
+ width: 9
+ margin-top: 2
+ anchors.right: parent.right
+ anchors.top: parent.top
+ phantom: false
+
RedPercentPanel < ProgressBar
id: percent
background-color: red
@@ -53,6 +64,27 @@ SkillPercentPanel < ProgressBar
anchors.top: parent.top
phantom: false
+XpBoostButton < UIWidget
+ height: 30
+ HorizontalSeparator
+ id: leftSeparator
+ anchors.top: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ margin-top: -23
+ margin-left: 5
+ UIWidget
+ id: xpBoostButton
+ image-source: /images/ui/button-storexp
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: prev.bottom
+ margin-top : -7
+ @onClick: |
+ modules.game_store.toggle()
+ g_game.sendRequestStorePremiumBoost()
+ $pressed:
+ image-source: /images/ui/button-storexp-pressed
+
MiniWindow
id: skillWindow
!text: tr('Skills')
@@ -69,20 +101,31 @@ MiniWindow
SkillButton
margin-top: 5
+ id: level
+ SkillNameLabel
+ !text: tr('Level')
+ SkillValueLabel
+ RedPercentPanel
+
+ SkillButton
id: experience
height: 15
SkillNameLabel
- !text: tr('Experience')
+ !text: tr('XP')
SkillValueLabel
SkillButton
- id: level
+ id: xpGainRate
+ height: 15
SkillNameLabel
- !text: tr('Level')
+ !text: tr('XP Gain Rate')
SkillValueLabel
- RedPercentPanel
+
+ XpBoostButton
+ id: xpBoos
SkillButton
+ margin-top: -12
id: health
height: 15
SkillNameLabel
@@ -119,6 +162,7 @@ MiniWindow
SkillButton
id: regenerationTime
+ height: 15
SkillNameLabel
!text: tr('Regeneration Time')
SkillValueLabel
@@ -137,6 +181,10 @@ MiniWindow
SkillValueLabel
RedPercentPanel
+ HorizontalSeparator
+ margin-top: 5
+ margin-bottom: 5
+
SkillButton
id: magiclevel
SkillNameLabel
@@ -281,3 +329,251 @@ MiniWindow
SkillNameLabel
!text: tr('Transcendence')
SkillValueLabel
+
+// 14.12
+ SmallSkillButton
+ id: damageHealing
+ visible: false
+ SkillNameLabel
+ !text: tr('Damage/Healing')
+ SkillValueLabel
+
+ SmallSkillButton
+ id: attackValue
+ visible: false
+ SkillNameLabel
+ !text: tr('Attack Value')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+
+ SmallSkillButton
+ visible: false
+ id: convertedDamage
+ SkillNameLabel
+ !text: tr('converted Damage')
+ SkillValueLabel
+ SmallSkillButton
+ visible: false
+ id: convertedElement
+ SkillNameLabel
+ !text: tr('converted Element')
+ SkillValueLabel
+
+ SmallSkillButton
+ id: lifeLeech
+ visible: false
+ SkillNameLabel
+ !text: tr('Life Leech')
+ SkillValueLabel
+ SmallSkillButton
+ id: manaLeech
+ visible: false
+ SkillNameLabel
+ !text: tr('Mana Leech')
+ SkillValueLabel
+ SmallSkillButton
+ visible: false
+ id: criticalHit
+ SkillNameLabel
+ !text: tr('Critical Hit:')
+ SmallSkillButton
+ id: criticalChance
+ visible: false
+ SkillNameLabel
+ margin-left: 15
+ !text: tr('Chance')
+ SkillValueLabel
+ SmallSkillButton
+ id: criticalExtraDamage
+ visible: false
+ SkillNameLabel
+ margin-left: 15
+ !text: tr('Extra Damage')
+ SkillValueLabel
+
+ SmallSkillButton
+ id: onslaught
+ visible: false
+ SkillNameLabel
+ !text: tr('Onslaught')
+ SkillValueLabel
+ HorizontalSeparator
+ id: separadorOnDefenseInfoChange
+ margin-top: 5
+ visible: false
+ margin-bottom: 5
+
+ SmallSkillButton
+ id: physicalResist
+ visible: false
+ SkillNameLabel
+ !text: tr('Physical')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-physical-resist
+ SmallSkillButton
+ id: fireResist
+ visible: false
+ SkillNameLabel
+ !text: tr('Fire')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-fire-resist
+
+ SmallSkillButton
+ id: earthResist
+ visible: false
+ SkillNameLabel
+ !text: tr('Earth')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-earth-resist
+ SmallSkillButton
+ id: energyResist
+ visible: false
+ SkillNameLabel
+ !text: tr('Energy')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-energy-resist
+ SmallSkillButton
+ id: IceResist
+ visible: false
+ SkillNameLabel
+ !text: tr('Ice')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-ice-resist
+ SmallSkillButton
+ id: HolyResist
+ visible: false
+ SkillNameLabel
+ !text: tr('Holy')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-holy-resist
+
+ SmallSkillButton
+ id: deathResist
+ visible: false
+ SkillNameLabel
+ !text: tr('Death')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-death-resist
+ SmallSkillButton
+ id: HealingResist
+ visible: false
+ SkillNameLabel
+ !text: tr('Healing')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-healing-resist
+
+ SmallSkillButton
+ id: drowResist
+ visible: false
+ SkillNameLabel
+ !text: tr('Drown')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-drowning-resist
+ SmallSkillButton
+ id: lifedrainResist
+ visible: false
+ SkillNameLabel
+ !text: tr('Lifedrain')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-lifedrain-resist
+ SmallSkillButton
+ id: manadRainResist
+ visible: false
+ SkillNameLabel
+ !text: tr('manadrain')
+ SkillValueLabel
+ margin-right: 12
+ ImageAbsorb
+ image-source: /game_cyclopedia/images/bestiary/icons/monster-icon-manadrain-resist
+
+
+
+
+
+ SmallSkillButton
+ id: defenceValue
+ visible: false
+ SkillNameLabel
+ !text: tr('Defence Value')
+ SkillValueLabel
+
+ SmallSkillButton
+ id: armorValue
+ visible: false
+ SkillNameLabel
+ !text: tr('Armor Value')
+ SkillValueLabel
+
+ SmallSkillButton
+ id: mitigation
+ visible: false
+ SkillNameLabel
+ !text: tr('Mitigation')
+ SkillValueLabel
+ SmallSkillButton
+ id: dodge
+ visible: false
+ SkillNameLabel
+ !text: tr('Dodge')
+ SkillValueLabel
+ SmallSkillButton
+ id: damageReflection
+ visible: false
+ SkillNameLabel
+ !text: tr('Damage Reflection')
+ SkillValueLabel
+
+ HorizontalSeparator
+ id: separadorOnForgeBonusesChange
+ visible: false
+ margin-top: 5
+ margin-bottom: 5
+
+ SmallSkillButton
+ id: momentum
+ visible: false
+ SkillNameLabel
+ !text: tr('Momentum')
+ SkillValueLabel
+ SmallSkillButton
+ id: transcendence
+ visible: false
+ SkillNameLabel
+ !text: tr('Transcendence')
+ SkillValueLabel
+ SmallSkillButton
+ id: amplification
+ visible: false
+ SkillNameLabel
+ !text: tr('Amplification')
+ SkillValueLabel
+
+ UIWidget
+ id: miniborder
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ image-source: /images/ui/miniborder
+ size: 14 14
+ margin-left: 2
+ margin-bottom: 2
\ No newline at end of file
diff --git a/modules/gamelib/const.lua b/modules/gamelib/const.lua
index dd167145f9..b21ef4619b 100644
--- a/modules/gamelib/const.lua
+++ b/modules/gamelib/const.lua
@@ -218,6 +218,8 @@ GameAllowPreWalk = 122
GamePlayerFamiliars = 123
GameLatencyAdaptiveCamera = 124
GameMapCache = 125
+GameForgeSkillStats = 126
+GameCharacterSkillStats = 127
TextColors = {
red = '#f55e5e', -- '#c83200'
@@ -417,10 +419,22 @@ ChannelEvent = {
ResourceTypes = {
BANK_BALANCE = 0,
GOLD_EQUIPPED = 1,
+ CURRENCY_CUSTOM_EQUIPPED = 2,
PREY_WILDCARDS = 10,
DAILYREWARD_STREAK = 20,
DAILYREWARD_JOKERS = 21,
+ CHARM = 30,
+ MINOR_CHARM = 31,
+ MAX_CHARM = 32,
+ MAX_MINOR_CHARM = 33,
TASK_HUNTING = 50,
+ FORGE_DUST = 70,
+ FORGE_SLIVER = 71,
+ FORGE_CORES = 72,
+ LESSER_GEMS = 81,
+ REGULAR_GEMS = 82,
+ GREATER_GEMS = 83,
+ WHEEL_OF_DESTINY = 86,
COIN_NORMAL = 90,
COIN_TRANSFERRABLE = 91,
COIN_AUCTION = 92,
@@ -439,10 +453,22 @@ CyclopediaCharacterInfoTypes = {
StoreSummary = 8,
Ispection = 9,
Badges = 10,
- Titles = 11
+ Titles = 11,
+ Wheel = 12,
+ Offencestats = 13,
+ Defencestats = 14,
+ Miscstats = 15
}
StoreConst = {
InstantRewardAccess = 233,
}
+
+ExperienceRate = {
+ BASE = 0,
+ VOUCHER = 1,
+ LOW_LEVEL = 2,
+ XP_BOOST = 3,
+ STAMINA_MULTIPLIER = 4
+}
-- @}
diff --git a/modules/gamelib/game.lua b/modules/gamelib/game.lua
index db48a69931..dff27eca02 100644
--- a/modules/gamelib/game.lua
+++ b/modules/gamelib/game.lua
@@ -58,7 +58,7 @@ function g_game.getSupportedClients()
1012, 1013, 1020, 1021, 1022, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041, 1050,
1051, 1052, 1053, 1054, 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1070, 1071, 1072, 1073,
1074, 1075, 1076, 1080, 1081, 1082, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1281, 1285,
- 1286, 1287, 1291, 1300, 1310, 1311, 1314, 1316, 1320, 1321, 1322, 1332, 1334, 1336, 1337, 1340}
+ 1286, 1287, 1291, 1300, 1310, 1311, 1314, 1316, 1320, 1321, 1322, 1332, 1334, 1336, 1337, 1340, 1400, 1405, 1410, 1412}
end
-- The client version and protocol version where
diff --git a/modules/gamelib/player.lua b/modules/gamelib/player.lua
index 66938f5f45..aff91e1f0b 100644
--- a/modules/gamelib/player.lua
+++ b/modules/gamelib/player.lua
@@ -69,6 +69,32 @@ Icons[PlayerStates.Agony] = { clip = 28, tooltip = tr('You are Agony'), id = 'c
Icons[PlayerStates.Rewards] = { clip = 30, tooltip = tr('Rewards'), id = 'condition_Rewards' }
Icons[PlayerStates.Hungry] = { clip = 32, tooltip = tr('You are hungry'), id = 'condition_hungry' }
+combatStates= {
+ CLIENT_COMBAT_PHYSICAL = 0,
+ CLIENT_COMBAT_FIRE = 1,
+ CLIENT_COMBAT_EARTH = 2,
+ CLIENT_COMBAT_ENERGY = 3,
+ CLIENT_COMBAT_ICE = 4,
+ CLIENT_COMBAT_HOLY = 5,
+ CLIENT_COMBAT_DEATH = 6,
+ CLIENT_COMBAT_HEALING = 7,
+ CLIENT_COMBAT_DROWN = 8,
+ CLIENT_COMBAT_LIFEDRAIN = 9,
+ CLIENT_COMBAT_MANADRAIN = 10,
+}
+clientCombat ={}
+clientCombat[combatStates.CLIENT_COMBAT_PHYSICAL] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-physical-resist', id = 'Physical' }
+clientCombat[combatStates.CLIENT_COMBAT_FIRE] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-fire-resist', id = 'Fire' }
+clientCombat[combatStates.CLIENT_COMBAT_EARTH] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-earth-resist', id = 'Earth' }
+clientCombat[combatStates.CLIENT_COMBAT_ENERGY] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-energy-resist', id = 'Energy' }
+clientCombat[combatStates.CLIENT_COMBAT_ICE] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-ice-resist', id = 'Ice' }
+clientCombat[combatStates.CLIENT_COMBAT_HOLY] = {path = '/game_cyclopedia/images/bestiary/icons/monster-icon-holy-resist', id = 'Holy' }
+clientCombat[combatStates.CLIENT_COMBAT_DEATH] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-death-resist', id = 'Death' }
+clientCombat[combatStates.CLIENT_COMBAT_HEALING] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-healing-resist', id = 'Healing' }
+clientCombat[combatStates.CLIENT_COMBAT_DROWN] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-drowning-resist', id = 'Drown' }
+clientCombat[combatStates.CLIENT_COMBAT_LIFEDRAIN] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-lifedrain-resist', id = 'Lifedrain ' }
+clientCombat[combatStates.CLIENT_COMBAT_MANADRAIN] = { path = '/game_cyclopedia/images/bestiary/icons/monster-icon-manadrain-resist', id = 'Manadrain' }
+
InventorySlotOther = 0
InventorySlotHead = 1
InventorySlotNeck = 2
diff --git a/modules/gamelib/util.lua b/modules/gamelib/util.lua
index 1bfb2bbba8..ec778ba2c3 100644
--- a/modules/gamelib/util.lua
+++ b/modules/gamelib/util.lua
@@ -14,3 +14,18 @@ function comma_value(n)
local left, num, right = string.match(n, '^([^%d]*%d)(%d*)(.-)$')
return left .. (num:reverse():gsub('(%d%d%d)', '%1,'):reverse()) .. right
end
+
+function formatTimeBySeconds(totalSeconds)
+ local hours = math.floor(totalSeconds / 3600)
+ local remainingSeconds = totalSeconds % 3600
+ local minutes = math.floor(remainingSeconds / 60)
+ return string.format("%02d:%02d", hours, minutes)
+end
+
+function formatTimeByMinutes(totalMinutes)
+ local totalSeconds = totalMinutes * 60
+ local hours = math.floor(totalSeconds / 3600)
+ local remainingSeconds = totalSeconds % 3600
+ local minutes = math.floor(remainingSeconds / 60)
+ return string.format("%02d:%02d", hours, minutes)
+end
diff --git a/src/client/const.h b/src/client/const.h
index d44fdf978f..87bdfcdfa3 100644
--- a/src/client/const.h
+++ b/src/client/const.h
@@ -560,6 +560,8 @@ namespace Otc
GamePlayerFamiliars = 123,
GameLatencyAdaptiveCamera = 124,
GameMapCache = 125,
+ GameForgeSkillStats = 126,
+ GameCharacterSkillStats = 127,
LastGameFeature
};
@@ -673,6 +675,10 @@ namespace Otc
RESOURCE_PREY_WILDCARDS = 10,
RESOURCE_DAILYREWARD_STREAK = 20,
RESOURCE_DAILYREWARD_JOKERS = 21,
+ RESOURCE_CHARM = 30,
+ RESOURCE_MINOR_CHARM = 31,
+ RESOURCE_MAX_CHARM = 32,
+ RESOURCE_MAX_MINOR_CHARM = 33,
RESOURCE_TASK_HUNTING = 50,
RESOURCE_FORGE_DUST = 70,
RESOURCE_FORGE_SLIVER = 71,
@@ -687,6 +693,15 @@ namespace Otc
RESOURE_COIN_TOURNAMENT = 93,
};
+ enum ExperienceRate_t : uint8_t
+ {
+ EXP_BASE = 0,
+ EXP_VOUCHER = 1,
+ EXP_LOWLEVEL = 2,
+ EXP_XPBOOST = 3,
+ EXP_STANINAMULTIPLIER = 4
+ };
+
enum MarketItemDescription : uint8_t
{
ITEM_DESC_ARMOR = 1,
@@ -783,7 +798,11 @@ namespace Otc
CYCLOPEDIA_CHARACTERINFO_STORESUMMARY = 8,
CYCLOPEDIA_CHARACTERINFO_INSPECTION = 9,
CYCLOPEDIA_CHARACTERINFO_BADGES = 10,
- CYCLOPEDIA_CHARACTERINFO_TITLES = 11
+ CYCLOPEDIA_CHARACTERINFO_TITLES = 11,
+ CYCLOPEDIA_CHARACTERINFO_WHEEL = 12,
+ CYCLOPEDIA_CHARACTERINFO_OFFENCESTATS = 13,
+ CYCLOPEDIA_CHARACTERINFO_DEFENCESTATS = 14,
+ CYCLOPEDIA_CHARACTERINFO_MISCSTATS = 15
};
enum InspectObjectTypes : uint8_t
diff --git a/src/client/game.cpp b/src/client/game.cpp
index d6af7392b4..52974b26ec 100644
--- a/src/client/game.cpp
+++ b/src/client/game.cpp
@@ -507,11 +507,6 @@ void Game::processItemDetail(const uint32_t itemId, const std::vector& bestiaryRaces)
-{
- g_lua.callGlobalField("g_game", "onParseBestiaryRaces", bestiaryRaces);
-}
-
void Game::processCyclopediaCharacterGeneralStats(const CyclopediaCharacterGeneralStats& stats, const std::vector>& skills,
const std::vector>& combats)
{
@@ -1999,3 +1994,19 @@ void Game::sendRequestTrackerQuestLog(const std::map& que
m_protocolGame->sendRequestTrackerQuestLog(quests);
}
+
+void Game::processCyclopediaCharacterOffenceStats(const CyclopediaCharacterOffenceStats& data)
+{
+ g_lua.callGlobalField("g_game", "onCyclopediaCharacterOffenceStats", data);
+}
+
+void Game::processCyclopediaCharacterDefenceStats(const CyclopediaCharacterDefenceStats& data)
+{
+ g_lua.callGlobalField("g_game", "onCyclopediaCharacterDefenceStats", data);
+}
+
+void Game::processCyclopediaCharacterMiscStats(const CyclopediaCharacterMiscStats& data)
+{
+ g_lua.callGlobalField("g_game", "onCyclopediaCharacterMiscStats", data);
+}
+
diff --git a/src/client/game.h b/src/client/game.h
index a43501c104..c5ebd1de3c 100644
--- a/src/client/game.h
+++ b/src/client/game.h
@@ -241,10 +241,15 @@ struct CharmData
bool asignedStatus;
uint16_t raceId;
uint32_t removeRuneCost;
+ uint8_t availableCharmSlots;
+ uint8_t tier;
};
struct BestiaryCharmsData
{
+ uint64_t resetAllCharmsCost;
+ uint8_t availableCharmSlots;
+
uint32_t points;
std::vector charms;
std::vector finishedMonsters;
@@ -450,6 +455,111 @@ struct DailyRewardData
uint8_t maxUnlockableDragons;
};
+struct CyclopediaCharacterOffenceStats
+{
+ double critChance;
+ double critDamage;
+ double critDamageBase;
+ double critDamageImbuement;
+ double critDamageWheel;
+
+ double lifeLeech;
+ double lifeLeechBase;
+ double lifeLeechImbuement;
+ double lifeLeechWheel;
+
+ double manaLeech;
+ double manaLeechBase;
+ double manaLeechImbuement;
+ double manaLeechWheel;
+
+ double onslaught;
+ double onslaughtBase;
+ double onslaughtBonus;
+
+ double cleavePercent;
+
+ std::vector perfectShotDamage;
+
+ uint16_t flatDamage;
+ uint16_t flatDamageBase;
+
+ uint16_t weaponAttack;
+ uint16_t weaponFlatModifier;
+ uint16_t weaponDamage;
+ uint8_t weaponSkillType;
+ uint16_t weaponSkillLevel;
+ uint16_t weaponSkillModifier;
+ uint8_t weaponElement;
+ double weaponElementDamage;
+ uint8_t weaponElementType;
+ std::vector weaponAccuracy;
+};
+
+struct CyclopediaCharacterDefenceStats
+{
+ double dodgeTotal;
+ double dodgeBase;
+ double dodgeBonus;
+ double dodgeWheel;
+
+ uint32_t magicShieldCapacity;
+ uint16_t magicShieldCapacityFlat;
+ double magicShieldCapacityPercent;
+
+ uint16_t reflectPhysical;
+ uint16_t armor;
+
+ uint16_t defense;
+ uint16_t defenseEquipment;
+ uint8_t defenseSkillType;
+ uint16_t shieldingSkill;
+ uint16_t defenseWheel;
+
+ double mitigation;
+ double mitigationBase;
+ double mitigationEquipment;
+ double mitigationShield;
+ double mitigationWheel;
+ double mitigationCombatTactics;
+
+ struct ElementalResistance
+ {
+ uint8_t element;
+ double value;
+ };
+
+ std::vector resistances;
+};
+
+struct CyclopediaCharacterMiscStats
+{
+ double momentumTotal;
+ double momentumBase;
+ double momentumBonus;
+ double momentumWheel;
+
+ double dodgeTotal;
+ double dodgeBase;
+ double dodgeBonus;
+ double dodgeWheel;
+
+ double damageReflectionTotal;
+ double damageReflectionBase;
+ double damageReflectionBonus;
+
+ uint8_t haveBlesses;
+ uint8_t totalBlesses;
+
+ struct Concoction
+ {
+ uint16_t id;
+ uint32_t duration;
+ };
+
+ std::vector concoctions;
+};
+
//@bindsingleton g_game
class Game
{
@@ -555,7 +665,6 @@ class Game
// cyclopedia
static void processItemDetail(uint32_t itemId, const std::vector>& descriptions);
- static void processBestiaryRaces(const std::vector& bestiaryRaces);
static void processCyclopediaCharacterGeneralStats(const CyclopediaCharacterGeneralStats& stats, const std::vector>& skills,
const std::vector>& combats);
static void processCyclopediaCharacterCombatStats(const CyclopediaCharacterCombatStats& data, double mitigation,
@@ -841,6 +950,9 @@ class Game
void requestOpenRewardHistory();
void requestGetRewardDaily(const uint8_t bonusShrine, const std::map& items);
void sendRequestTrackerQuestLog(const std::map& quests);
+ void processCyclopediaCharacterOffenceStats(const CyclopediaCharacterOffenceStats& data);
+ void processCyclopediaCharacterDefenceStats(const CyclopediaCharacterDefenceStats& data);
+ void processCyclopediaCharacterMiscStats(const CyclopediaCharacterMiscStats& data);
void updateMapLatency() {
if (!m_mapUpdateTimer.first) {
diff --git a/src/client/localplayer.cpp b/src/client/localplayer.cpp
index 7c49f4c10c..046ac3926b 100644
--- a/src/client/localplayer.cpp
+++ b/src/client/localplayer.cpp
@@ -514,4 +514,129 @@ void LocalPlayer::setResourceBalance(const Otc::ResourceTypes_t type, const uint
bool LocalPlayer::hasSight(const Position& pos)
{
return m_position.isInRange(pos, g_map.getAwareRange().left - 1, g_map.getAwareRange().top - 1);
-}
\ No newline at end of file
+}
+
+void LocalPlayer::setFlatDamageHealing(uint16_t flatBonus)
+{
+ if (m_flatDamageHealing == flatBonus)
+ return;
+
+ const uint16_t oldFlatBonus = m_flatDamageHealing;
+ m_flatDamageHealing = flatBonus;
+
+ callLuaField("onFlatDamageHealingChange", flatBonus);
+}
+
+void LocalPlayer::setAttackInfo(uint16_t attackValue, uint8_t attackElement)
+{
+ if (m_attackValue == attackValue && m_attackElement == attackElement)
+ return;
+
+ const uint16_t oldAttackValue = m_attackValue;
+ const uint8_t oldAttackElement = m_attackElement;
+ m_attackValue = attackValue;
+ m_attackElement = attackElement;
+
+ callLuaField("onAttackInfoChange", attackValue, attackElement);
+}
+
+void LocalPlayer::setConvertedDamage(double convertedDamage, uint8_t convertedElement)
+{
+ if (m_convertedDamage == convertedDamage && m_convertedElement == convertedElement)
+ return;
+
+ const double oldConvertedDamage = m_convertedDamage;
+ const uint8_t oldConvertedElement = m_convertedElement;
+ m_convertedDamage = convertedDamage;
+ m_convertedElement = convertedElement;
+
+ callLuaField("onConvertedDamageChange", convertedDamage, convertedElement);
+}
+
+void LocalPlayer::setImbuements(double lifeLeech, double manaLeech, double critChance, double critDamage, double onslaught)
+{
+ if (m_lifeLeech == lifeLeech && m_manaLeech == manaLeech && m_critChance == critChance &&
+ m_critDamage == critDamage && m_onslaught == onslaught)
+ return;
+
+ const double oldLifeLeech = m_lifeLeech;
+ const double oldManaLeech = m_manaLeech;
+ const double oldCritChance = m_critChance;
+ const double oldCritDamage = m_critDamage;
+ const double oldOnslaught = m_onslaught;
+
+ m_lifeLeech = lifeLeech;
+ m_manaLeech = manaLeech;
+ m_critChance = critChance;
+ m_critDamage = critDamage;
+ m_onslaught = onslaught;
+
+ callLuaField("onImbuementsChange", lifeLeech, manaLeech, critChance, critDamage, onslaught);
+}
+
+void LocalPlayer::setDefenseInfo(uint16_t defense, uint16_t armor, double mitigation, double dodge, uint16_t damageReflection)
+{
+ if (m_defense == defense && m_armor == armor && m_mitigation == mitigation &&
+ m_dodge == dodge && m_damageReflection == damageReflection)
+ return;
+
+ const uint16_t oldDefense = m_defense;
+ const uint16_t oldArmor = m_armor;
+ const double oldMitigation = m_mitigation;
+ const double oldDodge = m_dodge;
+ const uint16_t oldDamageReflection = m_damageReflection;
+
+ m_defense = defense;
+ m_armor = armor;
+ m_mitigation = mitigation;
+ m_dodge = dodge;
+ m_damageReflection = damageReflection;
+
+ callLuaField("onDefenseInfoChange", defense, armor, mitigation, dodge, damageReflection);
+}
+
+void LocalPlayer::setCombatAbsorbValues(const std::map& absorbValues)
+{
+ if (m_combatAbsorbValues == absorbValues)
+ return;
+
+ const auto oldAbsorbValues = m_combatAbsorbValues;
+ m_combatAbsorbValues = absorbValues;
+
+ callLuaField("onCombatAbsorbValuesChange", absorbValues);
+}
+
+void LocalPlayer::setForgeBonuses(double momentum, double transcendence, double amplification)
+{
+ if (m_momentum == momentum && m_transcendence == transcendence && m_amplification == amplification)
+ return;
+
+ const double oldMomentum = m_momentum;
+ const double oldTranscendence = m_transcendence;
+ const double oldAmplification = m_amplification;
+
+ m_momentum = momentum;
+ m_transcendence = transcendence;
+ m_amplification = amplification;
+
+ callLuaField("onForgeBonusesChange", momentum, transcendence, amplification);
+}
+
+void LocalPlayer::setExperienceRate(Otc::ExperienceRate_t type, uint16_t value)
+{
+ if (m_experienceRates[type] == value)
+ return;
+
+ const uint16_t oldValue = m_experienceRates[type];
+ m_experienceRates[type] = value;
+
+ callLuaField("onExperienceRateChange", type, value);
+}
+
+void LocalPlayer::setStoreExpBoostTime(uint16_t value)
+{
+ if (m_storeExpBoostTime == value)
+ return;
+
+ m_storeExpBoostTime = value;
+}
diff --git a/src/client/localplayer.h b/src/client/localplayer.h
index ef6a30eaa3..f364cbdb5f 100644
--- a/src/client/localplayer.h
+++ b/src/client/localplayer.h
@@ -60,6 +60,15 @@ class LocalPlayer final : public Player
void setBlessings(uint16_t blessings);
void setResourceBalance(Otc::ResourceTypes_t type, uint64_t value);
void takeScreenshot(uint8_t type);
+ void setFlatDamageHealing(uint16_t flatBonus);
+ void setAttackInfo(uint16_t attackValue, uint8_t attackElement);
+ void setConvertedDamage(double convertedDamage, uint8_t convertedElement);
+ void setImbuements(double lifeLeech, double manaLeech, double critChance, double critDamage, double onslaught);
+ void setDefenseInfo(uint16_t defense, uint16_t armor, double mitigation, double dodge, uint16_t damageReflection);
+ void setCombatAbsorbValues(const std::map& absorbValues);
+ void setForgeBonuses(double momentum, double transcendence, double amplification);
+ void setExperienceRate(Otc::ExperienceRate_t type, uint16_t value);
+ void setStoreExpBoostTime(uint16_t value);
uint32_t getFreeCapacity() { return m_freeCapacity; }
uint32_t getTotalCapacity() { return m_totalCapacity; }
@@ -79,6 +88,7 @@ class LocalPlayer final : public Player
uint16_t getBlessings() { return m_blessings; }
uint16_t getRegenerationTime() { return m_regenerationTime; }
uint16_t getOfflineTrainingTime() { return m_offlineTrainingTime; }
+ uint16_t getStoreExpBoostTime() { return m_offlineTrainingTime; }
uint32_t getStates() { return m_states; }
uint32_t getMana() { return m_mana; }
@@ -161,6 +171,8 @@ class LocalPlayer final : public Player
std::vector m_spells;
stdext::map m_resourcesBalance;
+ std::map m_combatAbsorbValues;
+ std::map m_experienceRates;
uint8_t m_autoWalkRetries{ 0 };
@@ -185,6 +197,28 @@ class LocalPlayer final : public Player
uint16_t m_stamina{ 0 };
uint16_t m_regenerationTime{ 0 };
uint16_t m_offlineTrainingTime{ 0 };
+ uint16_t m_storeExpBoostTime{ 0 };
+
+ uint8_t m_attackElement{ 0 };
+ uint8_t m_convertedElement{ 0 };
+
+ uint16_t m_flatDamageHealing{ 0 };
+ uint16_t m_attackValue{ 0 };
+ uint16_t m_defense{ 0 };
+ uint16_t m_armor{ 0 };
+ uint16_t m_damageReflection{ 0 };
+
+ double m_convertedDamage{ 0 };
+ double m_lifeLeech{ 0 };
+ double m_manaLeech{ 0 };
+ double m_critChance{ 0 };
+ double m_critDamage{ 0 };
+ double m_onslaught{ 0 };
+ double m_mitigation{ 0 };
+ double m_dodge{ 0 };
+ double m_momentum{ 0 };
+ double m_transcendence{ 0 };
+ double m_amplification{ 0 };
friend class Game;
};
diff --git a/src/client/luafunctions.cpp b/src/client/luafunctions.cpp
index f55b51200f..4ba1c823a3 100644
--- a/src/client/luafunctions.cpp
+++ b/src/client/luafunctions.cpp
@@ -863,6 +863,7 @@ void Client::registerLuaFunctions()
g_lua.bindClassMemberFunction("getSoul", &LocalPlayer::getSoul);
g_lua.bindClassMemberFunction("getStamina", &LocalPlayer::getStamina);
g_lua.bindClassMemberFunction("getOfflineTrainingTime", &LocalPlayer::getOfflineTrainingTime);
+ g_lua.bindClassMemberFunction("getStoreExpBoostTime", &LocalPlayer::getStoreExpBoostTime);
g_lua.bindClassMemberFunction("getRegenerationTime", &LocalPlayer::getRegenerationTime);
g_lua.bindClassMemberFunction("getBaseMagicLevel", &LocalPlayer::getBaseMagicLevel);
g_lua.bindClassMemberFunction("getTotalCapacity", &LocalPlayer::getTotalCapacity);
diff --git a/src/client/luavaluecasts_client.cpp b/src/client/luavaluecasts_client.cpp
index e6d9c667bc..58bf5a8754 100644
--- a/src/client/luavaluecasts_client.cpp
+++ b/src/client/luavaluecasts_client.cpp
@@ -813,7 +813,7 @@ int push_luavalue(const BestiaryMonsterData& data) {
}
int push_luavalue(const CharmData& charm) {
- g_lua.createTable(0, 7);
+ g_lua.createTable(0, 10);
g_lua.pushInteger(charm.id);
g_lua.setField("id");
g_lua.pushString(charm.name);
@@ -830,6 +830,13 @@ int push_luavalue(const CharmData& charm) {
g_lua.setField("raceId");
g_lua.pushInteger(charm.removeRuneCost);
g_lua.setField("removeRuneCost");
+ //if (g_game.getClientVersion() >= 1410) {
+ g_lua.pushInteger(charm.availableCharmSlots);
+ g_lua.setField("availableCharmSlots");
+ g_lua.pushInteger(charm.tier);
+ g_lua.setField("tier");
+ // }
+
return 1;
}
@@ -1283,3 +1290,243 @@ int push_luavalue(const DailyRewardData& data) {
return 1;
}
+
+int push_luavalue(const CyclopediaCharacterOffenceStats& data)
+{
+ g_lua.createTable(0, 30);
+
+ g_lua.pushNumber(data.critChance);
+ g_lua.setField("critChance");
+
+ g_lua.pushNumber(data.critDamage);
+ g_lua.setField("critDamage");
+
+ g_lua.pushNumber(data.critDamageBase);
+ g_lua.setField("critDamageBase");
+
+ g_lua.pushNumber(data.critDamageImbuement);
+ g_lua.setField("critDamageImbuement");
+
+ g_lua.pushNumber(data.critDamageWheel);
+ g_lua.setField("critDamageWheel");
+
+ g_lua.pushNumber(data.lifeLeech);
+ g_lua.setField("lifeLeech");
+
+ g_lua.pushNumber(data.lifeLeechBase);
+ g_lua.setField("lifeLeechBase");
+
+ g_lua.pushNumber(data.lifeLeechImbuement);
+ g_lua.setField("lifeLeechImbuement");
+
+ g_lua.pushNumber(data.lifeLeechWheel);
+ g_lua.setField("lifeLeechWheel");
+
+ g_lua.pushNumber(data.manaLeech);
+ g_lua.setField("manaLeech");
+
+ g_lua.pushNumber(data.manaLeechBase);
+ g_lua.setField("manaLeechBase");
+
+ g_lua.pushNumber(data.manaLeechImbuement);
+ g_lua.setField("manaLeechImbuement");
+
+ g_lua.pushNumber(data.manaLeechWheel);
+ g_lua.setField("manaLeechWheel");
+
+ g_lua.pushNumber(data.onslaught);
+ g_lua.setField("onslaught");
+
+ g_lua.pushNumber(data.onslaughtBase);
+ g_lua.setField("onslaughtBase");
+
+ g_lua.pushNumber(data.onslaughtBonus);
+ g_lua.setField("onslaughtBonus");
+
+ g_lua.pushNumber(data.cleavePercent);
+ g_lua.setField("cleavePercent");
+
+ g_lua.createTable(data.perfectShotDamage.size(), 0);
+ for (size_t i = 0; i < data.perfectShotDamage.size(); ++i) {
+ g_lua.pushInteger(data.perfectShotDamage[i]);
+ g_lua.rawSeti(i + 1);
+ }
+ g_lua.setField("perfectShotDamage");
+
+ g_lua.pushInteger(data.flatDamage);
+ g_lua.setField("flatDamage");
+
+ g_lua.pushInteger(data.flatDamageBase);
+ g_lua.setField("flatDamageBase");
+
+ g_lua.pushInteger(data.weaponAttack);
+ g_lua.setField("weaponAttack");
+
+ g_lua.pushInteger(data.weaponFlatModifier);
+ g_lua.setField("weaponFlatModifier");
+
+ g_lua.pushInteger(data.weaponDamage);
+ g_lua.setField("weaponDamage");
+
+ g_lua.pushInteger(data.weaponSkillType);
+ g_lua.setField("weaponSkillType");
+
+ g_lua.pushInteger(data.weaponSkillLevel);
+ g_lua.setField("weaponSkillLevel");
+
+ g_lua.pushInteger(data.weaponSkillModifier);
+ g_lua.setField("weaponSkillModifier");
+
+ g_lua.pushInteger(data.weaponElement);
+ g_lua.setField("weaponElement");
+
+ g_lua.pushNumber(data.weaponElementDamage);
+ g_lua.setField("weaponElementDamage");
+
+ g_lua.pushInteger(data.weaponElementType);
+ g_lua.setField("weaponElementType");
+
+ g_lua.createTable(data.weaponAccuracy.size(), 0);
+ for (size_t i = 0; i < data.weaponAccuracy.size(); ++i) {
+ g_lua.pushNumber(data.weaponAccuracy[i]);
+ g_lua.rawSeti(i + 1);
+ }
+ g_lua.setField("weaponAccuracy");
+
+ return 1;
+}
+
+int push_luavalue(const CyclopediaCharacterDefenceStats& data)
+{
+ g_lua.createTable(0, 20);
+
+ g_lua.pushNumber(data.dodgeTotal);
+ g_lua.setField("dodgeTotal");
+
+ g_lua.pushNumber(data.dodgeBase);
+ g_lua.setField("dodgeBase");
+
+ g_lua.pushNumber(data.dodgeBonus);
+ g_lua.setField("dodgeBonus");
+
+ g_lua.pushNumber(data.dodgeWheel);
+ g_lua.setField("dodgeWheel");
+
+ g_lua.pushInteger(data.magicShieldCapacity);
+ g_lua.setField("magicShieldCapacity");
+
+ g_lua.pushInteger(data.magicShieldCapacityFlat);
+ g_lua.setField("magicShieldCapacityFlat");
+
+ g_lua.pushNumber(data.magicShieldCapacityPercent);
+ g_lua.setField("magicShieldCapacityPercent");
+
+ g_lua.pushInteger(data.reflectPhysical);
+ g_lua.setField("reflectPhysical");
+
+ g_lua.pushInteger(data.armor);
+ g_lua.setField("armor");
+
+ g_lua.pushInteger(data.defense);
+ g_lua.setField("defense");
+
+ g_lua.pushInteger(data.defenseEquipment);
+ g_lua.setField("defenseEquipment");
+
+ g_lua.pushInteger(data.defenseSkillType);
+ g_lua.setField("defenseSkillType");
+
+ g_lua.pushInteger(data.shieldingSkill);
+ g_lua.setField("shieldingSkill");
+
+ g_lua.pushInteger(data.defenseWheel);
+ g_lua.setField("defenseWheel");
+
+ g_lua.pushNumber(data.mitigation);
+ g_lua.setField("mitigation");
+
+ g_lua.pushNumber(data.mitigationBase);
+ g_lua.setField("mitigationBase");
+
+ g_lua.pushNumber(data.mitigationEquipment);
+ g_lua.setField("mitigationEquipment");
+
+ g_lua.pushNumber(data.mitigationShield);
+ g_lua.setField("mitigationShield");
+
+ g_lua.pushNumber(data.mitigationWheel);
+ g_lua.setField("mitigationWheel");
+
+ g_lua.pushNumber(data.mitigationCombatTactics);
+ g_lua.setField("mitigationCombatTactics");
+
+ g_lua.createTable(data.resistances.size(), 0);
+ for (size_t i = 0; i < data.resistances.size(); ++i) {
+ g_lua.createTable(0, 2);
+ g_lua.pushInteger(data.resistances[i].element);
+ g_lua.setField("element");
+ g_lua.pushNumber(data.resistances[i].value);
+ g_lua.setField("value");
+ g_lua.rawSeti(i + 1);
+ }
+ g_lua.setField("resistances");
+
+ return 1;
+}
+
+int push_luavalue(const CyclopediaCharacterMiscStats& data)
+{
+ g_lua.createTable(0, 14);
+
+ g_lua.pushNumber(data.momentumTotal);
+ g_lua.setField("momentumTotal");
+
+ g_lua.pushNumber(data.momentumBase);
+ g_lua.setField("momentumBase");
+
+ g_lua.pushNumber(data.momentumBonus);
+ g_lua.setField("momentumBonus");
+
+ g_lua.pushNumber(data.momentumWheel);
+ g_lua.setField("momentumWheel");
+
+ g_lua.pushNumber(data.dodgeTotal);
+ g_lua.setField("dodgeTotal");
+
+ g_lua.pushNumber(data.dodgeBase);
+ g_lua.setField("dodgeBase");
+
+ g_lua.pushNumber(data.dodgeBonus);
+ g_lua.setField("dodgeBonus");
+
+ g_lua.pushNumber(data.dodgeWheel);
+ g_lua.setField("dodgeWheel");
+
+ g_lua.pushNumber(data.damageReflectionTotal);
+ g_lua.setField("damageReflectionTotal");
+
+ g_lua.pushNumber(data.damageReflectionBase);
+ g_lua.setField("damageReflectionBase");
+
+ g_lua.pushNumber(data.damageReflectionBonus);
+ g_lua.setField("damageReflectionBonus");
+
+ g_lua.pushInteger(data.haveBlesses);
+ g_lua.setField("haveBlesses");
+
+ g_lua.pushInteger(data.totalBlesses);
+ g_lua.setField("totalBlesses");
+
+ g_lua.createTable(data.concoctions.size(), 0);
+ for (size_t i = 0; i < data.concoctions.size(); ++i) {
+ g_lua.createTable(0, 2);
+ g_lua.pushInteger(data.concoctions[i].id);
+ g_lua.setField("id");
+ g_lua.pushInteger(data.concoctions[i].duration);
+ g_lua.setField("duration");
+ g_lua.rawSeti(i + 1);
+ }
+ g_lua.setField("concoctions");
+
+ return 1;
+}
diff --git a/src/client/luavaluecasts_client.h b/src/client/luavaluecasts_client.h
index 47200b2347..f663e5eddb 100644
--- a/src/client/luavaluecasts_client.h
+++ b/src/client/luavaluecasts_client.h
@@ -89,6 +89,9 @@ int push_luavalue(const OutfitColorStruct& currentOutfit);
int push_luavalue(const CharacterInfoOutfits& outfit);
int push_luavalue(const CharacterInfoMounts& mount);
int push_luavalue(const CharacterInfoFamiliar& familiar);
+int push_luavalue(const CyclopediaCharacterOffenceStats& data);
+int push_luavalue(const CyclopediaCharacterDefenceStats& data);
+int push_luavalue(const CyclopediaCharacterMiscStats& data);
// bestiary
int push_luavalue(const RaceType& raceData);
diff --git a/src/client/protocolgame.cpp b/src/client/protocolgame.cpp
index d2f2b9cb4f..b75bd007f1 100644
--- a/src/client/protocolgame.cpp
+++ b/src/client/protocolgame.cpp
@@ -66,7 +66,9 @@ void ProtocolGame::onRecv(const InputMessagePtr& inputMessage)
if (m_firstRecv) {
m_firstRecv = false;
- if (g_game.getFeature(Otc::GameMessageSizeCheck)) {
+ if (g_game.getClientVersion() >= 1405) {
+ const int padding = inputMessage->getU8();
+ } else if (g_game.getFeature(Otc::GameMessageSizeCheck)) {
const int size = inputMessage->getU16();
if (size != inputMessage->getUnreadSize()) {
g_logger.traceError("invalid message size");
diff --git a/src/client/protocolgame.h b/src/client/protocolgame.h
index 62e0a9e3f6..c59ed14a73 100644
--- a/src/client/protocolgame.h
+++ b/src/client/protocolgame.h
@@ -207,7 +207,7 @@ class ProtocolGame final : public Protocol
void parseSessionEnd(const InputMessagePtr& msg);
void parsePing(const InputMessagePtr& msg);
void parsePingBack(const InputMessagePtr& msg);
- void parseChallenge(const InputMessagePtr& msg);
+ void parseLoginChallenge(const InputMessagePtr& msg);
void parseDeath(const InputMessagePtr& msg);
void parseFloorDescription(const InputMessagePtr& msg);
void parseMapDescription(const InputMessagePtr& msg);
diff --git a/src/client/protocolgameparse.cpp b/src/client/protocolgameparse.cpp
index 1229f2e8ad..6bfe00a062 100644
--- a/src/client/protocolgameparse.cpp
+++ b/src/client/protocolgameparse.cpp
@@ -109,7 +109,7 @@ void ProtocolGame::parseMessage(const InputMessagePtr& msg)
}
break;
case Proto::GameServerChallenge:
- parseChallenge(msg);
+ parseLoginChallenge(msg);
break;
case Proto::GameServerDeath:
parseDeath(msg);
@@ -743,7 +743,19 @@ void ProtocolGame::parseRequestPurchaseData(const InputMessagePtr& msg)
void ProtocolGame::parseResourceBalance(const InputMessagePtr& msg) const
{
const auto type = static_cast(msg->getU8());
- const uint64_t value = msg->getU64();
+ uint64_t value;
+ switch (type) {
+ case Otc::RESOURCE_CHARM:
+ case Otc::RESOURCE_MINOR_CHARM:
+ case Otc::RESOURCE_MAX_CHARM:
+ case Otc::RESOURCE_MAX_MINOR_CHARM:
+ // 14.10
+ value = msg->getU32();
+ break;
+ default:
+ value = msg->getU64();
+ break;
+ }
m_localPlayer->setResourceBalance(type, value);
}
@@ -1247,11 +1259,15 @@ void ProtocolGame::parseSessionEnd(const InputMessagePtr& msg)
void ProtocolGame::parsePing(const InputMessagePtr&) { g_game.processPing(); }
void ProtocolGame::parsePingBack(const InputMessagePtr&) { g_game.processPingBack(); }
-void ProtocolGame::parseChallenge(const InputMessagePtr& msg)
+void ProtocolGame::parseLoginChallenge(const InputMessagePtr& msg)
{
const uint32_t timestamp = msg->getU32();
const uint8_t random = msg->getU8();
+ if (g_game.getClientVersion() >= 1405) {
+ msg->skipBytes(1);
+ }
+
sendLoginPacket(timestamp, random);
}
@@ -1622,7 +1638,7 @@ void ProtocolGame::parsePlayerGoods(const InputMessagePtr& msg) const
// 12.x NOTE: this u64 is parsed only, because TFS stil sends it, we use resource balance in this protocol
uint64_t money = 0;
if (g_game.getClientVersion() >= 1281) {
- money = m_localPlayer->getResourceBalance(Otc::RESOURCE_BANK_BALANCE) + m_localPlayer->getResourceBalance(Otc::RESOURCE_GOLD_EQUIPPED);
+ money = m_localPlayer->getTotalMoney();
} else {
money = g_game.getClientVersion() >= 973 ? msg->getU64() : msg->getU32();
}
@@ -2188,15 +2204,21 @@ void ProtocolGame::parsePlayerStats(const InputMessagePtr& msg) const
if (g_game.getFeature(Otc::GameExperienceBonus)) {
if (g_game.getClientVersion() <= 1096) {
- msg->getDouble(); // experienceBonus
+ const double experienceBonus = msg->getDouble();
+ m_localPlayer->setExperienceRate(Otc::EXP_BASE, experienceBonus * 100);
} else {
- msg->getU16(); // baseXpGain
+ const uint16_t baseXpGain = msg->getU16();
+ m_localPlayer->setExperienceRate(Otc::EXP_BASE, baseXpGain);
if (g_game.getClientVersion() < 1281) {
- msg->getU16(); // voucherAddend
+ const uint16_t voucherAddend = msg->getU16();
+ m_localPlayer->setExperienceRate(Otc::EXP_VOUCHER, voucherAddend);
}
- msg->getU16(); // grindingAddend
- msg->getU16(); // storeBoostAddend
- msg->getU16(); // huntingBoostFactor
+ const uint16_t grindingAddend = msg->getU16();
+ m_localPlayer->setExperienceRate(Otc::EXP_LOWLEVEL, grindingAddend);
+ const uint16_t storeBoostAddend = msg->getU16();
+ m_localPlayer->setExperienceRate(Otc::EXP_XPBOOST, storeBoostAddend);
+ const uint16_t huntingBoostFactor = msg->getU16();
+ m_localPlayer->setExperienceRate(Otc::EXP_STANINAMULTIPLIER, huntingBoostFactor);
}
}
@@ -2219,7 +2241,7 @@ void ProtocolGame::parsePlayerStats(const InputMessagePtr& msg) const
const uint16_t training = g_game.getFeature(Otc::GameOfflineTrainingTime) ? msg->getU16() : 0;
if (g_game.getClientVersion() >= 1097) {
- msg->getU16(); // xp boost time (seconds)
+ m_localPlayer->setStoreExpBoostTime(msg->getU16()); // xp boost time (seconds)
msg->getU8(); // enables exp boost in the store
}
@@ -2302,8 +2324,7 @@ void ProtocolGame::parsePlayerSkills(const InputMessagePtr& msg) const
msg->getU8();
}
- if (g_game.getClientVersion() >= 1281) {
- // forge skill stats
+ if (g_game.getFeature(Otc::GameForgeSkillStats)) {
const uint8_t lastSkill = g_game.getClientVersion() >= 1332 ? Otc::LastSkill : Otc::Momentum + 1;
for (int_fast32_t skill = Otc::Fatal; skill < lastSkill; ++skill) {
const uint16_t level = msg->getU16();
@@ -2318,13 +2339,65 @@ void ProtocolGame::parsePlayerSkills(const InputMessagePtr& msg) const
m_localPlayer->setTotalCapacity(capacity);
}
+
+ if (g_game.getFeature(Otc::GameCharacterSkillStats)) {
+ //msg->getU8(); // GameConcotions ??
+ const uint32_t capacity = msg->getU32(); // base + bonus capacity
+ msg->getU32(); // base capacity
+ m_localPlayer->setTotalCapacity(capacity);
+ // Flat Damage and Healing Total
+ const uint16_t flatBonus = msg->getU16();
+ m_localPlayer->setFlatDamageHealing(flatBonus);
+
+ // Weapon attack and element info
+ const uint16_t attackValue = msg->getU16();
+ const uint8_t attackElement = msg->getU8();
+ m_localPlayer->setAttackInfo(attackValue, attackElement);
+
+ // Converted Damage
+ const double convertedDamage = msg->getDouble();
+ const uint8_t convertedElement = msg->getU8();
+ m_localPlayer->setConvertedDamage(convertedDamage, convertedElement);
+
+ // Imbuements
+ const double lifeLeech = msg->getDouble();
+ const double manaLeech = msg->getDouble();
+ const double critChance = msg->getDouble();
+ const double critDamage = msg->getDouble();
+ const double onslaught = msg->getDouble();
+ m_localPlayer->setImbuements(lifeLeech, manaLeech, critChance, critDamage, onslaught);
+
+ // Defense info
+ const uint16_t defense = msg->getU16();
+ const uint16_t armor = msg->getU16();
+ const double mitigation = msg->getDouble();
+ const double dodge = msg->getDouble();
+ const uint16_t damageReflection = msg->getU16();
+ m_localPlayer->setDefenseInfo(defense, armor, mitigation, dodge, damageReflection);
+
+ // Combat absorb values
+ const uint8_t combatsCount = msg->getU8();
+ std::map absorbValues;
+ for (int i = 0; i < combatsCount; i++) {
+ const uint8_t combatType = msg->getU8();
+ const double value = msg->getDouble();
+ absorbValues[combatType] = value;
+ }
+ m_localPlayer->setCombatAbsorbValues(absorbValues);
+
+ // Forge bonuses
+ const double momentum = msg->getDouble();
+ const double transcendence = msg->getDouble();
+ const double amplification = msg->getDouble();
+ m_localPlayer->setForgeBonuses(momentum, transcendence, amplification);
+ }
}
void ProtocolGame::parsePlayerState(const InputMessagePtr& msg) const
{
uint32_t states;
if (g_game.getClientVersion() >= 1281) {
- states = msg->getU32();
+ states = g_game.getClientVersion() >= 1405 ? msg->getU64() : msg->getU32();
if (g_game.getFeature(Otc::GamePlayerStateCounter)) {
msg->getU8(); // icons counter
}
@@ -2786,29 +2859,33 @@ void ProtocolGame::parseQuestTracker(const InputMessagePtr& msg)
case 1: {
const uint8_t remainingQuests = msg->getU8();
const uint8_t missionCount = msg->getU8();
- std::vector> missions;
+ std::vector> missions;
for (uint8_t i = 0; i < missionCount; ++i) {
- const uint16_t missionId = msg->getU16();
- const std::string& questName = msg->getString();
- uint8_t questIsCompleted = 0;
+ uint8_t questId = 0;
if (g_game.getClientVersion() >= 1410) {
- questIsCompleted = msg->getU8();
+ questId = msg->getU16();
}
+ const uint16_t missionId = msg->getU16();
+ const std::string& questName = msg->getString();
const std::string& missionName = msg->getString();
const std::string& missionDesc = msg->getString();
- missions.emplace_back(missionId, questName, questIsCompleted, missionName, missionDesc);
+ missions.emplace_back(questId, missionId, questName, missionName, missionDesc);
}
return g_lua.callGlobalField("g_game", "onQuestTracker", remainingQuests, missions);
}
case 0: {
+ uint8_t questId = 0;
+ if (g_game.getClientVersion() >= 1410) {
+ questId = msg->getU16();
+ }
const uint16_t missionId = msg->getU16();
- const std::string& missionName = msg->getString();
- uint8_t questIsCompleted = 0;
+ std::string questName = "";
if (g_game.getClientVersion() >= 1410) {
- questIsCompleted = msg->getU8();
+ questName = msg->getString();
}
+ const std::string& missionName = msg->getString();
const std::string& missionDesc = msg->getString();
- return g_lua.callGlobalField("g_game", "onUpdateQuestTracker", missionId, missionName, questIsCompleted, missionDesc);
+ return g_lua.callGlobalField("g_game", "onUpdateQuestTracker", questId, missionId, questName, missionName, missionDesc);
}
}
}
@@ -2990,14 +3067,15 @@ void ProtocolGame::parseBestiaryMonsterData(const InputMessagePtr& msg)
msg->getU16();
data.location = msg->getString();
}
-
- if (data.currentLevel > 3) {
- const bool hasCharm = static_cast(msg->getU8());
- if (hasCharm) {
- msg->getU8();
- msg->getU32();
- } else {
- msg->getU8();
+ if (g_game.getClientVersion() < 1410) {
+ if (data.currentLevel > 3) {
+ const bool hasCharm = static_cast(msg->getU8());
+ if (hasCharm) {
+ msg->getU8();
+ msg->getU32();
+ } else {
+ msg->getU8();
+ }
}
}
@@ -3007,40 +3085,67 @@ void ProtocolGame::parseBestiaryMonsterData(const InputMessagePtr& msg)
void ProtocolGame::parseBestiaryCharmsData(const InputMessagePtr& msg)
{
BestiaryCharmsData charmData;
- charmData.points = msg->getU32();
+
+ if (g_game.getClientVersion() >= 1410) {
+ charmData.resetAllCharmsCost = msg->getU64();
+ } else {
+ charmData.points = msg->getU32();
+ }
const uint8_t charmsAmount = msg->getU8();
+ charmData.charms.reserve(charmsAmount);
+
for (auto i = 0; i < charmsAmount; ++i) {
CharmData charm;
charm.id = msg->getU8();
- charm.name = msg->getString();
- charm.description = msg->getString();
- msg->getU8();
- charm.unlockPrice = msg->getU16();
- charm.unlocked = msg->getU8() == 1;
charm.asignedStatus = false;
charm.raceId = 0;
charm.removeRuneCost = 0;
-
+ if (g_game.getClientVersion() >= 1410) {
+ charm.tier = msg->getU8();
+ charm.unlocked = msg->getU8() == 1;
+ } else {
+ charm.name = msg->getString();
+ charm.description = msg->getString();
+ msg->getU8(); // Unknown byte
+ charm.unlockPrice = msg->getU16();
+ charm.unlocked = msg->getU8() == 1;
+ charm.tier = charm.unlocked ? 1 : 0;
+ }
if (charm.unlocked) {
- const bool asigned = static_cast(msg->getU8());
+ bool asigned = true;
+ if (g_game.getClientVersion() < 1410) {
+ asigned = static_cast(msg->getU8());
+ }
+
if (asigned) {
- charm.asignedStatus = asigned;
+ charm.asignedStatus = true;
charm.raceId = msg->getU16();
charm.removeRuneCost = msg->getU32();
}
- } else {
- msg->getU8();
+ } else if (g_game.getClientVersion() < 1410) {
+ msg->getU8(); // ??
}
charmData.charms.emplace_back(charm);
}
- msg->getU8();
+ if (g_game.getClientVersion() >= 1410) {
+ charmData.availableCharmSlots = msg->getU8();
+ } else {
+ msg->getU8(); // ??
+ }
const uint16_t finishedMonstersSize = msg->getU16();
+ charmData.finishedMonsters.reserve(finishedMonstersSize);
+
for (auto i = 0; i < finishedMonstersSize; ++i) {
- const uint16_t raceId = msg->getU16();
+ uint32_t raceId;
+ if (g_game.getClientVersion() >= 1410) {
+ raceId = msg->getU32();
+ } else {
+ raceId = msg->getU16();
+ }
charmData.finishedMonsters.emplace_back(raceId);
}
@@ -4074,8 +4179,9 @@ void ProtocolGame::parseSupplyStash(const InputMessagePtr& msg)
uint32_t amount = msg->getU32();
stashItems.push_back({ itemId, amount });
}
-
- msg->getU16(); // free slots
+ if (g_game.getProtocolVersion() < 1410) {
+ msg->getU16(); // free slots
+ }
g_lua.callGlobalField("g_game", "onSupplyStashEnter", stashItems);
}
@@ -4120,21 +4226,27 @@ void ProtocolGame::parseImbuementDurations(const InputMessagePtr& msg)
std::vector itemList;
for (auto i = 0; i < itemListCount; ++i) {
- ImbuementTrackerItem item(msg->getU8());
+ ImbuementTrackerItem item(msg->getU8()); // slot
item.item = getItem(msg);
std::map slots;
const uint8_t slotsCount = msg->getU8(); // total amount of imbuing slots on item
+ if (slotsCount == 0) {
+ continue;
+ }
+
for (auto slotIndex = 0; slotIndex < slotsCount; ++slotIndex) {
const bool slotImbued = static_cast(msg->getU8()); // 0 - empty, 1 - imbued
- ImbuementSlot slot(slotIndex);
- if (slotImbued) {
- slot.name = msg->getString();
- slot.iconId = msg->getU16();
- slot.duration = msg->getU32();
- slot.state = msg->getU8(); // 0 - paused, 1 - decaying
+ if (!slotImbued) {
+ continue;
}
+
+ ImbuementSlot slot(slotIndex);
+ slot.name = msg->getString();
+ slot.iconId = msg->getU16();
+ slot.duration = msg->getU32();
+ slot.state = msg->getU8(); // 0 - paused, 1 - decaying
slots.emplace(slotIndex, slot);
}
@@ -4385,7 +4497,7 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg)
std::vector> forgeSkillsArray;
- if (g_game.getClientVersion() >= 1281) {
+ if (g_game.getFeature(Otc::GameForgeSkillStats)) {
// forge skill stats
const uint8_t lastSkill = g_game.getClientVersion() >= 1332 ? Otc::LastSkill : Otc::Momentum + 1;
for (uint16_t skill = Otc::Fatal; skill < lastSkill; ++skill) {
@@ -4524,8 +4636,11 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg)
for (auto i = 0; i < stashItemsCount; ++i) {
ItemSummary item;
const uint16_t itemId = msg->getU16();
- const auto& itemCreated = Item::create(itemId);
- const uint16_t classification = itemCreated->getClassification();
+ const auto& thing = g_things.getThingType(itemId, ThingCategoryItem);
+ if (!thing) {
+ continue;
+ }
+ const uint16_t classification = thing->getClassification();
uint8_t itemTier = 0;
if (classification > 0) {
@@ -4711,6 +4826,153 @@ void ProtocolGame::parseCyclopediaCharacterInfo(const InputMessagePtr& msg)
}
break;
}
+ case Otc::CYCLOPEDIA_CHARACTERINFO_OFFENCESTATS:
+ {
+ CyclopediaCharacterOffenceStats data;
+ data.critChance = msg->getDouble();
+ msg->getDouble(); // unused
+ msg->getDouble(); // unused
+ msg->getDouble(); // unused
+ msg->getDouble(); // unused
+
+ // Critical hit damage
+ data.critDamage = msg->getDouble();
+ data.critDamageBase = msg->getDouble();
+ data.critDamageImbuement = msg->getDouble();
+ data.critDamageWheel = msg->getDouble();
+ msg->getDouble(); // unused
+
+ // Life leech amount
+ data.lifeLeech = msg->getDouble();
+ data.lifeLeechBase = msg->getDouble();
+ data.lifeLeechImbuement = msg->getDouble();
+ data.lifeLeechWheel = msg->getDouble();
+ msg->getDouble(); // unused
+
+ // Mana leech amount
+ data.manaLeech = msg->getDouble();
+ data.manaLeechBase = msg->getDouble();
+ data.manaLeechImbuement = msg->getDouble();
+ data.manaLeechWheel = msg->getDouble();
+ msg->getDouble(); // unused
+
+ // Onslaught
+ data.onslaught = msg->getDouble();
+ data.onslaughtBase = msg->getDouble();
+ data.onslaughtBonus = msg->getDouble();
+ msg->getDouble(); // unused
+
+ data.cleavePercent = msg->getDouble();
+
+ // Perfect shot range
+ for (int i = 0; i < 5; i++) {
+ data.perfectShotDamage.push_back(msg->getU16());
+ }
+
+ data.flatDamage = msg->getU16();
+ data.flatDamageBase = msg->getU16();
+ msg->getU16(); // unused
+
+ data.weaponAttack = msg->getU16();
+ data.weaponFlatModifier = msg->getU16();
+ data.weaponDamage = msg->getU16();
+ data.weaponSkillType = msg->getU8();
+ data.weaponSkillLevel = msg->getU16();
+ data.weaponSkillModifier = msg->getU16();
+ data.weaponElement = msg->getU8();
+ data.weaponElementDamage = msg->getDouble();
+ data.weaponElementType = msg->getU8();
+
+ const uint8_t accuracyCount = msg->getU8();
+ for (int i = 0; i < accuracyCount; i++) {
+ msg->getU8(); // range
+ data.weaponAccuracy.push_back(msg->getDouble());
+ }
+
+ g_game.processCyclopediaCharacterOffenceStats(data);
+ break;
+ }
+ case Otc::CYCLOPEDIA_CHARACTERINFO_DEFENCESTATS:
+ {
+ CyclopediaCharacterDefenceStats data;
+
+ data.dodgeTotal = msg->getDouble();
+ data.dodgeBase = msg->getDouble();
+ data.dodgeBonus = msg->getDouble();
+ msg->getDouble(); // unused
+ data.dodgeWheel = msg->getDouble();
+
+ data.magicShieldCapacity = msg->getU32();
+ data.magicShieldCapacityFlat = msg->getU16();
+ data.magicShieldCapacityPercent = msg->getDouble();
+
+ data.reflectPhysical = msg->getU16();
+ data.armor = msg->getU16();
+
+ data.defense = msg->getU16();
+ data.defenseEquipment = msg->getU16();
+ data.defenseSkillType = msg->getU8();
+ data.shieldingSkill = msg->getU16();
+ data.defenseWheel = msg->getU16();
+ msg->getU16(); // unused
+
+ data.mitigation = msg->getDouble();
+ data.mitigationBase = msg->getDouble();
+ data.mitigationEquipment = msg->getDouble();
+ data.mitigationShield = msg->getDouble();
+ data.mitigationWheel = msg->getDouble();
+ data.mitigationCombatTactics = msg->getDouble();
+ const uint8_t combatsCount = msg->getU8();
+ for (int i = 0; i < combatsCount; ++i) {
+ uint8_t elementType = msg->getU8();
+ if (elementType == 0x04) {
+ CyclopediaCharacterDefenceStats::ElementalResistance resistance;
+ resistance.element = msg->getU8();
+ resistance.value = msg->getDouble();
+ data.resistances.push_back(resistance);
+ }
+ }
+
+ g_game.processCyclopediaCharacterDefenceStats(data);
+ break;
+ }
+ case Otc::CYCLOPEDIA_CHARACTERINFO_MISCSTATS:
+ {
+ CyclopediaCharacterMiscStats data;
+
+ data.momentumTotal = msg->getDouble();
+ data.momentumBase = msg->getDouble();
+ data.momentumBonus = msg->getDouble();
+ data.momentumWheel = msg->getDouble();
+ msg->getDouble(); // unused
+
+ data.dodgeTotal = msg->getDouble();
+ data.dodgeBase = msg->getDouble();
+ data.dodgeBonus = msg->getDouble();
+ data.dodgeWheel = msg->getDouble();
+
+ data.damageReflectionTotal = msg->getDouble();
+ data.damageReflectionBase = msg->getDouble();
+ data.damageReflectionBonus = msg->getDouble();
+
+ data.haveBlesses = msg->getU8();
+ data.totalBlesses = msg->getU8();
+
+ const uint8_t concoctionsCount = msg->getU8();
+ for (int i = 0; i < concoctionsCount; ++i) {
+ CyclopediaCharacterMiscStats::Concoction concoction;
+ concoction.id = msg->getU16();
+ msg->getU8(); // unused
+ msg->getU8(); // unused
+ concoction.duration = msg->getU32();
+ data.concoctions.push_back(concoction);
+ }
+
+ msg->getU8(); // unused
+
+ g_game.processCyclopediaCharacterMiscStats(data);
+ break;
+ }
}
}
diff --git a/src/client/protocolgamesend.cpp b/src/client/protocolgamesend.cpp
index 67fe6fa95a..49fd63b9c1 100644
--- a/src/client/protocolgamesend.cpp
+++ b/src/client/protocolgamesend.cpp
@@ -54,8 +54,11 @@ void ProtocolGame::sendLoginPacket(const uint32_t challengeTimestamp, const uint
msg->addString(std::to_string(g_game.getClientVersion()));
}
- if (g_game.getFeature(Otc::GameContentRevision))
+ if (g_game.getClientVersion() >= 1334) {
+ msg->addString("appearancesHash");
+ } else if (g_game.getFeature(Otc::GameContentRevision)) {
msg->addU16(g_things.getContentRevision());
+ }
if (g_game.getFeature(Otc::GamePreviewState))
msg->addU8(0);
@@ -1205,7 +1208,7 @@ void ProtocolGame::sendRequestStorePremiumBoost()
const auto& msg = std::make_shared();
msg->addU8(Proto::ClientRequestStoreOffers);
msg->addU8(Otc::Store_Type_Actions_t::OPEN_PREMIUM_BOOST);
- msg->addU8(0);
+ msg->addU8(1);
send(msg);
}
diff --git a/src/framework/net/inputmessage.cpp b/src/framework/net/inputmessage.cpp
index 5953783d66..32a5b4e294 100644
--- a/src/framework/net/inputmessage.cpp
+++ b/src/framework/net/inputmessage.cpp
@@ -23,11 +23,20 @@
#include "inputmessage.h"
#include
+#include "client/game.h"
+
+InputMessage::InputMessage() {
+ m_maxHeaderSize = g_game.getClientVersion() >= 1405 ? 7 : 8;
+ m_headerPos = m_maxHeaderSize;
+ m_readPos = m_maxHeaderSize;
+}
+
void InputMessage::reset()
{
+ m_maxHeaderSize = g_game.getClientVersion() >= 1405 ? 7 : 8;
m_messageSize = 0;
- m_readPos = MAX_HEADER_SIZE;
- m_headerPos = MAX_HEADER_SIZE;
+ m_readPos = m_maxHeaderSize;
+ m_headerPos = m_maxHeaderSize;
}
void InputMessage::setBuffer(const std::string& buffer)
@@ -112,8 +121,8 @@ void InputMessage::fillBuffer(const uint8_t* buffer, const uint16_t size)
void InputMessage::setHeaderSize(const uint16_t size)
{
- assert(MAX_HEADER_SIZE - size >= 0);
- m_headerPos = MAX_HEADER_SIZE - size;
+ assert(m_maxHeaderSize - size >= 0);
+ m_headerPos = m_maxHeaderSize - size;
m_readPos = m_headerPos;
}
diff --git a/src/framework/net/inputmessage.h b/src/framework/net/inputmessage.h
index f43c680293..040ff09b5c 100644
--- a/src/framework/net/inputmessage.h
+++ b/src/framework/net/inputmessage.h
@@ -29,15 +29,21 @@
class InputMessage final : public LuaObject
{
public:
+ InputMessage();
+
enum
{
- BUFFER_MAXSIZE = 65536,
- MAX_HEADER_SIZE = 8
+ BUFFER_MAXSIZE = 65536
};
+ inline auto getMaxHeaderSize() const
+ {
+ return m_maxHeaderSize;
+ }
+
void setBuffer(const std::string& buffer);
std::string_view getBuffer() { return std::string_view{ (char*)m_buffer + m_headerPos, m_messageSize }; }
- std::string getBodyBuffer() { return std::string((char*)m_buffer + MAX_HEADER_SIZE, m_messageSize - getHeaderSize()); }
+ std::string getBodyBuffer() { return std::string((char*)m_buffer + m_maxHeaderSize, m_messageSize - getHeaderSize()); }
void skipBytes(const uint16_t bytes) { m_readPos += bytes; }
void setReadPos(const uint16_t readPos) { m_readPos = readPos; }
@@ -73,6 +79,11 @@ class InputMessage final : public LuaObject
int getUnreadSize() { return m_messageSize - (m_readPos - m_headerPos); }
uint16_t getMessageSize() { return m_messageSize; }
+ void setPaddingSize(uint8_t padding) { m_padding = padding; }
+ uint8_t getPaddingSize() const {
+ return m_padding;
+ }
+
bool eof() { return (m_readPos - m_headerPos) >= m_messageSize; }
protected:
@@ -84,8 +95,8 @@ class InputMessage final : public LuaObject
uint8_t* getReadBuffer() { return m_buffer + m_readPos; }
uint8_t* getHeaderBuffer() { return m_buffer + m_headerPos; }
- uint8_t* getDataBuffer() { return m_buffer + MAX_HEADER_SIZE; }
- uint16_t getHeaderSize() const { return (MAX_HEADER_SIZE - m_headerPos); }
+ uint8_t* getDataBuffer() { return m_buffer + m_maxHeaderSize; }
+ uint16_t getHeaderSize() const { return (m_maxHeaderSize - m_headerPos); }
uint16_t readSize() { return getU16(); }
bool readChecksum();
@@ -97,8 +108,10 @@ class InputMessage final : public LuaObject
void checkRead(int bytes);
void checkWrite(int bytes);
- uint16_t m_headerPos{ MAX_HEADER_SIZE };
- uint16_t m_readPos{ MAX_HEADER_SIZE };
+ uint8_t m_maxHeaderSize = 8;
+ uint16_t m_headerPos{ m_maxHeaderSize };
+ uint16_t m_readPos{ m_maxHeaderSize };
uint16_t m_messageSize{ 0 };
+ uint8_t m_padding{ 0 };
uint8_t m_buffer[BUFFER_MAXSIZE]{};
};
diff --git a/src/framework/net/outputmessage.cpp b/src/framework/net/outputmessage.cpp
index 26518a5c0f..085b5ce3ae 100644
--- a/src/framework/net/outputmessage.cpp
+++ b/src/framework/net/outputmessage.cpp
@@ -23,10 +23,19 @@
#include
#include
+#include "client/game.h"
+
+OutputMessage::OutputMessage() {
+ m_maxHeaderSize = g_game.getClientVersion() >= 1405 ? 7 : 8;
+ m_writePos = m_maxHeaderSize;
+ m_headerPos = m_maxHeaderSize;
+}
+
void OutputMessage::reset()
{
- m_writePos = MAX_HEADER_SIZE;
- m_headerPos = MAX_HEADER_SIZE;
+ m_maxHeaderSize = g_game.getClientVersion() >= 1405 ? 7 : 8;
+ m_writePos = m_maxHeaderSize;
+ m_headerPos = m_maxHeaderSize;
m_messageSize = 0;
}
@@ -129,6 +138,19 @@ void OutputMessage::writeMessageSize()
m_messageSize += 2;
}
+void OutputMessage::writePaddingAmount()
+{
+ const uint8_t paddingAmount = 8 - (m_messageSize % 8) - 1;
+ addPaddingBytes(paddingAmount);
+ prependU8(paddingAmount);
+}
+
+void OutputMessage::writeHeaderSize()
+{
+ auto headerSize = static_cast((m_messageSize - 4) / 8); // -4 for checksum
+ prependU16(headerSize); // Uses writeULE16 and updates `m_headerPos` and `m_messageSize`
+}
+
bool OutputMessage::canWrite(const int bytes) const
{
return m_writePos + bytes <= BUFFER_MAXSIZE;
@@ -138,4 +160,27 @@ void OutputMessage::checkWrite(const int bytes)
{
if (!canWrite(bytes))
throw stdext::exception("OutputMessage max buffer size reached");
-}
\ No newline at end of file
+}
+
+void OutputMessage::prependU8(uint8_t value)
+{
+ assert(m_headerPos > 0);
+ m_headerPos--;
+ m_writePos--;
+ m_buffer[m_headerPos] = value;
+ m_messageSize++;
+}
+
+void OutputMessage::prependU16(uint16_t value)
+{
+ assert(m_headerPos >= 2);
+ m_headerPos -= 2;
+ m_writePos -= 2;
+ stdext::writeULE16(m_buffer + m_headerPos, value);
+ m_messageSize += 2;
+}
+
+uint8_t* OutputMessage::getXteaEncryptionBuffer()
+{
+ return g_game.getClientVersion() >= 1405 ? getHeaderBuffer() : getDataBuffer() - 2;
+}
diff --git a/src/framework/net/outputmessage.h b/src/framework/net/outputmessage.h
index d16be7690d..a06b8ad3b0 100644
--- a/src/framework/net/outputmessage.h
+++ b/src/framework/net/outputmessage.h
@@ -32,10 +32,11 @@ class OutputMessage final : public LuaObject
enum
{
BUFFER_MAXSIZE = 65536,
- MAX_STRING_LENGTH = 65536,
- MAX_HEADER_SIZE = 8
+ MAX_STRING_LENGTH = 65536
};
+ OutputMessage();
+
void reset();
void setBuffer(const std::string& buffer);
@@ -47,6 +48,8 @@ class OutputMessage final : public LuaObject
void addU64(uint64_t value);
void addString(std::string_view buffer);
void addPaddingBytes(int bytes, uint8_t byte = 0);
+ void prependU8(uint8_t value);
+ void prependU16(uint16_t value);
void encryptRsa();
@@ -56,14 +59,18 @@ class OutputMessage final : public LuaObject
void setWritePos(const uint16_t writePos) { m_writePos = writePos; }
void setMessageSize(const uint16_t messageSize) { m_messageSize = messageSize; }
+ uint8_t* getXteaEncryptionBuffer();
+
protected:
uint8_t* getWriteBuffer() { return m_buffer + m_writePos; }
uint8_t* getHeaderBuffer() { return m_buffer + m_headerPos; }
- uint8_t* getDataBuffer() { return m_buffer + MAX_HEADER_SIZE; }
+ uint8_t* getDataBuffer() { return m_buffer + m_maxHeaderSize; }
void writeChecksum();
void writeSequence(uint32_t sequence);
void writeMessageSize();
+ void writePaddingAmount();
+ void writeHeaderSize();
friend class Protocol;
friend class PacketPlayer;
@@ -72,8 +79,9 @@ class OutputMessage final : public LuaObject
bool canWrite(int bytes) const;
void checkWrite(int bytes);
- uint16_t m_headerPos{ MAX_HEADER_SIZE };
- uint16_t m_writePos{ MAX_HEADER_SIZE };
+ uint8_t m_maxHeaderSize { 8 };
+ uint16_t m_headerPos{ m_maxHeaderSize };
+ uint16_t m_writePos{ m_maxHeaderSize };
uint16_t m_messageSize{ 0 };
uint8_t m_buffer[BUFFER_MAXSIZE];
};
diff --git a/src/framework/net/protocol.cpp b/src/framework/net/protocol.cpp
index 9ecacedc52..2d1abffcb6 100644
--- a/src/framework/net/protocol.cpp
+++ b/src/framework/net/protocol.cpp
@@ -28,6 +28,7 @@
#include "inputmessage.h"
#include "outputmessage.h"
#include "framework/core/graphicalapplication.h"
+#include "client/game.h"
#ifdef __EMSCRIPTEN__
#include "webconnection.h"
#else
@@ -131,19 +132,29 @@ void Protocol::send(const OutputMessagePtr& outputMessage)
m_recorder->addOutputPacket(outputMessage);
}
+ // padding
+ if (g_game.getClientVersion() >= 1405) {
+ outputMessage->writePaddingAmount();
+ }
// encrypt
- if (m_xteaEncryptionEnabled)
+ if (m_xteaEncryptionEnabled) {
xteaEncrypt(outputMessage);
+ }
// write checksum
- if (m_sequencedPackets)
+ if (m_sequencedPackets) {
outputMessage->writeSequence(m_packetNumber++);
- else if (m_checksumEnabled)
+ } else if (m_checksumEnabled) {
outputMessage->writeChecksum();
+ }
// write message size
- outputMessage->writeMessageSize();
+ if (g_game.getClientVersion() >= 1405) {
+ outputMessage->writeHeaderSize();
+ } else {
+ outputMessage->writeMessageSize();
+ }
onSend();
@@ -174,8 +185,11 @@ void Protocol::recv()
int headerSize = 2; // 2 bytes for message size
if (m_checksumEnabled)
headerSize += 4; // 4 bytes for checksum
- if (m_xteaEncryptionEnabled)
+ if (g_game.getClientVersion() >= 1405) {
+ headerSize += 1; // 1 bytes for padding size
+ } else if (m_xteaEncryptionEnabled) {
headerSize += 2; // 2 bytes for XTEA encrypted message size
+ }
m_inputMessage->setHeaderSize(headerSize);
// read the first 2 bytes which contain the message size
@@ -190,7 +204,16 @@ void Protocol::internalRecvHeader(const uint8_t* buffer, const uint16_t size)
{
// read message size
m_inputMessage->fillBuffer(buffer, size);
- const uint16_t remainingSize = m_inputMessage->readSize();
+ uint16_t remainingSize = m_inputMessage->readSize();
+ if (g_game.getClientVersion() >= 1405) {
+ remainingSize = remainingSize * 8 + 4;
+ }
+
+ constexpr uint32_t MAX_PACKET = InputMessage::BUFFER_MAXSIZE;
+ if (remainingSize == 0 || remainingSize > MAX_PACKET) {
+ g_logger.error(fmt::format("invalid packet size = {}", remainingSize));
+ return;
+ }
// read remaining message data
if (m_connection)
@@ -214,10 +237,14 @@ void Protocol::internalRecvData(const uint8_t* buffer, const uint16_t size)
if (m_sequencedPackets) {
decompress = (m_inputMessage->getU32() & 1 << 31);
} else if (m_checksumEnabled && !m_inputMessage->readChecksum()) {
- g_logger.traceError(
- "got a network message with invalid checksum, size: {}",
- static_cast(m_inputMessage->getMessageSize())
- );
+ std::string headerHex;
+ headerHex.reserve(m_inputMessage->getHeaderSize() * 3); // 2 chars + space por byte
+
+ for (size_t i = 0; i < m_inputMessage->getHeaderSize(); ++i) {
+ fmt::format_to(std::back_inserter(headerHex), "{:02X} ", static_cast(m_inputMessage->getBuffer()[i]));
+ }
+
+ g_logger.traceError(fmt::format("got a network message with invalid checksum, header: {}, size: {}", headerHex, static_cast(m_inputMessage->getMessageSize())));
return;
}
@@ -306,20 +333,31 @@ bool Protocol::xteaDecrypt(const InputMessagePtr& inputMessage) const
});
}
- const uint16_t decryptedSize = inputMessage->getU16() + 2;
- const int sizeDelta = decryptedSize - encryptedSize;
- if (sizeDelta > 0 || -sizeDelta > encryptedSize) {
- g_logger.traceError("invalid decrypted network message");
- return false;
+ uint16_t decryptedSize;
+ if (g_game.getClientVersion() >= 1405) {
+ const uint8_t paddingSize = inputMessage->getU8();
+ inputMessage->setPaddingSize(paddingSize);
+ decryptedSize = encryptedSize - paddingSize - 1;
+ inputMessage->setMessageSize(inputMessage->getHeaderSize() + decryptedSize);
+ } else {
+ decryptedSize = inputMessage->getU16() + 2;
+ const int sizeDelta = decryptedSize - encryptedSize;
+ if (sizeDelta > 0 || -sizeDelta > encryptedSize) {
+ g_logger.traceError("invalid decrypted network message");
+ return false;
+ }
+ inputMessage->setMessageSize(inputMessage->getMessageSize() + sizeDelta);
}
- inputMessage->setMessageSize(inputMessage->getMessageSize() + sizeDelta);
return true;
}
void Protocol::xteaEncrypt(const OutputMessagePtr& outputMessage) const
{
- outputMessage->writeMessageSize();
+ if (g_game.getClientVersion() < 1405) {
+ outputMessage->writeMessageSize();
+ }
+
uint16_t encryptedSize = outputMessage->getMessageSize();
//add bytes until reach 8 multiple
@@ -330,7 +368,7 @@ void Protocol::xteaEncrypt(const OutputMessagePtr& outputMessage) const
}
for (uint32_t i = 0, sum = 0, next_sum = sum + delta; i < 32; ++i, sum = next_sum, next_sum += delta) {
- apply_rounds(outputMessage->getDataBuffer() - 2, encryptedSize, [&](uint32_t& left, uint32_t& right) {
+ apply_rounds(outputMessage->getXteaEncryptionBuffer(), encryptedSize, [&, sum, next_sum, this](uint32_t& left, uint32_t& right) mutable {
left += ((right << 4 ^ right >> 5) + right) ^ (sum + m_xteaKey[sum & 3]);
right += ((left << 4 ^ left >> 5) + left) ^ (next_sum + m_xteaKey[(next_sum >> 11) & 3]);
});
@@ -364,8 +402,11 @@ void Protocol::onProxyPacket(const std::shared_ptr>& packet
int headerSize = 2; // 2 bytes for message size
if (m_checksumEnabled)
headerSize += 4; // 4 bytes for checksum
- if (m_xteaEncryptionEnabled)
+ if (g_game.getClientVersion() >= 1405) {
+ headerSize += 1; // 1 bytes for padding size
+ } else if (m_xteaEncryptionEnabled) {
headerSize += 2; // 2 bytes for XTEA encrypted message size
+ }
m_inputMessage->setHeaderSize(headerSize);
m_inputMessage->fillBuffer(packet->data(), 2);
m_inputMessage->readSize();
diff --git a/src/protobuf/appearances.proto b/src/protobuf/appearances.proto
index 80499a1dc4..465c08b67e 100644
--- a/src/protobuf/appearances.proto
+++ b/src/protobuf/appearances.proto
@@ -47,6 +47,7 @@ enum ITEM_CATEGORY {
ITEM_CATEGORY_BACKPACK = 28;
ITEM_CATEGORY_ONEHANDWEAPON = 29;
ITEM_CATEGORY_ARROW = 30;
+ ITEM_CATEGORY_SOULCORES = 31;
}
enum PLAYER_PROFESSION {
@@ -56,6 +57,7 @@ enum PLAYER_PROFESSION {
PLAYER_PROFESSION_PALADIN = 2;
PLAYER_PROFESSION_SORCERER = 3;
PLAYER_PROFESSION_DRUID = 4;
+ PLAYER_PROFESSION_MONK = 5;
PLAYER_PROFESSION_PROMOTED = 10;
}
@@ -183,12 +185,21 @@ message AppearanceFlags {
optional bool expire = 55;
optional bool expirestop = 56;
optional bool deco_kit = 57;
+ optional bool dual_wielding = 59;
+ reserved 58, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69;
+ optional bool hook_south = 70;
+ optional bool hook_east = 71;
+ optional AppearanceFlagTransparencyLevel transparencylevel = 72;
}
message AppearanceFlagUpgradeClassification {
optional uint32 upgrade_classification = 1;
}
+message AppearanceFlagTransparencyLevel {
+ optional uint32 level = 1;
+}
+
message AppearanceFlagBank {
optional uint32 waypoints = 1;
}