mirror of
https://gitlab.com/2009scape/rt4-client.git
synced 2025-12-09 16:45:46 -07:00
animate globe on level-up
This commit is contained in:
parent
e8bed52163
commit
e0cb4568a1
2 changed files with 133 additions and 18 deletions
|
|
@ -8,7 +8,7 @@ object Constants {
|
||||||
const val MAX_LEVEL = 99
|
const val MAX_LEVEL = 99
|
||||||
const val INVALID_LEVEL = -1
|
const val INVALID_LEVEL = -1
|
||||||
const val INVALID_XP = -1
|
const val INVALID_XP = -1
|
||||||
const val GLOBE_LIFETIME = 5000L // 5 seconds
|
const val GLOBE_LIFETIME = 7000L // 7 seconds
|
||||||
const val GLOBES_Y_OFFSET = 48 // y-offset in screen space where globes are drawn
|
const val GLOBES_Y_OFFSET = 48 // y-offset in screen space where globes are drawn
|
||||||
val GLOBE_BKG_COLOR: Color = Color.GRAY
|
val GLOBE_BKG_COLOR: Color = Color.GRAY
|
||||||
const val GLOBE_BKG_SIZE = 33 // Size of the globe background
|
const val GLOBE_BKG_SIZE = 33 // Size of the globe background
|
||||||
|
|
@ -20,4 +20,11 @@ object Constants {
|
||||||
const val MAX_GLOBES = 6 // maximum number of globes we will draw on resizable clients
|
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 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_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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@ import plugin.api.*
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import java.awt.geom.Arc2D
|
import java.awt.geom.Arc2D
|
||||||
import java.awt.image.BufferedImage
|
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(
|
@PluginMeta(
|
||||||
|
|
@ -18,14 +25,14 @@ import java.awt.image.BufferedImage
|
||||||
|
|
||||||
|
|
||||||
class plugin : Plugin() {
|
class plugin : Plugin() {
|
||||||
private var xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null) }
|
private var xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null, null ) }
|
||||||
private var lastGain = 0L
|
private var hasActiveGlobes = false
|
||||||
private var backgroundSprite: Sprite? = null
|
private var backgroundSprite: Sprite? = null
|
||||||
private var borderSprite: Sprite? = null
|
private var borderSprite: Sprite? = null
|
||||||
|
|
||||||
|
|
||||||
override fun Draw(deltaTime: Long) {
|
override fun Draw(deltaTime: Long) {
|
||||||
if (System.currentTimeMillis() - lastGain >= Constants.GLOBE_LIFETIME)
|
if (!hasActiveGlobes)
|
||||||
return
|
return
|
||||||
|
|
||||||
var posX = API.GetWindowDimensions().width / 2
|
var posX = API.GetWindowDimensions().width / 2
|
||||||
|
|
@ -43,6 +50,7 @@ class plugin : Plugin() {
|
||||||
val globeDelta = System.currentTimeMillis() - xpGlobe.timestamp
|
val globeDelta = System.currentTimeMillis() - xpGlobe.timestamp
|
||||||
if (globeDelta >= Constants.GLOBE_LIFETIME) {
|
if (globeDelta >= Constants.GLOBE_LIFETIME) {
|
||||||
xpGlobe.timestamp = 0L // dead
|
xpGlobe.timestamp = 0L // dead
|
||||||
|
xpGlobe.textSprite = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xpGlobe.timestamp != 0L) {
|
if (xpGlobe.timestamp != 0L) {
|
||||||
|
|
@ -50,6 +58,11 @@ class plugin : Plugin() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeGlobes.isEmpty()) {
|
||||||
|
hasActiveGlobes = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val maxGlobes = if (API.GetWindowMode() == WindowMode.FIXED) Constants.MAX_GLOBES_SD else Constants.MAX_GLOBES
|
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 globeCount = if (activeGlobes.size > maxGlobes) maxGlobes else activeGlobes.size
|
||||||
val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions()
|
val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions()
|
||||||
|
|
@ -66,18 +79,19 @@ class plugin : Plugin() {
|
||||||
|
|
||||||
|
|
||||||
override fun OnXPUpdate(skillId: Int, xp: Int) {
|
override fun OnXPUpdate(skillId: Int, xp: Int) {
|
||||||
if (xpGlobes[skillId].xp == Constants.INVALID_XP) {
|
val xpGlobe = xpGlobes[skillId]
|
||||||
xpGlobes[skillId].xp = xp
|
if (xpGlobe.xp == Constants.INVALID_XP) {
|
||||||
|
xpGlobe.xp = xp
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xp == xpGlobes[skillId].xp) {
|
if (xp == xpGlobe.xp) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val prevXp = xpGlobes[skillId].xp
|
val prevXp = xpGlobe.xp
|
||||||
xpGlobes[skillId].xp = xp
|
xpGlobe.xp = xp
|
||||||
xpGlobes[skillId].timestamp = 0L
|
xpGlobe.timestamp = 0L
|
||||||
val (prevLevel, _) = XPTable.getLevelForXp(prevXp)
|
val (prevLevel, _) = XPTable.getLevelForXp(prevXp)
|
||||||
val (level, gainedXp) = XPTable.getLevelForXp(xp)
|
val (level, gainedXp) = XPTable.getLevelForXp(xp)
|
||||||
|
|
||||||
|
|
@ -85,20 +99,24 @@ class plugin : Plugin() {
|
||||||
if (level != Constants.INVALID_LEVEL && level < Constants.MAX_LEVEL) {
|
if (level != Constants.INVALID_LEVEL && level < Constants.MAX_LEVEL) {
|
||||||
var arcWeight = 1.0
|
var arcWeight = 1.0
|
||||||
var arcColor = Constants.GLOBE_XP_ARC_LEVEL_UP_COLOR
|
var arcColor = Constants.GLOBE_XP_ARC_LEVEL_UP_COLOR
|
||||||
|
var hasLeveledUp = level != prevLevel
|
||||||
|
|
||||||
if (level == prevLevel) {
|
if (!hasLeveledUp) {
|
||||||
val levelXpDiff = XPTable.getXpRequiredForLevel(level + 1) - XPTable.getXpRequiredForLevel(level)
|
val levelXpDiff = XPTable.getXpRequiredForLevel(level + 1) - XPTable.getXpRequiredForLevel(level)
|
||||||
arcWeight = gainedXp.toDouble() / levelXpDiff.toDouble()
|
arcWeight = gainedXp.toDouble() / levelXpDiff.toDouble()
|
||||||
arcColor = Constants.GLOBE_XP_ARC_COLOR
|
arcColor = Constants.GLOBE_XP_ARC_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
// remember timestamps
|
// remember timestamp
|
||||||
lastGain = System.currentTimeMillis()
|
xpGlobe.timestamp = System.currentTimeMillis()
|
||||||
xpGlobes[skillId].timestamp = lastGain
|
hasActiveGlobes = true
|
||||||
|
|
||||||
val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions()
|
val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions()
|
||||||
val globeSize = backgroundSize + globeBorder * 2 + xpBorder * 2
|
val globeSize = backgroundSize + globeBorder * 2 + xpBorder * 2
|
||||||
xpGlobes[skillId].arcSprite = createArcSprite(backgroundSize + xpBorder * 2, arcColor, arcWeight)
|
if (xpGlobe.textSprite == null) {
|
||||||
|
// update the arcSprite only if the textSprite has finished the animation
|
||||||
|
xpGlobe.arcSprite = createArcSprite(backgroundSize + xpBorder * 2, arcColor, arcWeight)
|
||||||
|
}
|
||||||
|
|
||||||
if (borderSprite == null) {
|
if (borderSprite == null) {
|
||||||
borderSprite = createArcSprite(globeSize, Constants.GLOBE_BORDER_COLOR, 1.0)
|
borderSprite = createArcSprite(globeSize, Constants.GLOBE_BORDER_COLOR, 1.0)
|
||||||
|
|
@ -106,17 +124,25 @@ class plugin : Plugin() {
|
||||||
if (backgroundSprite == null) {
|
if (backgroundSprite == null) {
|
||||||
backgroundSprite = createArcSprite(backgroundSize, Constants.GLOBE_BKG_COLOR, 1.0)
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun OnLogout() {
|
override fun OnLogout() {
|
||||||
lastGain = 0L
|
hasActiveGlobes = false
|
||||||
xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null) }
|
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?)
|
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> {
|
private fun getGlobeDimensions() : Triple<Int, Int, Int> {
|
||||||
|
|
@ -153,6 +179,88 @@ class plugin : Plugin() {
|
||||||
val (spriteXOffset, spriteYOffset) = XPSprites.getSpriteOffsetForSkill(globe.skillId)
|
val (spriteXOffset, spriteYOffset) = XPSprites.getSpriteOffsetForSkill(globe.skillId)
|
||||||
|
|
||||||
skillSprite?.render(drawX + spriteXOffset, drawY + spriteYOffset)
|
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 offset is to respect number of pulses)
|
||||||
|
var pulseWeight = cos(animWeight * (PI + PI * 2 * Constants.GLOBE_TEXT_PULSES).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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue