diff --git a/sprites/particles/Smoke.png b/sprites/particles/Smoke.png new file mode 100644 index 0000000..97b7cba Binary files /dev/null and b/sprites/particles/Smoke.png differ diff --git a/src/main/resources/ocelot/desktop/sounds/minecraft/countdown_beep.ogg b/src/main/resources/ocelot/desktop/sounds/minecraft/countdown_beep.ogg new file mode 100644 index 0000000..d0105c9 Binary files /dev/null and b/src/main/resources/ocelot/desktop/sounds/minecraft/countdown_beep.ogg differ diff --git a/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala b/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala index cfeee21..0a62f33 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala @@ -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", diff --git a/src/main/scala/ocelot/desktop/audio/SoundSource.scala b/src/main/scala/ocelot/desktop/audio/SoundSource.scala index 5598fec..0081f61 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundSource.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundSource.scala @@ -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) } diff --git a/src/main/scala/ocelot/desktop/graphics/Graphics.scala b/src/main/scala/ocelot/desktop/graphics/Graphics.scala index c17f164..390bb43 100644 --- a/src/main/scala/ocelot/desktop/graphics/Graphics.scala +++ b/src/main/scala/ocelot/desktop/graphics/Graphics.scala @@ -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) diff --git a/src/main/scala/ocelot/desktop/graphics/IconSource.scala b/src/main/scala/ocelot/desktop/graphics/IconSource.scala index c9c9d1e..116b13f 100644 --- a/src/main/scala/ocelot/desktop/graphics/IconSource.scala +++ b/src/main/scala/ocelot/desktop/graphics/IconSource.scala @@ -367,4 +367,10 @@ object IconSource { val InnerBorderB: IconSource = IconSource(s"$prefix/InnerBorderB") } + + // ----------------------- Particles ----------------------- + + object Particles { + val Smoke: IconSource = IconSource("particles/Smoke") + } } diff --git a/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala b/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala index 1e1264c..4a3ddc6 100644 --- a/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala +++ b/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala @@ -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 _ => } diff --git a/src/main/scala/ocelot/desktop/node/SmokeParticleNode.scala b/src/main/scala/ocelot/desktop/node/SmokeParticleNode.scala new file mode 100644 index 0000000..2898972 --- /dev/null +++ b/src/main/scala/ocelot/desktop/node/SmokeParticleNode.scala @@ -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) +} \ No newline at end of file diff --git a/src/main/scala/ocelot/desktop/util/Spritesheet.scala b/src/main/scala/ocelot/desktop/util/Spritesheet.scala index 4a65d26..4015fb3 100644 --- a/src/main/scala/ocelot/desktop/util/Spritesheet.scala +++ b/src/main/scala/ocelot/desktop/util/Spritesheet.scala @@ -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