Added AddOneshotBeatEvent.

This commit is contained in:
2026-03-08 18:58:59 +08:00
parent 9769fda79e
commit 9d20d87035
4 changed files with 349 additions and 0 deletions
@@ -0,0 +1,46 @@
package cn.rdlevel.rdkt.core.data
import kotlin.jvm.JvmStatic
/**
* Represents the freeze or burn modifier applied to a oneshot pulse.
* Used in [cn.rdlevel.rdkt.core.events.AddOneshotBeatEvent].
*/
public sealed class OneshotFreezeBurn(public val type: String) {
/**
* Neither freeze nor burn modifier is applied to the oneshot pulse.
*/
public data object None : OneshotFreezeBurn("None")
/**
* The oneshot pulse is frozen and the actual hit point is delayed for a certain duration.
*
* @param delay The duration of the delay in beats.
*/
public data class Freezeshot(val delay: Double) : OneshotFreezeBurn("Freezeshot")
/**
* The oneshot pulse is burned and the actual hit point is advanced by a certain duration.
*/
public data object Burnshot : OneshotFreezeBurn("Burnshot")
public companion object {
/**
* Returns an instance of [None], indicating that neither freeze nor burn modifier is applied to the oneshot pulse.
*/
@JvmStatic
public fun none(): None = None
/**
* Returns an instance of [Freezeshot] with the specified delay, indicating that the oneshot pulse is frozen and the actual hit point is delayed for the given duration.
*/
@JvmStatic
public fun freezeshot(delay: Double): Freezeshot = Freezeshot(delay)
/**
* Returns an instance of [Burnshot], indicating that the oneshot pulse is burned and the actual hit point is advanced by a certain duration.
*/
@JvmStatic
public fun burnshot(): Burnshot = Burnshot
}
}
@@ -0,0 +1,63 @@
package cn.rdlevel.rdkt.core.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
/**
* Represents the hold settings of a oneshot pulse.
* Used in [cn.rdlevel.rdkt.core.events.AddOneshotBeatEvent].
*/
public sealed class OneshotHold {
/**
* The oneshot pulse is not holdshot.
*/
public data object Off : OneshotHold()
/**
* The oneshot pulse is holdshot.
*
* @property cueType The type of the holdshot cue.
*/
public data class On(var cueType: CueType = CueType.AUTO) : OneshotHold()
/**
* Represents when the holdshot cue is cued.
*/
@Serializable
public enum class CueType {
/**
* The time of holdshot cue is automatically determined.
*/
@SerialName("Auto")
AUTO,
/**
* The cue is cued on the end of the previous oneshot.
*/
@SerialName("Early")
EARLY,
/**
* The cue is cued on the start of the pulse of the holdshot.
*/
@SerialName("Late")
LATE,
}
public companion object {
/**
* Returns an instance of [Off], indicating that the oneshot pulse is not holdshot.
*/
@JvmStatic
public fun off(): Off = Off
/**
* Returns an instance of [On] with the specified cue type, indicating that the oneshot pulse is holdshot with the given cue type.
*/
@JvmStatic
@JvmOverloads
public fun on(cueType: CueType = CueType.AUTO): On = On(cueType)
}
}
@@ -0,0 +1,99 @@
package cn.rdlevel.rdkt.core.data
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
/**
* Represents a oneshot pulse with its settings.
* Used in [cn.rdlevel.rdkt.core.events.AddOneshotBeatEvent].
*/
public sealed class OneshotPulse(
public val pulseType: String,
) {
/**
* The number of subdivisions for the pulse. Must be between 1 and 10.
* For normal waves this is defaulted to 1.
*/
public abstract val subdivisions: Int
/**
* Represent a oneshot that can have subdivisions and sound settings.
*
* @property sound Whether the pulse should have special sounds. Default is true.
*/
public sealed class OneshotSubdivisionPulse(
pulseType: String,
public var sound: Boolean = true
) : OneshotPulse(pulseType)
/**
* Represents a simple oneshot pulse without subdivisions or sound settings.
*/
public object Wave : OneshotPulse("Wave") {
override val subdivisions: Int
get() = 1
}
/**
* Represents a heart-shaped oneshot pulse without subdivisions or sound settings.
*/
public object Heart : OneshotPulse("Heart") {
override val subdivisions: Int
get() = 1
}
/**
* Represents a oneshot pulse with a single subdivision (squareshot) and sound settings.
*/
public class Square(sound: Boolean) : OneshotSubdivisionPulse("Square", sound) {
override val subdivisions: Int
get() = 1
}
/**
* Represents a oneshot pulse with multiple subdivisions (triangleshot) and sound settings.
*/
public class Triangle(subdivisions: Int, sound: Boolean) : OneshotSubdivisionPulse("Triangle", sound) {
override var subdivisions: Int = subdivisions
set(value) {
require(value in 1..10) { "Subdivisions must be between 1 and 10." }
field = value
}
init {
this.subdivisions = subdivisions
}
}
public companion object {
/**
* Returns a [Wave] oneshot pulse, which has no subdivisions or sound settings.
*/
@JvmStatic
public fun wave(): Wave = Wave
/**
* Returns a [Heart] oneshot pulse, which has no subdivisions or sound settings.
*/
public fun heart(): Heart = Heart
/**
* Returns either a [Square] or a [Triangle] based on the number of subdivisions.
*/
@JvmStatic
@JvmOverloads
public fun subdivision(subdivisions: Int, sound: Boolean = true): OneshotSubdivisionPulse =
when (subdivisions) {
1 -> Square(sound)
in 2..10 -> Triangle(subdivisions, sound)
else -> throw IllegalArgumentException("Subdivisions must be between 1 and 10.")
}
internal fun from(type: String, subdivisions: Int, sound: Boolean): OneshotPulse = when (type) {
"Wave" -> wave()
"Square" -> subdivision(1, sound)
"Triangle" -> subdivision(subdivisions, sound)
else -> throw IllegalArgumentException("Unknown pulse type: $type")
}
}
}
@@ -0,0 +1,141 @@
@file:OptIn(RDKTInternalAPI::class)
package cn.rdlevel.rdkt.core.events
import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI
import cn.rdlevel.rdkt.core.data.OneshotFreezeBurn
import cn.rdlevel.rdkt.core.data.OneshotHold
import cn.rdlevel.rdkt.core.data.OneshotPulse
import cn.rdlevel.rdkt.core.serialization.TransformSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmStatic
/**
* Adds a oneshot pulse to the specific row.
*
* @property pulse The type of the oneshot pulse to be added.
* @property freezeBurnMode The freeze or burn modifier applied to the oneshot pulse.
* @property hold The hold behavior of the oneshot pulse.
* @property skip Whether the oneshot pulse is a skipshot.
*/
@Serializable(AddOneshotBeatEvent.Serializer::class)
@SerialName("AddOneshotBeat")
public data class AddOneshotBeatEvent(
var pulse: OneshotPulse = OneshotPulse.Wave,
var freezeBurnMode: OneshotFreezeBurn = OneshotFreezeBurn.None,
var hold: OneshotHold = OneshotHold.Off,
var skip: Boolean = false,
) : RowEvent() {
/**
* The time between the pulse appears and the actual hit point in beats.
* Must be non-negative.
*/
var tick: Double = 1.0
set(value) {
require(value >= 0) { "Tick must be non-negative." }
field = value
}
/**
* The interval between every two hits with normal looped oneshots in beats.
* Must be non-negative.
* This value is only used if this is a skipshot, freezeshot, burnshot, holdshot, or [loops] is positive.
*/
var interval: Double = 2.0
set(value) {
require(value >= 0) { "Interval must be non-negative." }
field = value
}
/**
* How many repetitions of the oneshot pattern will create. Must be non-negative.
* Setting this to 0 means the oneshot will only occur once.
*/
var loops: Int = 0
set(value) {
require(value >= 0) { "Loops must be non-negative." }
field = value
}
public object Serializer :
TransformSerializer<AddOneshotBeatEvent, Serializer.Data>("AddOneshotBeat", Data.serializer()) {
override fun toData(value: AddOneshotBeatEvent): Data {
return Data.fromBase(value).apply {
pulseType = value.pulse.pulseType
freezeBurnMode = value.freezeBurnMode.type
interval = value.interval
tick = value.tick
delay = (value.freezeBurnMode as? OneshotFreezeBurn.Freezeshot)?.delay
loops = value.loops
subdivisions = value.pulse.subdivisions
subdivSound = (value.pulse as? OneshotPulse.OneshotSubdivisionPulse)?.sound
skipshot = value.skip
hold = value.hold is OneshotHold.On
holdCue = (value.hold as? OneshotHold.On)?.cueType
}
}
override fun fromData(data: Data): AddOneshotBeatEvent {
val pulse = OneshotPulse.from(data.pulseType, data.subdivisions, data.subdivSound ?: true)
val freezeBurnMode = when (data.freezeBurnMode) {
"None", null -> OneshotFreezeBurn.None
"Freezeshot" -> OneshotFreezeBurn.Freezeshot(
data.delay ?: error("Delay must be provided for Freezeshot mode.")
)
"Burnshot" -> OneshotFreezeBurn.Burnshot
else -> throw IllegalArgumentException("Unknown freeze/burn mode: ${data.freezeBurnMode}")
}
val hold = if (data.hold == true) {
OneshotHold.On(data.holdCue ?: OneshotHold.CueType.AUTO)
} else {
OneshotHold.Off
}
return AddOneshotBeatEvent(
pulse = pulse,
freezeBurnMode = freezeBurnMode,
hold = hold,
skip = data.skipshot ?: false,
).apply {
copyBaseFrom(data)
this.interval = data.interval
this.tick = data.tick
this.loops = data.loops
}
}
@Serializable
@RDKTInternalAPI
public data class Data(
var pulseType: String = "",
var freezeBurnMode: String? = null,
var interval: Double = 2.0,
var tick: Double = 1.0,
var delay: Double? = 0.5,
var loops: Int = 0,
var subdivisions: Int = 0,
var subdivSound: Boolean? = true,
var skipshot: Boolean? = false,
var hold: Boolean? = false,
var holdCue: OneshotHold.CueType? = OneshotHold.CueType.AUTO,
) : RowEvent() {
public companion object {
public fun fromBase(other: RowEvent): Data {
return Data().apply { copyBaseFrom(other) }
}
}
}
}
public companion object {
/**
* Creates a new [AddOneshotBeatEvent] using the provided block to configure its properties.
*/
@JvmStatic
public fun of(block: AddOneshotBeatEvent.() -> Unit): AddOneshotBeatEvent {
return AddOneshotBeatEvent().apply(block)
}
}
}