mirror of
https://gitlab.com/2009scape/rt4-client.git
synced 2025-12-09 16:45:46 -07:00
Merge branch 'xp_globes' into 'master'
Introduce XPGlobesPlugin See merge request 2009scape/rt4-client!18
This commit is contained in:
commit
70a79c3785
4 changed files with 449 additions and 0 deletions
|
|
@ -0,0 +1,30 @@
|
|||
package XPGlobesPlugin
|
||||
|
||||
import java.awt.Color
|
||||
|
||||
|
||||
object Constants {
|
||||
const val SKILL_COUNT = 24
|
||||
const val MAX_LEVEL = 99
|
||||
const val INVALID_LEVEL = -1
|
||||
const val INVALID_XP = -1
|
||||
const val GLOBE_LIFETIME = 7000L // 7 seconds
|
||||
const val GLOBES_Y_OFFSET = 48 // y-offset in screen space where globes are drawn
|
||||
val GLOBE_BKG_COLOR: Color = Color.GRAY
|
||||
const val GLOBE_BKG_SIZE = 33 // Size of the globe background
|
||||
val GLOBE_BORDER_COLOR: Color = Color.BLACK
|
||||
const val GLOBE_BORDER_WIDTH = 1 // Width of the globe border
|
||||
val GLOBE_XP_ARC_COLOR: Color = Color.YELLOW
|
||||
val GLOBE_XP_ARC_LEVEL_UP_COLOR: Color = Color.BLUE
|
||||
const val GLOBE_XP_ARC_WIDTH = 3 // Width of the Xp arc
|
||||
const val MAX_GLOBES = 6 // maximum number of globes we will draw on resizable clients
|
||||
const val MAX_GLOBES_SD = 3 // maximum number of globes we will draw on fixed clients
|
||||
const val GLOBE_PADDING = 3 // horizontal padding between globes
|
||||
const val GLOBE_TEXT_SIZE = 20 // font size of level text
|
||||
const val GLOBE_TEXT_PULSES = 7 // 7 text pulses per globe lifetime, on level-up
|
||||
const val GLOBE_TEXT_PULSE_LOW = 0.2F // alpha during text fade-out
|
||||
const val GLOBE_TEXT_PULSE_HIGH = 0.8F // alpha during text fade-in
|
||||
val GLOBE_TEXT_FG_COLOR: Color = Color.BLUE // foreground color of level text during level-up
|
||||
val GLOBE_TEXT_BG_COLOR: Color = Color.BLACK // background color (outline) of text number during level-up
|
||||
const val GLOBE_TEXT_OUTLINE_WIDTH = 2 // outline width
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package XPGlobesPlugin
|
||||
|
||||
import plugin.api.API
|
||||
import rt4.Sprite
|
||||
|
||||
|
||||
object XPSprites {
|
||||
|
||||
private val spriteOffsets: Array<Pair<Int, Int>> = arrayOf(
|
||||
Pair(1,-1), // attack
|
||||
Pair(0,0), // defense
|
||||
Pair(0,0), // strength
|
||||
Pair(0,1), // health
|
||||
Pair(0,0), // ranged
|
||||
Pair(0,0), // prayer
|
||||
Pair(0,-1), // magic
|
||||
Pair(0,-1), // cooking
|
||||
Pair(0,0), // woodcutting
|
||||
Pair(1,0), // fletching
|
||||
Pair(3,0), // fishing
|
||||
Pair(0,-1), // fire-making
|
||||
Pair(1,0), // crafting
|
||||
Pair(1,0), // smithing
|
||||
Pair(2,-1), // mining
|
||||
Pair(0,1), // herblore
|
||||
Pair(2,0), // agility
|
||||
Pair(0,0), // thieving
|
||||
Pair(-1,0), // slayer
|
||||
Pair(0,0), // farming
|
||||
Pair(0,0), // runecrafting
|
||||
Pair(0,0), // hunter
|
||||
Pair(1,-1), // construction
|
||||
Pair(2,-1), // summoning
|
||||
)
|
||||
|
||||
|
||||
fun getSpriteForSkill(skillId: Int) : Sprite? {
|
||||
return API.GetSprite(getSpriteId(skillId))
|
||||
}
|
||||
|
||||
|
||||
fun getSpriteOffsetForSkill(skillId: Int) : Pair<Int, Int> {
|
||||
if (skillId < 0 || skillId >= Constants.SKILL_COUNT) {
|
||||
return Pair(0,0)
|
||||
}
|
||||
|
||||
return spriteOffsets[skillId];
|
||||
}
|
||||
|
||||
|
||||
private fun getSpriteId(skillId: Int) : Int {
|
||||
return when (skillId) {
|
||||
0 -> 197
|
||||
1 -> 199
|
||||
2 -> 198
|
||||
3 -> 203
|
||||
4 -> 200
|
||||
5 -> 201
|
||||
6 -> 202
|
||||
7 -> 212
|
||||
8 -> 214
|
||||
9 -> 208
|
||||
10 -> 211
|
||||
11 -> 213
|
||||
12 -> 207
|
||||
13 -> 210
|
||||
14 -> 209
|
||||
15 -> 205
|
||||
16 -> 204
|
||||
17 -> 206
|
||||
18 -> 216
|
||||
19 -> 217
|
||||
20 -> 215
|
||||
21 -> 220
|
||||
22 -> 221
|
||||
23 -> 222
|
||||
else -> 222
|
||||
}
|
||||
}
|
||||
}
|
||||
45
plugin-playground/src/main/kotlin/XPGlobesPlugin/XPTable.kt
Normal file
45
plugin-playground/src/main/kotlin/XPGlobesPlugin/XPTable.kt
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package XPGlobesPlugin
|
||||
|
||||
object XPTable {
|
||||
|
||||
// source for the experience table: https://oldschool.runescape.wiki/w/Experience#Experience_table
|
||||
private val xpTable = arrayOf(
|
||||
0, 83, 174, 276, 388, 512, 650, 801, 969, 1154,
|
||||
1358, 1584, 1833, 2107, 2411, 2746, 3115, 3523, 3973, 4470,
|
||||
5018, 5624, 6291, 7028, 7842, 8740, 9730, 10824, 12031, 13363,
|
||||
14833, 16456, 18247, 20224, 22406, 24815, 27473, 30408, 33648, 37224,
|
||||
41171, 45529, 50339, 55649, 61512, 67983, 75127, 83014, 91721, 101333,
|
||||
111945, 123660, 136594, 150872, 166636, 184040, 203254, 224466, 247886, 273742,
|
||||
302288, 333804, 368599, 407015, 449428, 496254, 547953, 605032, 668051, 737627,
|
||||
814445, 899257, 992895, 1096278, 1210421, 1336443, 1475581, 1629200, 1798808, 1986068,
|
||||
2192818, 2421087, 2673114, 2951373, 3258594, 3597792, 3972294, 4385776, 4842295, 5346332,
|
||||
5902831, 6517253, 7195629, 7944614, 8771558, 9684577, 10692629, 11805606, 13034431
|
||||
)
|
||||
|
||||
fun getXpRequiredForLevel(level: Int): Int {
|
||||
if (level in 1..xpTable.size) {
|
||||
return xpTable[level - 1]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fun getLevelForXp(xp: Int): Pair<Int, Int> {
|
||||
var lowIndex = 0
|
||||
var highIndex = xpTable.size - 1
|
||||
|
||||
while (lowIndex <= highIndex) {
|
||||
val midIndex = (lowIndex + highIndex) / 2
|
||||
when {
|
||||
xp < xpTable[midIndex] -> highIndex = midIndex - 1
|
||||
xp >= xpTable[midIndex + 1] -> lowIndex = midIndex + 1
|
||||
else -> {
|
||||
val currentLevel = midIndex + 1
|
||||
val xpGained = xp - xpTable[midIndex]
|
||||
return Pair(currentLevel, xpGained)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(Constants.INVALID_LEVEL, 0) // If xp is above all defined levels
|
||||
}
|
||||
}
|
||||
294
plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt
Normal file
294
plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
package XPGlobesPlugin
|
||||
|
||||
|
||||
import rt4.Sprite
|
||||
import plugin.Plugin
|
||||
import plugin.annotations.PluginMeta
|
||||
import plugin.api.*
|
||||
import java.awt.Color
|
||||
import java.awt.geom.Arc2D
|
||||
import java.awt.image.BufferedImage
|
||||
import java.awt.Font
|
||||
import java.awt.font.FontRenderContext
|
||||
import java.awt.geom.AffineTransform
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.BasicStroke
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.PI
|
||||
|
||||
|
||||
@PluginMeta(
|
||||
author = "Pyrethus",
|
||||
description = "Draws experience globes (level progress) on experience gains.",
|
||||
version = 0.9
|
||||
)
|
||||
|
||||
|
||||
class plugin : Plugin() {
|
||||
private var xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null, null ) }
|
||||
private var hasActiveGlobes = false
|
||||
private var backgroundSprite: Sprite? = null
|
||||
private var borderSprite: Sprite? = null
|
||||
|
||||
|
||||
override fun Draw(deltaTime: Long) {
|
||||
if (!hasActiveGlobes)
|
||||
return
|
||||
|
||||
var posX = API.GetWindowDimensions().width / 2
|
||||
val posY = API.GetWindowDimensions().height / 4
|
||||
|
||||
if (API.GetWindowMode() == WindowMode.FIXED) {
|
||||
posX += 60
|
||||
}
|
||||
|
||||
API.ClipRect(0, 0, posX * 2, posY * 4)
|
||||
|
||||
// update globes
|
||||
val activeGlobes = ArrayList<XPGlobe>()
|
||||
for (xpGlobe in xpGlobes) {
|
||||
val globeDelta = System.currentTimeMillis() - xpGlobe.timestamp
|
||||
if (globeDelta >= Constants.GLOBE_LIFETIME) {
|
||||
xpGlobe.timestamp = 0L // dead
|
||||
xpGlobe.textSprite = null
|
||||
}
|
||||
|
||||
if (xpGlobe.timestamp != 0L) {
|
||||
activeGlobes.add(xpGlobe) // alive
|
||||
}
|
||||
}
|
||||
|
||||
if (activeGlobes.isEmpty()) {
|
||||
hasActiveGlobes = false
|
||||
return
|
||||
}
|
||||
|
||||
val maxGlobes = if (API.GetWindowMode() == WindowMode.FIXED) Constants.MAX_GLOBES_SD else Constants.MAX_GLOBES
|
||||
val globeCount = if (activeGlobes.size > maxGlobes) maxGlobes else activeGlobes.size
|
||||
val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions()
|
||||
val globeSize = backgroundSize + globeBorder * 2 + xpBorder * 2
|
||||
val allGlobesWidth = globeCount * (globeSize + Constants.GLOBE_PADDING) - Constants.GLOBE_PADDING
|
||||
var globePosX = posX - (allGlobesWidth / 2)
|
||||
|
||||
// render globes
|
||||
activeGlobes.take(globeCount).forEach { xpGlobe ->
|
||||
drawXpGlobe(xpGlobe, globePosX)
|
||||
globePosX += globeSize + Constants.GLOBE_PADDING // Update globePosX for the next globe
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun OnXPUpdate(skillId: Int, xp: Int) {
|
||||
val xpGlobe = xpGlobes[skillId]
|
||||
if (xpGlobe.xp == Constants.INVALID_XP) {
|
||||
xpGlobe.xp = xp
|
||||
return
|
||||
}
|
||||
|
||||
if (xp == xpGlobe.xp) {
|
||||
return
|
||||
}
|
||||
|
||||
val prevXp = xpGlobe.xp
|
||||
xpGlobe.xp = xp
|
||||
val (prevLevel, _) = XPTable.getLevelForXp(prevXp)
|
||||
val (level, gainedXp) = XPTable.getLevelForXp(xp)
|
||||
|
||||
// we do not draw XP globes for level >= MAX_LEVEL
|
||||
if (level != Constants.INVALID_LEVEL && level < Constants.MAX_LEVEL) {
|
||||
var arcWeight = 1.0
|
||||
var arcColor = Constants.GLOBE_XP_ARC_LEVEL_UP_COLOR
|
||||
val hasLeveledUp = level != prevLevel
|
||||
|
||||
if (!hasLeveledUp) {
|
||||
val levelXpDiff = XPTable.getXpRequiredForLevel(level + 1) - XPTable.getXpRequiredForLevel(level)
|
||||
arcWeight = gainedXp.toDouble() / levelXpDiff.toDouble()
|
||||
arcColor = Constants.GLOBE_XP_ARC_COLOR
|
||||
}
|
||||
|
||||
val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions()
|
||||
val globeSize = backgroundSize + globeBorder * 2 + xpBorder * 2
|
||||
if (xpGlobe.textSprite == null) {
|
||||
// update arcSprite and timestamp only if the textSprite has finished the animation
|
||||
xpGlobe.arcSprite = createArcSprite(backgroundSize + xpBorder * 2, arcColor, arcWeight)
|
||||
xpGlobe.timestamp = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
if (borderSprite == null) {
|
||||
borderSprite = createArcSprite(globeSize, Constants.GLOBE_BORDER_COLOR, 1.0)
|
||||
}
|
||||
|
||||
if (backgroundSprite == null) {
|
||||
backgroundSprite = createArcSprite(backgroundSize, Constants.GLOBE_BKG_COLOR, 1.0)
|
||||
}
|
||||
|
||||
if (hasLeveledUp) {
|
||||
val fontSize = Constants.GLOBE_TEXT_SIZE
|
||||
val fgColor = Constants.GLOBE_TEXT_FG_COLOR
|
||||
val bgColor = Constants.GLOBE_TEXT_BG_COLOR
|
||||
val outlineSize = Constants.GLOBE_TEXT_OUTLINE_WIDTH
|
||||
xpGlobes[skillId].textSprite = createTextSprite(fontSize, fgColor, bgColor, outlineSize, level.toString())
|
||||
xpGlobe.timestamp = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
hasActiveGlobes = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun OnLogout() {
|
||||
hasActiveGlobes = false
|
||||
xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null, null) }
|
||||
}
|
||||
|
||||
|
||||
data class XPGlobe(val skillId: Int, var xp: Int, var timestamp: Long, var arcSprite: Sprite?, var textSprite: Sprite?)
|
||||
|
||||
|
||||
private fun getGlobeDimensions() : Triple<Int, Int, Int> {
|
||||
val backgroundSize = Constants.GLOBE_BKG_SIZE
|
||||
val globeBorder = Constants.GLOBE_BORDER_WIDTH
|
||||
val xpBorder: Int = Constants.GLOBE_XP_ARC_WIDTH
|
||||
return Triple(backgroundSize, globeBorder, xpBorder)
|
||||
}
|
||||
|
||||
|
||||
private fun drawXpGlobe(globe: XPGlobe, posX: Int, posY: Int = Constants.GLOBES_Y_OFFSET) {
|
||||
|
||||
val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions()
|
||||
val totalBorder = globeBorder + xpBorder
|
||||
|
||||
// rendering background
|
||||
borderSprite?.render(posX, posY)
|
||||
globe.arcSprite?.render(posX + globeBorder, posY + globeBorder)
|
||||
backgroundSprite?.render(posX + totalBorder, posY + totalBorder)
|
||||
|
||||
// rendering skill sprite
|
||||
val skillSprite = XPSprites.getSpriteForSkill(globe.skillId)
|
||||
|
||||
val spriteWidth = skillSprite?.anInt1860 ?: 0 // sprite width without trimmed pixels
|
||||
val spriteHeight = skillSprite?.anInt1866 ?: 0 // sprite height without trimmed pixels
|
||||
val xOffset = (backgroundSize - spriteWidth) / 2
|
||||
val yOffset = (backgroundSize - spriteHeight) / 2
|
||||
|
||||
val drawX = posX + totalBorder + xOffset
|
||||
val drawY = posY + totalBorder + yOffset
|
||||
|
||||
// even if the centering logic is correct, the sprite seems not to be well-centered inside the
|
||||
// graphic resource. Manually adjust...
|
||||
val (spriteXOffset, spriteYOffset) = XPSprites.getSpriteOffsetForSkill(globe.skillId)
|
||||
|
||||
skillSprite?.render(drawX + spriteXOffset, drawY + spriteYOffset)
|
||||
|
||||
// rendering level-up text animation
|
||||
if (globe.textSprite != null) {
|
||||
val clamp: (Float, Float, Float) -> Float = { value, min, max ->
|
||||
when {
|
||||
value < min -> min
|
||||
value > max -> max
|
||||
else -> value
|
||||
}
|
||||
}
|
||||
|
||||
val lerp: (Float, Float, Float) -> Float = { valA, valB, factor ->
|
||||
valA * (1.0F - factor) + (valB * factor)
|
||||
}
|
||||
|
||||
val currentTime = System.currentTimeMillis()
|
||||
var animWeight = (currentTime - globe.timestamp).toFloat() / Constants.GLOBE_LIFETIME
|
||||
animWeight = clamp(animWeight, 0.0F, 1.0F)
|
||||
// sample cosine for the text pulse animation effect (cosine phase-shift is to respect number of pulses)
|
||||
var pulseWeight = cos(PI + animWeight * (PI * 2 * Constants.GLOBE_TEXT_PULSES).toFloat()).toFloat()
|
||||
pulseWeight = (pulseWeight + 1.0F) / 2.0F // map pulseWeight from [-1,1] to [0,1]
|
||||
val pulseIntensity = lerp(Constants.GLOBE_TEXT_PULSE_LOW, Constants.GLOBE_TEXT_PULSE_HIGH, pulseWeight)
|
||||
|
||||
val globeSize = backgroundSize + globeBorder * 2 + xpBorder * 2
|
||||
|
||||
val textWidth = globe.textSprite?.anInt1860 ?: 0
|
||||
val textHeight = globe.textSprite?.anInt1866 ?: 0
|
||||
val textXOffset = (globeSize - textWidth) / 2
|
||||
val textYOffset = (globeSize - textHeight) / 2
|
||||
|
||||
val textX = posX + textXOffset + (Constants.GLOBE_TEXT_OUTLINE_WIDTH / 2)
|
||||
val textY = posY + textYOffset + (Constants.GLOBE_TEXT_OUTLINE_WIDTH / 2)
|
||||
|
||||
val alpha = lerp(0f, 255f, pulseIntensity).toInt()
|
||||
globe.textSprite?.renderAlpha(textX, textY, alpha)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTextSprite(size: Int, fgColor: Color, bgColor: Color, outlineSize: Int, text: String) : Sprite {
|
||||
return SpritePNGLoader.getImageIndexedSprite(createTextImage(size, fgColor, bgColor, outlineSize, text))
|
||||
}
|
||||
|
||||
private fun createTextImage(fontSize: Int, fgColor: Color, bgColor: Color, outlineSize: Int, text: String) : BufferedImage {
|
||||
// Create a dummy font to calculate the size of the BufferedImage
|
||||
val font = Font("Arial", Font.PLAIN, fontSize)
|
||||
val affineTransform = AffineTransform()
|
||||
val frc = FontRenderContext(affineTransform, true, true)
|
||||
val textWidth = font.getStringBounds(text, frc).width.toInt() + outlineSize
|
||||
val textHeight = font.getStringBounds(text, frc).height.toInt() + outlineSize
|
||||
|
||||
// Create an image that can contain the text
|
||||
val image = BufferedImage(textWidth, textHeight, BufferedImage.TYPE_INT_ARGB)
|
||||
val graphics = image.createGraphics() as Graphics2D
|
||||
|
||||
// Enable antialiasing for smoother text
|
||||
graphics.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
|
||||
// Set the font
|
||||
graphics.font = font
|
||||
|
||||
// Calculate position for the text
|
||||
val metrics = graphics.getFontMetrics(font)
|
||||
val x = 0
|
||||
val y = metrics.ascent
|
||||
|
||||
// Create a glyph vector for the outline
|
||||
val glyphVector = font.createGlyphVector(frc, text)
|
||||
val shape = glyphVector.getOutline(x.toFloat(), y.toFloat())
|
||||
|
||||
// Draw the outline (background color)
|
||||
graphics.color = bgColor
|
||||
graphics.stroke = BasicStroke(outlineSize.toFloat()) // Set the outline width
|
||||
graphics.draw(shape)
|
||||
|
||||
// Fill the text (foreground color)
|
||||
graphics.color = fgColor
|
||||
graphics.fill(shape)
|
||||
|
||||
// Dispose graphics to release resources
|
||||
graphics.dispose()
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
|
||||
private fun createArcSprite(size: Int, arcColor: Color, weight: Double): Sprite {
|
||||
return SpritePNGLoader.getImageIndexedSprite(createArcImage(size, arcColor, weight))
|
||||
}
|
||||
|
||||
|
||||
private fun createArcImage(size: Int, arcColor: Color, weight: Double): BufferedImage {
|
||||
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
|
||||
val graphics = image.createGraphics()
|
||||
|
||||
// Enable antialiasing for smoother circle edges
|
||||
graphics.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
|
||||
// Set the color for the circle
|
||||
graphics.color = arcColor
|
||||
|
||||
// Calculate the angle and starting angle based on the weight
|
||||
val angle = weight * 360
|
||||
val startAngle = 360 * (0.75 - weight)
|
||||
|
||||
// Draw the portion of the circle
|
||||
graphics.fill(Arc2D.Double(0.0, 0.0, size.toDouble(), size.toDouble(), startAngle, angle, Arc2D.PIE))
|
||||
|
||||
// Dispose graphics to release resources
|
||||
graphics.dispose()
|
||||
|
||||
return image
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue