Skip to content

Commit

Permalink
✨ feat: Drawing image for gaming events
Browse files Browse the repository at this point in the history
🐛 Fix: User may die twice
  • Loading branch information
HatoYuze committed Dec 21, 2024
1 parent 6f0cd8c commit 80bfe86
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 28 deletions.
Binary file added docs_img/talents-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions src/main/kotlin/draw/GameLayoutDrawer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package com.github.hatoyuze.restarter.draw

import com.github.hatoyuze.restarter.PluginMain
import com.github.hatoyuze.restarter.game.data.Talent
import com.github.hatoyuze.restarter.game.entity.Life
import com.github.hatoyuze.restarter.mirai.ResourceManager
import com.github.hatoyuze.restarter.mirai.ResourceManager.newCacheFile
import com.github.hatoyuze.restarter.mirai.config.GameConfig
import org.jetbrains.skia.Data
import org.jetbrains.skia.EncodedImageFormat
import org.jetbrains.skia.Font
import org.jetbrains.skia.FontMgr
import java.io.File
Expand All @@ -18,6 +20,15 @@ object GameLayoutDrawer {
}
}

fun createGamingImage(life: Life): File {
val surface = GameProgressLayoutDrawer(font, life).draw()
return newCacheFile("life-${life.hashCode()}.png").also {
it.writeBytes(
surface.makeImageSnapshot().encodeToData(EncodedImageFormat.PNG)?.bytes ?: byteArrayOf(0)
)
}
}

// static resource. shouldn't be closed
val font by lazy {
val inputStream =
Expand All @@ -35,6 +46,8 @@ object GameLayoutDrawer {
)
}

val fontChineseLetterWidth = font.apply { size = 24f }.measureTextWidth("")

const val BACKGROUND_COLOR_4F = 0xFF_222831.toInt()

}
152 changes: 152 additions & 0 deletions src/main/kotlin/draw/GameProgressLayoutDrawer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.github.hatoyuze.restarter.draw

import com.github.hatoyuze.restarter.game.entity.ExecutedEvent
import com.github.hatoyuze.restarter.game.entity.ExecutedEvent.Companion.linesCount
import com.github.hatoyuze.restarter.game.entity.Life
import org.jetbrains.skia.*
import org.jetbrains.skia.RRect.Companion.makeXYWH

class GameProgressLayoutDrawer(
private val font: Font,
life0: Life
) {
private val life = life0.toList().map { it.warpString() }
private val textLineHeight by lazy {
font.also {
it.size = 24f
it.edging = FontEdging.ANTI_ALIAS
}.measureText("你出生了。").let { it.bottom - it.top }
}
private val textWeight =
font.also {
it.size = 24f
it.edging = FontEdging.ANTI_ALIAS
}.measureTextWidth("第 500 岁")

private val surface by lazy {
fun getLifeLines(): Int {
return life.sumOf { it.linesCount() }
}
Surface.makeRasterN32Premul(
1000,
(getLifeLines() * (textLineHeight + 10) + life.size * 35 + INIT_EVENT_MESSAGE_Y + 50).toInt()
)
}
private val canvas = surface.canvas
val paint = Paint {
color = Color.WHITE
isAntiAlias = true
}
private var lastY = INIT_EVENT_MESSAGE_Y


private fun drawBackground() {
val paint = Paint {
color4f = Color4f(BACKGROUND_COLOR4F)
}
val rect = Rect.makeWH(surface.width.toFloat(), surface.height.toFloat())
canvas.drawRect(rect, paint)

paint.color4f = Color4f(EVENT_BACKGROUND_COLOR4F)
canvas.drawRRectWithEdge(
backgroundColor4f = Color4f(EVENT_BACKGROUND_COLOR4F),
rRect = makeXYWH(
30f,
INIT_EVENT_MESSAGE_Y,
surface.width - 2 * MESSAGE_START_X,
surface.height - INIT_EVENT_MESSAGE_Y - 50f,
15f
)
)
}


fun draw(): Surface {
drawBackground()

lastY += 10f
for ((index, event) in life.withIndex()) {
drawNextEntry(index + 1, event)
}

return surface
}

private fun drawNextEntry(age: Int, currentEvent: ExecutedEvent) {
lastY += 20
val messageLineStartX = MESSAGE_START_X + textWeight

fun drawEventLine(eventName: String, isAppendLine: Boolean = true) {
var isFirst = true
eventName.split('\n').onEach {
if (isAppendLine && isFirst)
lastY += textLineHeight + 10
canvas.drawString(it, messageLineStartX, lastY, font, paint)
isFirst = false
}
}

val heightRange = currentEvent.linesCount() * (textLineHeight + 10)

// Message background
canvas.drawRRectWithEdge(
backgroundColor4f = Color4f(eventGradeColor4F[currentEvent.mainEvent.grade]),
rRect = makeXYWH(30f, lastY, surface.width - 2 * MESSAGE_START_X, heightRange + 15, 3f),
initFont = paint
)
paint.color4f = Color4f(Color.WHITE)
lastY += textLineHeight + 10
canvas.drawString("$age", MESSAGE_START_X, lastY, font, paint)
drawEventLine(currentEvent.mainEvent.eventName, false)

currentEvent.subEvents.onEach {
drawEventLine(it.eventName)
}

lastY += 15
}


companion object {
const val INIT_EVENT_MESSAGE_Y = 35f
const val MESSAGE_START_X = 50f
const val EVENT_BACKGROUND_COLOR4F = 0xFF_383D45.toInt()
const val BACKGROUND_COLOR4F = 0xFF_222831.toInt()


private val maxLetterCountOneLine = (850 / GameLayoutDrawer.fontChineseLetterWidth).toInt()
private fun String.wrapString(): String {
if (length <= maxLetterCountOneLine) return this
val sb = StringBuilder(length + (length / maxLetterCountOneLine))
var lineStart = 0

for (i in indices) {
if (i - lineStart == maxLetterCountOneLine) {
sb.append(substring(lineStart, i + 1)).append("\n")
lineStart = i + 1
}
}

sb.append(substring(lineStart))
return sb.toString()
}

fun ExecutedEvent.warpString(): ExecutedEvent {
return ExecutedEvent(
mainEvent.copy(eventName = mainEvent.eventName.wrapString()),
subEvents.map { it.copy(eventName = it.eventName.wrapString()) }
)
}

private val eventGradeColor4F = listOf(
//grade = 0 灰色
0XFF_454545.toInt(),
//grade = 1 蓝色
0Xff_6494EC.toInt(),
//grade = 2 紫色
0xff_E5BDFD.toInt(),
//grade = 3 橙色
0xff_FE7878.toInt()
)
}
}
15 changes: 4 additions & 11 deletions src/main/kotlin/draw/TalentLayoutDrawer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,10 @@ class TalentLayoutDrawer(
isAntiAlias = true
}

val edgePaint = initFont.apply {
strokeWidth = 4f
mode = PaintMode.STROKE
}
canvas.drawRRect(range, edgePaint)

val backgroundPaint = initFont.apply {
color4f = Color4f(talentsGradeColor4F[talent.grade])
mode = PaintMode.FILL
}
canvas.drawRRect(range, backgroundPaint)
canvas.drawRRectWithEdge(
backgroundColor4f = Color4f(talentsGradeColor4F[talent.grade]),
rRect = range
)

val font = font.apply {
size = 24f
Expand Down
31 changes: 28 additions & 3 deletions src/main/kotlin/draw/util.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.github.hatoyuze.restarter.draw

import org.jetbrains.skia.EncodedImageFormat
import org.jetbrains.skia.Paint
import org.jetbrains.skia.Surface
import org.jetbrains.skia.*
import java.io.File


Expand All @@ -23,4 +21,31 @@ suspend fun createImage(output: File, width: Int, height: Int, block: suspend Su
surface.close()

return output
}

/**
* the color of [initFont] will be changed to [backgroundColor4f]
* */
fun Canvas.drawRRectWithEdge(
backgroundColor4f: Color4f,
edgeColor4f: Color4f = Color4f(0xFF_FF_FF_FF.toInt()),
rRect: RRect,
initFont: Paint = Paint {
color4f = edgeColor4f
isAntiAlias = true
}
) {


val edgePaint = initFont.apply {
strokeWidth = 4f
mode = PaintMode.STROKE
}
drawRRect(rRect, edgePaint)

val backgroundPaint = initFont.apply {
color4f = backgroundColor4f
mode = PaintMode.FILL
}
drawRRect(rRect, backgroundPaint)
}
8 changes: 6 additions & 2 deletions src/main/kotlin/game/entity/Life.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,14 @@ data class Life(
userEvent.applyEffect(this)

userEvent.branch.let { branches ->
branches.forEach { (condition, refer) ->
if (refer == -1 || refer == 0) return@forEach
branches.forEach branch@{ (condition, refer) ->
if (refer == -1 || refer == 0) return@branch
if (condition.judgeAll(this)) {
finishUserEvent(refer).forEach {
userEventList.add(it)
if (it.first.id == DEAD_EVENT_ID) {
return@branch
}
}
}
}
Expand Down Expand Up @@ -132,5 +135,6 @@ data class Life(

companion object {
val Life.talents: List<Talent> get() = property.attribute.talents.map { Talent.data[it]!! }
private const val DEAD_EVENT_ID = 10000
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/game/entity/element.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ data class ExecutedEvent(
}
deleteAt(lastIndex)
}

companion object {
private fun UserEvent.lineCount() = eventName.split('\n').size
fun ExecutedEvent.linesCount() = mainEvent.lineCount() + subEvents.sumOf { sub -> sub.lineCount() }
}
}
16 changes: 4 additions & 12 deletions src/main/kotlin/mirai/RestartLifeCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,11 @@ ${engine.life.talents.joinToString("\n") { it.introduction }}
""".trimIndent()
)
)
for ((base, i) in lifeList.chunked(20).withIndex()) {
add(
sender = commandContext.sender.user!!,
message = PlainText(
buildString {
for ((offset, event) in i.withIndex()) {
val age = (base * 20) + offset
appendLine("${age}岁:${event.name}")
}
}
)
)

val image = GameLayoutDrawer.createGamingImage(engine.life).toExternalResource().use {
subject.uploadImage(it)
}
add(sender = commandContext.sender.user!!, message = image)
add(
sender = commandContext.sender.user!!, message = PlainText("以上为模拟结果。以下为结算:")
)
Expand Down
28 changes: 28 additions & 0 deletions src/test/kotlin/Drawer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package com.github.hatoyuze.restarter

import com.github.hatoyuze.restarter.draw.GameLayoutDrawer
import com.github.hatoyuze.restarter.game.LifeEngine
import com.github.hatoyuze.restarter.game.entity.Attribute
import com.github.hatoyuze.restarter.game.entity.Life
import com.github.hatoyuze.restarter.game.entity.TalentManager
import kotlinx.coroutines.runBlocking
import kotlin.system.measureTimeMillis
import kotlin.test.Test
Expand All @@ -23,4 +26,29 @@ class Drawer {

}

@Test
fun game() {
TestGame().data()
TalentManager.talentRandom(3).map { it.id }.toMutableList()
val life = Life()
life.restartLife(
Attribute(
-1,
(0..15).random(),
(0..15).random(),
(0..15).random(),
(0..15).random(),
(0..15).random(),
1,
)

)
measureTimeMillis {
GameLayoutDrawer.createGamingImage(life).also {
println(it.absolutePath)
}
}.also {
println("Drawing used $it ms")
}
}
}

0 comments on commit 80bfe86

Please sign in to comment.