Added explosion animation

This commit is contained in:
smok1e 2025-07-08 04:44:06 +03:00 committed by UnicornFreedom
parent 549833f3e9
commit 4209a05c62
No known key found for this signature in database
GPG Key ID: B4ED0DB6B940024F
9 changed files with 151 additions and 28 deletions

BIN
sprites/particles/Smoke.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -44,6 +44,8 @@ object SoundBuffers extends Resource {
lazy val MinecraftClickRelease: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/click_release.ogg")
lazy val MinecraftExplosion: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/explosion.ogg")
lazy val SelfDestructingCardCountdownBeep: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/countdown_beep.ogg")
lazy val NoteBlock: Map[String, SoundBuffer] = List(
"banjo", "basedrum", "bass", "bell", "bit", "chime", "cow_bell", "didgeridoo", "flute", "guitar",
"harp", "hat", "iron_xylophone", "pling", "snare", "xylophone",

View File

@ -133,6 +133,6 @@ object SoundSource {
lazy val MachineFloppyEject: SoundSource =
SoundSource.fromBuffer(SoundBuffers.MachineFloppyEject, SoundCategory.Environment)
lazy val SelfDestructingCardBeep: SoundSource =
BeepGenerator.newBeep(".", 1000, 100)
lazy val SelfDestructingCardCountdownBeep: SoundSource =
SoundSource.fromBuffer(SoundBuffers.SelfDestructingCardCountdownBeep, SoundCategory.Environment)
}

View File

@ -345,7 +345,40 @@ class Graphics(private var width: Int, private var height: Int, private var scal
animation: Option[Animation] = None): Unit = {
sprite = name
foreground = color
_rect(x, y, width, height, fixUV = true, animation)
val spriteRect = animation match {
case Some(animation) =>
val duration = animation.frames.map(_._2).sum
var timeOffset = 0f
var curFrame = 0
breakable {
for ((idx, dur) <- animation.frames) {
timeOffset += dur
curFrame = idx
if (timeOffset >= time % duration) break
}
}
val size = animation.frameSize match {
case Some(size) => Size2D(this.spriteRect.w, this.spriteRect.w * size.height / size.width)
case None => Size2D(this.spriteRect.w, this.spriteRect.w)
}
Some(this.spriteRect.copy(y = this.spriteRect.y + curFrame * size.height, h = size.height))
case None => None
}
_rect(x, y, width, height, fixUV = true, spriteRect)
}
def sprite(name: String, x: Float, y: Float, width: Float, height: Float,
color: Color,
spriteRect: Rect2D): Unit = {
sprite = name
foreground = color
_rect(x, y, width, height, fixUV = true, Some(spriteRect))
}
def rect(r: Rect2D, color: Color): Unit = {
@ -368,28 +401,8 @@ class Graphics(private var width: Int, private var height: Int, private var scal
private def _rect(x: Float, y: Float, width: Float, height: Float,
fixUV: Boolean = true,
animation: Option[Animation] = None): Unit = {
val spriteRect = animation match {
case None => this.spriteRect
case Some(animation) =>
val duration = animation.frames.map(_._2).sum
var timeOffset = 0f
var curFrame = 0
breakable {
for ((idx, dur) <- animation.frames) {
timeOffset += dur
curFrame = idx
if (timeOffset >= time % duration) break
}
}
val size = animation.frameSize match {
case Some(size) => Size2D(this.spriteRect.w, this.spriteRect.w * size.height / size.width)
case None => Size2D(this.spriteRect.w, this.spriteRect.w)
}
this.spriteRect.copy(y = this.spriteRect.y + curFrame * size.height, h = size.height)
}
spriteRectOptional: Option[Rect2D] = None): Unit = {
val spriteRect = spriteRectOptional.getOrElse(this.spriteRect)
val uvTransform = Transform2D.translate(spriteRect.x, spriteRect.y) >>
(if (fixUV)

View File

@ -367,4 +367,10 @@ object IconSource {
val InnerBorderB: IconSource = IconSource(s"$prefix/InnerBorderB")
}
// ----------------------- Particles -----------------------
object Particles {
val Smoke: IconSource = IconSource("particles/Smoke")
}
}

View File

@ -24,6 +24,7 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
with SyncedInventory
with DiskActivityHandler
with OcelotLogParticleNode
with SmokeParticleNode
with ShiftClickNode
with PositionalSoundSourcesNode {
@ -32,7 +33,10 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
private def soundCardSource: SoundSource = soundCardSounds._2
// PositionalSoundSourcesNode
override def soundSources: Seq[SoundSource] = Seq(SoundSource.MinecraftExplosion)
override def soundSources: Seq[SoundSource] = Seq(
SoundSource.MinecraftExplosion,
SoundSource.SelfDestructingCardCountdownBeep
)
eventHandlers += {
case BrainEvent(event: MachineCrashEvent) =>
@ -69,7 +73,7 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
slot.get match {
case Some(card: SelfDestructingCard) =>
if (card.time > 0 && card.time % 20 == 0) {
SoundSource.SelfDestructingCardBeep.play()
SoundSource.SelfDestructingCardCountdownBeep.play()
}
case _ =>
}

View File

@ -0,0 +1,98 @@
package ocelot.desktop.node
import ocelot.desktop.color.{Color, RGBAColorNorm}
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.BrainEvent
import ocelot.desktop.util.Spritesheet
import totoro.ocelot.brain.event.SelfDestructingCardBoomEvent
import scala.collection.mutable
import scala.util.Random
trait SmokeParticleNode extends Node {
private case class SmokeParticle(
var velocity: Vector2D = Vector2D(randomParticleVelocityComponent, randomParticleVelocityComponent),
var offset: Vector2D = Vector2D(0, 0),
var color: RGBAColorNorm = randomParticleColor,
var time: Float = 0,
var duration: Float = randomParticleDuration
)
private def randomParticleVelocityComponent: Float = Random.between(
-SmokeParticleNode.SmokeParticleVelocityRange,
SmokeParticleNode.SmokeParticleVelocityRange,
)
private def randomParticleColor: RGBAColorNorm = {
val channel = Random.between(0.7f, 1f)
RGBAColorNorm(channel, channel, channel)
}
private def randomParticleDuration: Float = Random.between(
SmokeParticleNode.SmokeParticleAnimationDuration._1,
SmokeParticleNode.SmokeParticleAnimationDuration._2
)
private val smokeParticles = mutable.ArrayDeque.empty[SmokeParticle]
eventHandlers += {
case BrainEvent(_: SelfDestructingCardBoomEvent) =>
val count = Random.between(
SmokeParticleNode.SmokeParticleCount._1,
SmokeParticleNode.SmokeParticleCount._2
)
for (_ <- 1 to count) {
smokeParticles += SmokeParticle()
}
}
override def update(): Unit = {
super.update()
smokeParticles.foreach(particle => {
particle.time += UiHandler.dt
particle.offset += (particle.velocity + SmokeParticleNode.SmokeParticleVolatilizationSpeed) * UiHandler.dt
particle.velocity *= SmokeParticleNode.SmokeParticleVelocityDamping
})
smokeParticles.filterInPlace(particle => particle.time <= particle.duration)
}
override def drawParticles(g: Graphics): Unit = {
super.drawParticles(g)
for (particle <- smokeParticles) {
val spriteRect = Spritesheet.sprites(IconSource.Particles.Smoke.path)
val animationFrameCount = spriteRect.h / spriteRect.w
val animationFrame = (particle.time / particle.duration * animationFrameCount).toInt
val particlePosition = bounds.center + particle.offset
g.sprite(
IconSource.Particles.Smoke.path,
particlePosition.x,
particlePosition.y,
SmokeParticleNode.SmokeParticleSize.width,
SmokeParticleNode.SmokeParticleSize.height,
particle.color,
spriteRect.copy(
y = spriteRect.y + animationFrame * spriteRect.w,
h = spriteRect.w
)
)
}
}
}
private object SmokeParticleNode {
private final val SmokeParticleSize: Size2D = Size2D(32, 32)
private final val SmokeParticleVelocityRange: Float = 300
private final val SmokeParticleVolatilizationSpeed: Vector2D = Vector2D(0, -50)
private final val SmokeParticleVelocityDamping: Float = .99f
private final val SmokeParticleCount: (Int, Int) = (7, 10)
private final val SmokeParticleAnimationDuration: (Float, Float) = (1f, 4f)
}

View File

@ -1,6 +1,6 @@
package ocelot.desktop.util
import ocelot.desktop.geometry.{Rect2D, Size2D}
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.{IconSource, Texture}
import javax.imageio.ImageIO