├── gradle.properties ├── settings.gradle.kts ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── src └── main │ ├── kotlin │ └── com │ │ └── heledron │ │ └── spideranimation │ │ ├── spider │ │ ├── configuration │ │ │ ├── CloakOptions.kt │ │ │ ├── SpiderDebugOptions.kt │ │ │ ├── SpiderOptions.kt │ │ │ ├── BodyPlan.kt │ │ │ └── Gait.kt │ │ ├── setupSpider.kt │ │ ├── components │ │ │ ├── body │ │ │ │ ├── LegLookUp.kt │ │ │ │ ├── GaitType.kt │ │ │ │ ├── Leg.kt │ │ │ │ └── SpiderBody.kt │ │ │ ├── PointDetector.kt │ │ │ ├── TridentHitDetector.kt │ │ │ ├── rendering │ │ │ │ ├── renderSpiderEntities.kt │ │ │ │ ├── SpiderRenderer.kt │ │ │ │ └── spiderDebugRenderEntities.kt │ │ │ ├── splay.kt │ │ │ ├── Behaviour.kt │ │ │ ├── SoundsAndParticles.kt │ │ │ ├── Mountable.kt │ │ │ └── Cloak.kt │ │ └── presets │ │ │ ├── AnimatedPalettes.kt │ │ │ ├── applyLegModels.kt │ │ │ ├── SpiderLegModels.kt │ │ │ └── presets.kt │ │ ├── utilities │ │ ├── events │ │ │ ├── SeriesScheduler.kt │ │ │ ├── scheduler.kt │ │ │ └── events.kt │ │ ├── DisplayModel.kt │ │ ├── custom_entities │ │ │ └── customEntities.kt │ │ ├── maths │ │ │ ├── Rect.kt │ │ │ └── maths.kt │ │ ├── polygons.kt │ │ ├── overloads │ │ │ └── overloads.kt │ │ ├── block_colors │ │ │ ├── blockColors.kt │ │ │ └── blockColorMaps.kt │ │ ├── core.kt │ │ ├── parseModelFromCommand.kt │ │ ├── rendering │ │ │ ├── utilities.kt │ │ │ ├── RenderItem.kt │ │ │ └── textDisplays.kt │ │ ├── colors │ │ │ ├── oklab.kt │ │ │ └── colors.kt │ │ ├── Serializer.kt │ │ ├── custom_items │ │ │ └── customItems.kt │ │ ├── KinematicChain.kt │ │ ├── maths.kt │ │ └── ecs │ │ │ └── ecs.kt │ │ ├── AppState.kt │ │ ├── SpiderAnimationPlugin.kt │ │ ├── laser │ │ └── LaserPoint.kt │ │ ├── setupItems.kt │ │ └── kinematic_chain_visualizer │ │ └── KinematicChainVisualizer.kt │ └── resources │ └── plugin.yml ├── .gitignore └── README.md /gradle.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "spider-animation" 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 2 | -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/configuration/CloakOptions.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.configuration 2 | 3 | class CloakOptions { 4 | var moveSpeed = 1.0 / 255 5 | var lerpSpeed = .3 6 | var lerpRandomness = .3 7 | var allowCustomBrightness = true 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/events/SeriesScheduler.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.events 2 | 3 | 4 | class SeriesScheduler { 5 | var time = 0L 6 | 7 | fun sleep(time: Long) { 8 | this.time += time 9 | } 10 | 11 | fun run(task: () -> Unit) { 12 | runLater(time, task) 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/configuration/SpiderDebugOptions.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.configuration 2 | 3 | class SpiderDebugOptions { 4 | var scanBars = true 5 | var triggerZones = true 6 | var endEffectors = true 7 | var targetPositions = true 8 | var legPolygons = true 9 | var centreOfMass = true 10 | var normalForce = true 11 | var orientation = true 12 | var preferredOrientation = true 13 | 14 | var disableFabrik = false 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/setupSpider.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider 2 | 3 | import com.heledron.spideranimation.spider.components.body.setupSpiderBody 4 | import com.heledron.spideranimation.spider.components.* 5 | import com.heledron.spideranimation.spider.components.body.SpiderBody 6 | import com.heledron.spideranimation.spider.components.rendering.setupRenderer 7 | import com.heledron.spideranimation.utilities.ecs.ECS 8 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 9 | 10 | fun setupSpider(app: ECS) { 11 | setupSpiderBody(app) 12 | setupBehaviours(app) 13 | 14 | app.onTick { 15 | for ((entity, _) in app.query()) { 16 | entity.replaceComponent(StayStillBehaviour()) 17 | } 18 | } 19 | 20 | setupCloak(app) 21 | setupMountable(app) 22 | setupPointDetector(app) 23 | setupSoundAndParticles(app) 24 | setupTridentHitDetector(app) 25 | setupRenderer(app) 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/configuration/SpiderOptions.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.configuration 2 | 3 | import org.bukkit.Sound 4 | import org.bukkit.util.Vector 5 | import kotlin.random.Random 6 | 7 | class SpiderOptions { 8 | var walkGait = Gait.defaultWalk() 9 | var gallopGait = Gait.defaultGallop() 10 | 11 | var cloak = CloakOptions() 12 | 13 | var bodyPlan = BodyPlan() 14 | var debug = SpiderDebugOptions() 15 | 16 | var sound = SoundOptions() 17 | 18 | // fun scale(scale: Double) { 19 | // walkGait.scale(scale) 20 | // gallopGait.scale(scale) 21 | // bodyPlan.scale(scale) 22 | // } 23 | } 24 | 25 | 26 | 27 | class SoundOptions { 28 | var step = SoundPlayer( 29 | sound = Sound.BLOCK_NETHERITE_BLOCK_STEP, 30 | volume = .3f, 31 | pitch = 1.0f 32 | ) 33 | } 34 | 35 | 36 | class SoundPlayer( 37 | val sound: Sound, 38 | val volume: Float, 39 | val pitch: Float, 40 | val volumeVary: Float = 0.1f, 41 | val pitchVary: Float = 0.1f 42 | ) { 43 | fun play(world: org.bukkit.World, position: Vector) { 44 | val volume = volume + Random.nextFloat() * volumeVary 45 | val pitch = pitch + Random.nextFloat() * pitchVary 46 | world.playSound(position.toLocation(world), sound, volume, pitch) 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/DisplayModel.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities 2 | 3 | import org.bukkit.block.data.BlockData 4 | import org.bukkit.entity.Display 5 | import org.joml.Matrix4f 6 | 7 | class BlockDisplayModelPiece ( 8 | var block: BlockData, 9 | var transform: Matrix4f, 10 | var brightness: Display.Brightness? = null, 11 | var tags: List = emptyList(), 12 | ) { 13 | fun scale(scale: Float) { 14 | transform.set(Matrix4f().scale(scale).mul(transform)) 15 | } 16 | 17 | fun scale(x: Float, y: Float, z: Float) { 18 | transform.set(Matrix4f().scale(x, y, z).mul(transform)) 19 | } 20 | 21 | fun clone() = BlockDisplayModelPiece( 22 | block = block.clone(), 23 | transform = Matrix4f(transform), 24 | brightness = brightness?.let { Display.Brightness(it.blockLight, it.skyLight) }, 25 | tags = tags, 26 | ) 27 | } 28 | 29 | class DisplayModel(var pieces: List) { 30 | fun scale(scale: Float) = apply { 31 | pieces.forEach { it.scale(scale, scale, scale) } 32 | } 33 | 34 | fun scale(x: Float, y: Float, z: Float) = apply { 35 | pieces.forEach { it.scale(x, y, z) } 36 | } 37 | 38 | fun clone() = DisplayModel(pieces.map { it.clone() }) 39 | 40 | companion object { 41 | fun empty() = DisplayModel(emptyList()) 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/configuration/BodyPlan.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.configuration 2 | 3 | import com.heledron.spideranimation.spider.presets.AnimatedPalettes 4 | import com.heledron.spideranimation.spider.presets.SpiderTorsoModels 5 | import com.heledron.spideranimation.utilities.DisplayModel 6 | import org.bukkit.util.Vector 7 | 8 | class SegmentPlan( 9 | var length: Double, 10 | var initDirection: Vector, 11 | var model: DisplayModel = DisplayModel(listOf()) 12 | ) { 13 | fun clone() = SegmentPlan(length, initDirection.clone(), model.clone()) 14 | } 15 | 16 | class LegPlan( 17 | var attachmentPosition: Vector, 18 | var restPosition: Vector, 19 | var segments: List, 20 | ) 21 | 22 | class BodyPlan { 23 | var scale = 1.0 24 | var legs = emptyList() 25 | 26 | var bodyModel = SpiderTorsoModels.EMPTY.model.clone() 27 | 28 | var eyePalette = AnimatedPalettes.CYAN_EYES.palette 29 | var blinkingPalette = AnimatedPalettes.CYAN_BLINKING_LIGHTS.palette 30 | 31 | fun scale(scale: Double) { 32 | this.scale *= scale 33 | bodyModel.scale(scale.toFloat()) 34 | legs.forEach { 35 | it.attachmentPosition.multiply(scale) 36 | it.restPosition.multiply(scale) 37 | it.segments.forEach { segment -> 38 | segment.length *= scale 39 | segment.model.scale(scale.toFloat()) 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/body/LegLookUp.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components.body 2 | 3 | object LegLookUp { 4 | fun diagonalPairs(legs: List): List> { 5 | return legs.map { diagonal(it) + it } 6 | } 7 | 8 | fun isLeftLeg(leg: Int): Boolean { 9 | return leg % 2 == 0 10 | } 11 | 12 | fun isRightLeg(leg: Int): Boolean { 13 | return !isLeftLeg(leg) 14 | } 15 | 16 | fun getPairIndex(leg: Int): Int { 17 | return leg / 2 18 | } 19 | 20 | fun isDiagonal1(leg: Int): Boolean { 21 | return if (getPairIndex(leg) % 2 == 0) isLeftLeg(leg) else isRightLeg(leg) 22 | } 23 | 24 | fun isDiagonal2(leg: Int): Boolean { 25 | return !isDiagonal1(leg) 26 | } 27 | 28 | fun diagonalFront(leg: Int): Int { 29 | return if (isLeftLeg(leg)) leg - 1 else leg - 3 30 | } 31 | 32 | fun diagonalBack(leg: Int): Int { 33 | return if (isLeftLeg(leg)) leg + 3 else leg + 1 34 | } 35 | 36 | fun front(leg: Int): Int { 37 | return leg - 2 38 | } 39 | 40 | fun back(leg: Int): Int { 41 | return leg + 2 42 | } 43 | 44 | fun horizontal(leg: Int): Int { 45 | return if (isLeftLeg(leg)) leg + 1 else leg - 1 46 | } 47 | 48 | fun diagonal(leg: Int): List { 49 | return listOf(diagonalFront(leg), diagonalBack(leg)) 50 | } 51 | 52 | fun adjacent(leg: Int): List { 53 | return listOf(front(leg), back(leg), horizontal(leg)) 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/PointDetector.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components 2 | 3 | import com.heledron.spideranimation.spider.components.body.Leg 4 | import com.heledron.spideranimation.spider.components.body.SpiderBody 5 | import com.heledron.spideranimation.utilities.ecs.ECS 6 | import com.heledron.spideranimation.utilities.lookingAtPoint 7 | import com.heledron.spideranimation.utilities.overloads.direction 8 | import com.heledron.spideranimation.utilities.overloads.eyePosition 9 | import org.bukkit.World 10 | import org.bukkit.entity.Player 11 | import org.bukkit.util.Vector 12 | 13 | class PointDetector { 14 | var checkPlayers = setOf() 15 | val selectedLeg = mutableMapOf() 16 | } 17 | 18 | fun setupPointDetector(app: ECS) { 19 | fun rayCastLeg(spider: SpiderBody, world: World, rayOrigin: Vector, rayDirection: Vector): Leg? { 20 | if (spider.world != world) return null 21 | 22 | val tolerance = spider.walkGait.stationary.bodyHeight * .15 23 | for (leg in spider.legs) { 24 | val lookingAt = lookingAtPoint(rayOrigin, rayDirection, leg.endEffector, tolerance) 25 | if (lookingAt) return leg 26 | } 27 | return null 28 | } 29 | 30 | app.onTick { 31 | for ((spider, pointDetector) in app.query()) { 32 | pointDetector.selectedLeg.clear() 33 | 34 | for (player in pointDetector.checkPlayers) { 35 | val leg = rayCastLeg(spider, player.world, player.eyePosition, player.direction) ?: continue 36 | pointDetector.selectedLeg[player] = leg 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/events/scheduler.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.events 2 | 3 | import com.heledron.spideranimation.utilities.currentPlugin 4 | import org.bukkit.scheduler.BukkitTask 5 | import java.io.Closeable 6 | 7 | fun runLater(delay: Long, task: () -> Unit): Closeable { 8 | val plugin = currentPlugin 9 | val handler = plugin.server.scheduler.runTaskLater(plugin, task, delay) 10 | return Closeable { 11 | handler.cancel() 12 | } 13 | } 14 | 15 | fun interval(delay: Long, period: Long, task: (it: Closeable) -> Unit): Closeable { 16 | val plugin = currentPlugin 17 | lateinit var handler: BukkitTask 18 | val closeable = Closeable { handler.cancel() } 19 | handler = plugin.server.scheduler.runTaskTimer(plugin, Runnable { task(closeable) }, delay, period) 20 | return closeable 21 | } 22 | 23 | fun onTick(task: (it: Closeable) -> Unit) = TickSchedule.schedule(TickSchedule.main, task) 24 | fun onTickEnd(task: (it: Closeable) -> Unit) = TickSchedule.schedule(TickSchedule.end, task) 25 | 26 | 27 | 28 | 29 | private object TickSchedule { 30 | val main = mutableListOf<() -> Unit>() 31 | val end = mutableListOf<() -> Unit>() 32 | 33 | fun schedule(list: MutableList<() -> Unit>, task: (it: Closeable) -> Unit): Closeable { 34 | lateinit var closeable: Closeable 35 | 36 | val handler = { task(closeable) } 37 | closeable = Closeable { list.remove(handler) } 38 | 39 | list.add(handler) 40 | 41 | return closeable 42 | } 43 | 44 | 45 | init { 46 | interval(0,1) { 47 | main.toList().forEach { it() } 48 | end.toList().forEach { it() } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/custom_entities/customEntities.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.custom_entities 2 | 3 | import com.heledron.spideranimation.utilities.events.onInteractEntity 4 | import org.bukkit.Bukkit 5 | import org.bukkit.NamespacedKey 6 | import org.bukkit.entity.Entity 7 | import org.bukkit.event.player.PlayerInteractEntityEvent 8 | 9 | class CustomEntityComponent private constructor(val tag: String) { 10 | companion object { 11 | fun create(tag: NamespacedKey): CustomEntityComponent { 12 | return CustomEntityComponent(tag.namespace + "_" + tag.key) 13 | } 14 | 15 | fun fromString(tag: String): CustomEntityComponent { 16 | return CustomEntityComponent(tag) 17 | } 18 | } 19 | 20 | fun entities() = allEntities().filter { it.scoreboardTags.contains(tag) } 21 | 22 | fun isAttached(entity: Entity): Boolean { 23 | return entity.scoreboardTags.contains(tag) 24 | } 25 | 26 | fun onTick(action: (Entity) -> Unit) { 27 | com.heledron.spideranimation.utilities.events.onTick { 28 | entities().forEach { action(it) } 29 | } 30 | } 31 | 32 | fun onInteract(action: (event: PlayerInteractEntityEvent) -> Unit) { 33 | onInteractEntity { event -> 34 | if (!event.rightClicked.scoreboardTags.contains(tag)) return@onInteractEntity 35 | action(event) 36 | } 37 | } 38 | } 39 | 40 | fun allEntities() = Bukkit.getServer().worlds.flatMap { it.entities } 41 | 42 | 43 | fun Entity.attach(component: CustomEntityComponent) { 44 | this.addScoreboardTag(component.tag) 45 | } 46 | 47 | fun Entity.detach(component: CustomEntityComponent) { 48 | this.removeScoreboardTag(component.tag) 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/maths/Rect.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.maths 2 | 3 | import org.joml.Vector2d 4 | 5 | class Rect private constructor(var minX: Double, var minY: Double, var maxX: Double, var maxY: Double) { 6 | val width; get() = maxX - minX 7 | val height; get() = maxY - minY 8 | 9 | val dimensions; get() = Vector2d(width, height) 10 | 11 | companion object { 12 | fun fromMinMax(min: Vector2d, max: Vector2d): Rect { 13 | return Rect(min.x, min.y, max.x, max.y) 14 | } 15 | 16 | fun fromCenter(center: Vector2d, dimensions: Vector2d): Rect { 17 | return Rect( 18 | center.x - dimensions.x / 2, 19 | center.y - dimensions.y / 2, 20 | center.x + dimensions.x / 2, 21 | center.y + dimensions.y / 2 22 | ) 23 | } 24 | } 25 | 26 | fun clone(): Rect { 27 | return Rect(minX, minY, maxX, maxY) 28 | } 29 | 30 | fun expand(padding: Double): Rect { 31 | minX -= padding 32 | minY -= padding 33 | maxX += padding 34 | maxY += padding 35 | return this 36 | } 37 | 38 | fun setYCenter(center: Double, height: Double): Rect { 39 | minY = center - height / 2 40 | maxY = center + height / 2 41 | return this 42 | } 43 | 44 | fun lerp(other: Rect, t: Double): Rect { 45 | minX = minX.lerp(other.minX, t) 46 | minY = minY.lerp(other.minY, t) 47 | maxX = maxX.lerp(other.maxX, t) 48 | maxY = maxY.lerp(other.maxY, t) 49 | return this 50 | } 51 | 52 | fun set(other: Rect): Rect { 53 | minX = other.minX 54 | minY = other.minY 55 | maxX = other.maxX 56 | maxY = other.maxY 57 | return this 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/polygons.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities 2 | 3 | import org.joml.Vector2d 4 | 5 | fun pointInPolygon(point: Vector2d, polygon: List): Boolean { 6 | // count intersections 7 | var count = 0 8 | for (i in polygon.indices) { 9 | val a = polygon[i] 10 | val b = polygon[(i + 1) % polygon.size] 11 | 12 | if (a.y <= point.y && b.y > point.y || b.y <= point.y && a.y > point.y) { 13 | val slope = (b.x - a.x) / (b.y - a.y) 14 | val intersect = a.x + (point.y - a.y) * slope 15 | if (intersect < point.x) count++ 16 | } 17 | } 18 | 19 | return count % 2 == 1 20 | } 21 | 22 | fun nearestPointInPolygon(point: Vector2d, polygon: List): Vector2d { 23 | var closest = polygon[0] 24 | var closestDistance = point.distance(closest) 25 | 26 | for (i in polygon.indices) { 27 | val a = polygon[i] 28 | val b = polygon[(i + 1) % polygon.size] 29 | 30 | val closestOnLine = nearestPointOnClampedLine(point, a, b) 31 | val distance = point.distance(closestOnLine) 32 | 33 | if (distance < closestDistance) { 34 | closest = closestOnLine 35 | closestDistance = distance 36 | } 37 | } 38 | 39 | return closest 40 | } 41 | 42 | fun nearestPointOnClampedLine(point: Vector2d, a: Vector2d, b: Vector2d): Vector2d { 43 | val ap = Vector2d(point.x - a.x, point.y - a.y) 44 | val ab = Vector2d(b.x - a.x, b.y - a.y) 45 | 46 | val dotProduct = ap.dot(ab) 47 | val lengthAB = a.distance(b) 48 | 49 | val t = dotProduct / (lengthAB * lengthAB) 50 | 51 | // Ensure the nearest point lies within the line segment 52 | val tClamped = t.coerceIn(0.0, 1.0) 53 | 54 | val nearestX = a.x + tClamped * ab.x 55 | val nearestY = a.y + tClamped * ab.y 56 | 57 | return Vector2d(nearestX, nearestY) 58 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/overloads/overloads.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.overloads 2 | 3 | import com.heledron.spideranimation.utilities.maths.pitchRadians 4 | import com.heledron.spideranimation.utilities.maths.yawRadians 5 | import net.md_5.bungee.api.ChatMessageType 6 | import net.md_5.bungee.api.chat.TextComponent 7 | import org.bukkit.Bukkit 8 | import org.bukkit.Sound 9 | import org.bukkit.World 10 | import org.bukkit.command.CommandSender 11 | import org.bukkit.entity.Entity 12 | import org.bukkit.entity.LivingEntity 13 | import org.bukkit.entity.Player 14 | import org.bukkit.util.Vector 15 | 16 | 17 | fun CommandSender.sendActionBarOrMessage(message: String) { 18 | if (this is Player) { 19 | this.sendActionBar(message) 20 | } else { 21 | this.sendMessage(message) 22 | } 23 | } 24 | 25 | fun Player.sendActionBar(message: String) { 26 | this.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent(message)) 27 | } 28 | 29 | fun sendDebugActionBar(message: String) { 30 | Bukkit.getOnlinePlayers().firstOrNull()?.sendActionBar(message) 31 | } 32 | 33 | fun sendDebugChatMessage(message: String) { 34 | Bukkit.getOnlinePlayers().firstOrNull()?.sendMessage(message) 35 | } 36 | 37 | fun World.spawnEntity(position: Vector, clazz: Class, initializer: (T) -> Unit): T { 38 | return this.spawn(position.toLocation(this), clazz, initializer) 39 | } 40 | 41 | fun World.playSound(position: Vector, sound: Sound, volume: Float, pitch: Float) { 42 | this.playSound(position.toLocation(this), sound, volume, pitch) 43 | } 44 | 45 | val Entity.position get() = this.location.toVector() 46 | val LivingEntity.eyePosition get() = this.eyeLocation.toVector() 47 | val Entity.direction get() = this.location.direction 48 | val Entity.yaw get() = this.location.yaw 49 | val Entity.pitch get() = this.location.pitch 50 | fun Entity.yawRadians() = this.location.yawRadians() 51 | fun Entity.pitchRadians() = this.location.pitchRadians() -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/presets/AnimatedPalettes.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.presets 2 | 3 | import org.bukkit.Material 4 | import org.bukkit.block.data.BlockData 5 | import org.bukkit.entity.Display 6 | 7 | enum class AnimatedPalettes(val palette: List>) { 8 | CYAN_EYES(arrayOf( 9 | * Array(3) { Material.CYAN_SHULKER_BOX }, 10 | Material.CYAN_CONCRETE, 11 | Material.CYAN_CONCRETE_POWDER, 12 | 13 | Material.LIGHT_BLUE_SHULKER_BOX, 14 | Material.LIGHT_BLUE_CONCRETE, 15 | Material.LIGHT_BLUE_CONCRETE_POWDER, 16 | ).map { it.createBlockData() to Display.Brightness(15,15) }), 17 | 18 | CYAN_BLINKING_LIGHTS(arrayOf( 19 | * Array(3) { Material.BLACK_SHULKER_BOX to Display.Brightness(0,15) }, 20 | * Array(3) { Material.VERDANT_FROGLIGHT to Display.Brightness(15,15) }, 21 | Material.LIGHT_BLUE_SHULKER_BOX to Display.Brightness(15,15), 22 | Material.LIGHT_BLUE_CONCRETE to Display.Brightness(15,15), 23 | Material.LIGHT_BLUE_CONCRETE_POWDER to Display.Brightness(15,15), 24 | ).map { (block, brightness) -> block.createBlockData() to brightness }), 25 | 26 | 27 | RED_EYES(arrayOf( 28 | * Array(3) { Material.RED_SHULKER_BOX }, 29 | Material.RED_CONCRETE, 30 | Material.RED_CONCRETE_POWDER, 31 | 32 | Material.FIRE_CORAL_BLOCK, 33 | Material.REDSTONE_BLOCK, 34 | ).map { it.createBlockData() to Display.Brightness(15,15) }), 35 | 36 | RED_BLINKING_LIGHTS(arrayOf( 37 | * Array(3) { Material.BLACK_SHULKER_BOX to Display.Brightness(0,15) }, 38 | * Array(3) { Material.PEARLESCENT_FROGLIGHT to Display.Brightness(15,15) }, 39 | Material.RED_TERRACOTTA to Display.Brightness(15,15), 40 | Material.REDSTONE_BLOCK to Display.Brightness(15,15), 41 | Material.FIRE_CORAL_BLOCK to Display.Brightness(15,15), 42 | ).map { (block, brightness) -> block.createBlockData() to brightness }), 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/TridentHitDetector.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components 2 | 3 | import com.heledron.spideranimation.spider.components.body.SpiderBody 4 | import com.heledron.spideranimation.utilities.ecs.ECS 5 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 6 | import com.heledron.spideranimation.utilities.maths.UP_VECTOR 7 | import com.heledron.spideranimation.utilities.overloads.position 8 | import org.bukkit.entity.Trident 9 | 10 | class TridentHitEvent(val entity: ECSEntity, val spider: SpiderBody, val trident: Trident) 11 | 12 | class TridentHitDetector() { 13 | var stunned = false 14 | } 15 | 16 | 17 | fun setupTridentHitDetector(app: ECS) { 18 | app.onTick { 19 | for ((entity, spider, _) in app.query()) { 20 | val rider = entity.query()?.getRider() 21 | 22 | val location = spider.position.toLocation(spider.world) 23 | val tridents = spider.world.getNearbyEntities(location, 1.5, 1.5, 1.5) 24 | .filterIsInstance() 25 | 26 | for (trident in tridents) { 27 | if (rider !== null && trident.shooter == rider) continue 28 | 29 | if (trident.velocity.length() < 2.0) continue 30 | 31 | val tridentDirection = trident.velocity.normalize() 32 | 33 | trident.velocity = tridentDirection.clone().multiply(-.3) 34 | app.emit(TridentHitEvent(entity = entity, spider = spider, trident = trident)) 35 | 36 | spider.velocity.add(tridentDirection.multiply(spider.gait.tridentKnockBack)) 37 | 38 | // apply rotational acceleration 39 | val hitDirection = spider.position.clone().subtract(trident.position).normalize() 40 | val axis = UP_VECTOR.crossProduct(tridentDirection) 41 | val angle = hitDirection.angle(UP_VECTOR) 42 | 43 | val accelerationMagnitude = angle * spider.gait.tridentRotationalKnockBack.toFloat() 44 | 45 | spider.accelerateRotation(axis, accelerationMagnitude) 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/presets/applyLegModels.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.presets 2 | 3 | import com.heledron.spideranimation.spider.configuration.BodyPlan 4 | import com.heledron.spideranimation.utilities.BlockDisplayModelPiece 5 | import com.heledron.spideranimation.utilities.DisplayModel 6 | import org.bukkit.block.data.BlockData 7 | import org.bukkit.entity.Display 8 | import org.joml.Matrix4f 9 | 10 | 11 | private fun createDefaultModel(block: BlockData, length: Double, thickness: Double) = DisplayModel(listOf(BlockDisplayModelPiece( 12 | block = block, 13 | transform = Matrix4f() 14 | .scale(thickness.toFloat(), thickness.toFloat(), length.toFloat()) 15 | .translate(-.5f,-.5f,.0f), 16 | brightness = Display.Brightness(0, 15), 17 | tags = listOf("cloak") 18 | ))) 19 | 20 | fun applyEmptyLegModel(bodyPlan: BodyPlan) { 21 | for (leg in bodyPlan.legs) { 22 | for (segment in leg.segments) { 23 | segment.model = DisplayModel.empty() 24 | } 25 | } 26 | } 27 | 28 | fun applyLineLegModel(bodyPlan: BodyPlan, block: BlockData) { 29 | val rootThickness = 1.0/16 * 4.5 30 | val tipThickness = 1.0/16 * 1.5 31 | 32 | for (leg in bodyPlan.legs) { 33 | for ((index, segment) in leg.segments.withIndex()) { 34 | val fraction = index.toDouble() / (leg.segments.size - 1) 35 | val thickness = rootThickness + (tipThickness - rootThickness) * fraction 36 | segment.model = createDefaultModel(block, segment.length, thickness) 37 | } 38 | } 39 | } 40 | 41 | fun applyMechanicalLegModel(bodyPlan: BodyPlan) { 42 | for (leg in bodyPlan.legs) { 43 | for ((index, segment) in leg.segments.withIndex()) { 44 | val model = when (index) { 45 | 0 -> SpiderLegModel.BASE 46 | 1 -> SpiderLegModel.FEMUR 47 | leg.segments.size - 2 -> SpiderLegModel.TIBIA 48 | leg.segments.size - 1 -> SpiderLegModel.TIP 49 | else -> SpiderLegModel.FEMUR 50 | } 51 | 52 | segment.model = model.clone().scale(1f, 1f, segment.length.toFloat()) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | /.kotlin/ 4 | 5 | *.iml 6 | *.ipr 7 | *.iws 8 | 9 | # IntelliJ 10 | out/ 11 | # mpeltonen/sbt-idea plugin 12 | .idea_modules/ 13 | 14 | # JIRA plugin 15 | atlassian-ide-plugin.xml 16 | 17 | # Compiled class file 18 | *.class 19 | 20 | # Log file 21 | *.log 22 | 23 | # BlueJ files 24 | *.ctxt 25 | 26 | # Package Files # 27 | *.jar 28 | *.war 29 | *.nar 30 | *.ear 31 | *.zip 32 | *.tar.gz 33 | *.rar 34 | 35 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 36 | hs_err_pid* 37 | 38 | *~ 39 | 40 | # temporary files which can be created if a process still has a handle open of a deleted file 41 | .fuse_hidden* 42 | 43 | # KDE directory preferences 44 | .directory 45 | 46 | # Linux trash folder which might appear on any partition or disk 47 | .Trash-* 48 | 49 | # .nfs files are created when an open file is removed but is still being accessed 50 | .nfs* 51 | 52 | # General 53 | .DS_Store 54 | .AppleDouble 55 | .LSOverride 56 | 57 | # Icon must end with two \r 58 | Icon 59 | 60 | # Thumbnails 61 | ._* 62 | 63 | # Files that might appear in the root of a volume 64 | .DocumentRevisions-V100 65 | .fseventsd 66 | .Spotlight-V100 67 | .TemporaryItems 68 | .Trashes 69 | .VolumeIcon.icns 70 | .com.apple.timemachine.donotpresent 71 | 72 | # Directories potentially created on remote AFP share 73 | .AppleDB 74 | .AppleDesktop 75 | Network Trash Folder 76 | Temporary Items 77 | .apdisk 78 | 79 | # Windows thumbnail cache files 80 | Thumbs.db 81 | Thumbs.db:encryptable 82 | ehthumbs.db 83 | ehthumbs_vista.db 84 | 85 | # Dump file 86 | *.stackdump 87 | 88 | # Folder config file 89 | [Dd]esktop.ini 90 | 91 | # Recycle Bin used on file shares 92 | $RECYCLE.BIN/ 93 | 94 | # Windows Installer files 95 | *.cab 96 | *.msi 97 | *.msix 98 | *.msm 99 | *.msp 100 | 101 | # Windows shortcuts 102 | *.lnk 103 | 104 | .gradle 105 | build/ 106 | 107 | # Ignore Gradle GUI config 108 | gradle-app.setting 109 | 110 | # Cache of project 111 | .gradletasknamecache 112 | 113 | **/build/ 114 | 115 | # Common working directory 116 | run/ 117 | runs/ 118 | 119 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 120 | !gradle-wrapper.jar 121 | -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/block_colors/blockColors.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.block_colors 2 | 3 | import com.heledron.spideranimation.utilities.colors.Oklab 4 | import com.heledron.spideranimation.utilities.colors.distanceTo 5 | import com.heledron.spideranimation.utilities.colors.toOklab 6 | import org.bukkit.Color 7 | import org.bukkit.block.data.BlockData 8 | 9 | class BlockColorMatch( 10 | val block: BlockData, 11 | val brightness: Int, 12 | ) 13 | 14 | fun getBlockColor(block: BlockData, brightness: Int): Color? { 15 | return getBlockColor(block)?.withBrightness(brightness) 16 | } 17 | 18 | fun getBlockColor(block: BlockData): Color? { 19 | return blockToColor[block.material] 20 | } 21 | 22 | fun findBlockWithColor(color: Oklab, allowCustomBrightness: Boolean): BlockColorMatch { 23 | val list = if (allowCustomBrightness) colorToBlock else colorToBlock.filter { it.brightness == 15 } 24 | 25 | val bestMatch = list.minBy { it.oklab.distanceTo(color) } 26 | return BlockColorMatch( 27 | block = bestMatch.material.createBlockData(), 28 | brightness = bestMatch.brightness, 29 | ) 30 | } 31 | 32 | fun findBlockWithColor(color: Color, allowCustomBrightness: Boolean): BlockColorMatch { 33 | val list = if (allowCustomBrightness) colorToBlock else colorToBlock.filter { it.brightness == 15 } 34 | 35 | val bestMatch = list.minBy { it.rgb.distanceTo(color) } 36 | return BlockColorMatch( 37 | block = bestMatch.material.createBlockData(), 38 | brightness = bestMatch.brightness, 39 | ) 40 | } 41 | 42 | enum class FindBlockWithColor(val customBrightness: Boolean, val match: (Color) -> BlockColorMatch) { 43 | RGB(false, { color -> findBlockWithColor(color, false) }), 44 | RGB_WITH_BRIGHTNESS(true, { color -> findBlockWithColor(color, true) }), 45 | OKLAB(false, { color -> findBlockWithColor(color.toOklab(), false) }), 46 | OKLAB_WITH_BRIGHTNESS(true, { color -> findBlockWithColor(color.toOklab(), true) }), 47 | } 48 | 49 | 50 | internal fun Color.withBrightness(brightness: Int): Color { 51 | return Color.fromRGB( 52 | (red * brightness.toDouble() / 15).toInt(), 53 | (green * brightness.toDouble() / 15).toInt(), 54 | (blue * brightness.toDouble() / 15).toInt(), 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/events/events.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.events 2 | 3 | import com.heledron.spideranimation.utilities.currentPlugin 4 | import org.bukkit.entity.Entity 5 | import org.bukkit.entity.Player 6 | import org.bukkit.event.EventHandler 7 | import org.bukkit.event.HandlerList 8 | import org.bukkit.event.Listener 9 | import org.bukkit.event.block.Action 10 | import org.bukkit.event.entity.EntitySpawnEvent 11 | import org.bukkit.event.player.PlayerInteractEntityEvent 12 | import org.bukkit.event.player.PlayerInteractEvent 13 | import org.bukkit.inventory.EquipmentSlot 14 | import org.bukkit.inventory.ItemStack 15 | import java.io.Closeable 16 | 17 | fun addEventListener(listener: Listener): Closeable { 18 | val plugin = currentPlugin 19 | plugin.server.pluginManager.registerEvents(listener, plugin) 20 | return Closeable { 21 | HandlerList.unregisterAll(listener) 22 | } 23 | } 24 | 25 | fun onInteractEntity(listener: (Player, Entity, EquipmentSlot) -> Unit): Closeable { 26 | return addEventListener(object : Listener { 27 | @EventHandler 28 | fun onInteract(event: PlayerInteractEntityEvent) { 29 | listener(event.player, event.rightClicked, event.hand) 30 | } 31 | }) 32 | } 33 | 34 | 35 | fun onInteractEntity(listener: (event: PlayerInteractEntityEvent) -> Unit): Closeable { 36 | return addEventListener(object : Listener { 37 | @EventHandler 38 | fun onInteract(event: PlayerInteractEntityEvent) { 39 | listener(event) 40 | } 41 | }) 42 | } 43 | 44 | fun onSpawnEntity(listener: (Entity) -> Unit): Closeable { 45 | return addEventListener(object : Listener { 46 | @EventHandler 47 | fun onSpawn(event: EntitySpawnEvent) { 48 | listener(event.entity) 49 | } 50 | }) 51 | } 52 | 53 | fun onGestureUseItem(listener: (Player, ItemStack) -> Unit) = addEventListener(object : Listener { 54 | @EventHandler 55 | fun onPlayerInteract(event: PlayerInteractEvent) { 56 | if (event.action != Action.RIGHT_CLICK_AIR && event.action != Action.RIGHT_CLICK_BLOCK) return 57 | if (event.action == Action.RIGHT_CLICK_BLOCK && !(event.clickedBlock?.type?.isInteractable == false || event.player.isSneaking)) return 58 | listener(event.player, event.item ?: return) 59 | } 60 | }) -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/core.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities 2 | 3 | import com.heledron.spideranimation.utilities.events.onTick 4 | import com.heledron.spideranimation.utilities.maths.pitchRadians 5 | import com.heledron.spideranimation.utilities.maths.yawRadians 6 | import com.heledron.spideranimation.utilities.overloads.spawnEntity 7 | import net.md_5.bungee.api.ChatMessageType 8 | import net.md_5.bungee.api.chat.TextComponent 9 | import org.bukkit.Bukkit 10 | import org.bukkit.Location 11 | import org.bukkit.NamespacedKey 12 | import org.bukkit.Sound 13 | import org.bukkit.World 14 | import org.bukkit.command.CommandSender 15 | import org.bukkit.command.PluginCommand 16 | import org.bukkit.entity.Entity 17 | import org.bukkit.entity.LivingEntity 18 | import org.bukkit.entity.Player 19 | import org.bukkit.entity.minecart.CommandMinecart 20 | import org.bukkit.plugin.java.JavaPlugin 21 | import org.bukkit.util.Vector 22 | import java.io.Closeable 23 | import java.io.InputStream 24 | 25 | lateinit var currentPlugin: JavaPlugin 26 | var currentTick = 0 27 | 28 | private val closeableList = mutableListOf() 29 | 30 | fun onPluginShutdown(task: () -> Unit) = closeableList.add(task) 31 | 32 | fun JavaPlugin.setupCoreUtils() { 33 | currentPlugin = this 34 | onTick { currentTick ++ } 35 | } 36 | 37 | fun JavaPlugin.shutdownCoreUtils() { 38 | closeableList.forEach { it.close() } 39 | } 40 | 41 | fun namespacedID(id: String): NamespacedKey { 42 | return NamespacedKey(currentPlugin, id) 43 | } 44 | 45 | fun requireResource(name: String): InputStream { 46 | return currentPlugin.getResource(name) ?: error("Resource $name not found") 47 | } 48 | 49 | fun requireCommand(name: String): PluginCommand { 50 | return currentPlugin.getCommand(name) ?: error("Command $name not found") 51 | } 52 | 53 | private var commandBlockMinecart: CommandMinecart? = null 54 | fun runCommandSilently( 55 | command: String, 56 | world: World = Bukkit.getWorlds().first(), 57 | position: Vector = world.spawnLocation.toVector() 58 | ) { 59 | val server = Bukkit.getServer() 60 | 61 | val commandBlockMinecart = commandBlockMinecart ?: world.spawnEntity(position, CommandMinecart::class.java) { 62 | commandBlockMinecart = it 63 | it.remove() 64 | } 65 | 66 | server.dispatchCommand(commandBlockMinecart, command) 67 | } 68 | -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/AppState.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation 2 | 3 | import com.heledron.spideranimation.kinematic_chain_visualizer.KinematicChainVisualizer 4 | import com.heledron.spideranimation.spider.components.body.SpiderBody 5 | import com.heledron.spideranimation.spider.components.Cloak 6 | import com.heledron.spideranimation.spider.components.Mountable 7 | import com.heledron.spideranimation.spider.components.PointDetector 8 | import com.heledron.spideranimation.spider.components.SoundsAndParticles 9 | import com.heledron.spideranimation.spider.components.TridentHitDetector 10 | import com.heledron.spideranimation.spider.presets.hexBot 11 | import com.heledron.spideranimation.spider.components.rendering.SpiderRenderer 12 | import com.heledron.spideranimation.utilities.ecs.ECS 13 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 14 | import org.bukkit.Location 15 | 16 | object AppState { 17 | var options = hexBot(4, 1.0) 18 | var miscOptions = MiscellaneousOptions() 19 | var renderDebugVisuals = false 20 | 21 | var gallop = false 22 | 23 | val ecs = ECS() 24 | 25 | var target: Location? = null 26 | 27 | fun createSpider(location: Location): ECSEntity { 28 | location.y += options.walkGait.stationary.bodyHeight 29 | return ecs.spawn( 30 | SpiderBody.fromLocation(location, options.bodyPlan, walkGait = options.walkGait, gallopGait = options.gallopGait), 31 | TridentHitDetector(), 32 | Cloak(options.cloak), 33 | SoundsAndParticles(options.sound), 34 | Mountable(), 35 | PointDetector(), 36 | SpiderRenderer(), 37 | ) 38 | } 39 | 40 | fun createChainVisualizer(location: Location): ECSEntity { 41 | val segmentPlans = options.bodyPlan.legs.lastOrNull()?.segments ?: throw Error("Cannot find segment plans") 42 | 43 | return ecs.spawn(KinematicChainVisualizer.create( 44 | segmentPlans = segmentPlans, 45 | root = location.toVector(), 46 | world = location.world ?: throw Error("location.world is null"), 47 | straightenRotation = options.walkGait.legStraightenRotation, 48 | ).apply { 49 | detailed = renderDebugVisuals 50 | }) 51 | } 52 | 53 | fun recreateSpider() { 54 | val spider = ecs.query().firstOrNull() ?: return 55 | createSpider(spider.location()) 56 | } 57 | } 58 | 59 | class MiscellaneousOptions { 60 | var showLaser = true 61 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/parseModelFromCommand.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities 2 | 3 | import com.google.gson.Gson 4 | import org.bukkit.Material 5 | import org.joml.Matrix4f 6 | 7 | 8 | fun parseModelFromCommand(command: String): DisplayModel { 9 | // /summon block_display ~-0.5 ~ ~-0.5 {Passengers:[{id:"minecraft:block_display",block_state:{Name:"minecraft:smooth_quartz",Properties:{}},transformation:[0.1f,0f,0f,0.15f,0f,0.0427f,0.0288f,0.4922f,0f,-0.0022f,0.5492f,-0.8771f,0f,0f,0f,1f]}... 10 | 11 | val pieces = mutableListOf() 12 | 13 | var json = command.substring("/summon block_display ~-0.5 ~ ~-0.5 ".length) 14 | 15 | // convert 1.0f -> 1.0 16 | json = json.replace(Regex("""(\d*\.*\d+)f"""), "$1") 17 | 18 | val parsed = Gson().fromJson(json, Map::class.java) 19 | 20 | @Suppress("UNCHECKED_CAST") 21 | for (passenger in parsed["Passengers"] as List>) { 22 | val blockDisplay = passenger["block_state"] as? Map<*, *> ?: throw IllegalArgumentException("Missing block_state") 23 | val blockName = blockDisplay["Name"] as? String ?: throw IllegalArgumentException("Missing block_state.Name") 24 | val blockProperties = blockDisplay["Properties"] as Map<*, *> 25 | val blockData = Material.matchMaterial(blockName)?.createBlockData() ?: throw IllegalArgumentException("Unknown block name: $blockName") 26 | if (blockProperties.contains("facing")) { 27 | val directional = blockData as? org.bukkit.block.data.Directional ?: throw IllegalArgumentException("Block is not directional") 28 | directional.facing = org.bukkit.block.BlockFace.valueOf((blockProperties["facing"] as String).uppercase()) 29 | } 30 | 31 | val transformation = passenger["transformation"] as? List ?: throw IllegalArgumentException("Missing transformation") 32 | val matrix = Matrix4f( 33 | transformation[0], transformation[4], transformation[8], transformation[12], 34 | transformation[1], transformation[5], transformation[9], transformation[13], 35 | transformation[2], transformation[6], transformation[10], transformation[14], 36 | transformation[3], transformation[7], transformation[11], transformation[15] 37 | ) 38 | 39 | 40 | pieces += BlockDisplayModelPiece( 41 | block = blockData, 42 | transform = matrix, 43 | tags = passenger["Tags"] as? List ?: emptyList() 44 | ) 45 | } 46 | 47 | return DisplayModel(pieces) 48 | } -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: spider-animation 2 | version: '4.0-SNAPSHOT' 3 | main: com.heledron.spideranimation.SpiderAnimationPlugin 4 | api-version: '1.21' 5 | 6 | commands: 7 | items: 8 | description: Open the items menu 9 | permission: spider-animation.items 10 | options: 11 | description: Set gait options 12 | permission: spider-animation.options 13 | fall: 14 | description: Teleport the spider up by the specified height 15 | permission: spider-animation.fall 16 | preset: 17 | description: Apply a preset 18 | permission: spider-animation.preset 19 | scale: 20 | description: Scale the spider 21 | permission: spider-animation.scale 22 | modify_model: 23 | description: Modify model 24 | permission: spider-animation.modify_model 25 | torso_model: 26 | description: Set the torso model of the spider 27 | permission: spider-animation.body_model 28 | leg_model: 29 | description: Set the leg model of the spider 30 | permission: spider-animation.leg_model 31 | animated_palette: 32 | description: Modify animated palette 33 | permission: spider-animation.animated_palette 34 | set_sound: 35 | description: Set the sound of the spider 36 | permission: spider-animation.set_sound 37 | splay: 38 | description: Splay 39 | permission: spider-animation.splay 40 | 41 | permissions: 42 | spider-animation.items: 43 | description: Allows access to the items command 44 | default: op 45 | spider-animation.options: 46 | description: Allows access to the options command 47 | default: op 48 | spider-animation.fall: 49 | description: Allows access to the fall command 50 | default: op 51 | spider-animation.preset: 52 | description: Allows access to the preset command 53 | default: op 54 | spider-animation.scale: 55 | description: Allows access to the scale command 56 | default: op 57 | spider-animation.modify_model: 58 | description: Allows access to the modify_model command 59 | default: op 60 | spider-animation.torso_model: 61 | description: Allows access to the torso_model command 62 | default: op 63 | spider-animation.leg_model: 64 | description: Allows access to the leg_model command 65 | default: op 66 | spider-animation.animated_palette: 67 | description: Allows access to the animated_palette command 68 | default: op 69 | spider-animation.set_sound: 70 | description: Allows access to the set_sound command 71 | default: op 72 | spider-animation.splay: 73 | description: Allows access to the splay command 74 | default: op -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/rendering/utilities.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.rendering 2 | 3 | import org.bukkit.Location 4 | import org.bukkit.World 5 | import org.bukkit.entity.BlockDisplay 6 | import org.bukkit.entity.Display 7 | import org.bukkit.entity.ItemDisplay 8 | import org.bukkit.entity.TextDisplay 9 | import org.bukkit.util.Transformation 10 | import org.bukkit.util.Vector 11 | import org.joml.Matrix4f 12 | 13 | 14 | fun Display.interpolateTransform(transformation: Transformation) { 15 | if (this.transformation == transformation) return 16 | this.transformation = transformation 17 | this.interpolationDelay = 0 18 | } 19 | 20 | fun Display.interpolateTransform(matrix: Matrix4f) { 21 | val oldTransformation = this.transformation 22 | setTransformationMatrix(matrix) 23 | 24 | if (oldTransformation == this.transformation) return 25 | this.interpolationDelay = 0 26 | } 27 | 28 | 29 | fun renderBlock( 30 | location: Location, 31 | init: (BlockDisplay) -> Unit = {}, 32 | update: (BlockDisplay) -> Unit = {} 33 | ) = RenderEntity( 34 | clazz = BlockDisplay::class.java, 35 | location = location, 36 | init = init, 37 | update = update 38 | ) 39 | 40 | fun renderBlock( 41 | world: World, 42 | position: Vector, 43 | init: (BlockDisplay) -> Unit = {}, 44 | update: (BlockDisplay) -> Unit = {} 45 | ) = RenderEntity( 46 | clazz = BlockDisplay::class.java, 47 | location = position.toLocation(world), 48 | init = init, 49 | update = update, 50 | ) 51 | 52 | fun renderText( 53 | location: Location, 54 | init: (TextDisplay) -> Unit = {}, 55 | update: (TextDisplay) -> Unit = {}, 56 | ) = RenderEntity( 57 | clazz = TextDisplay::class.java, 58 | location = location, 59 | init = init, 60 | update = update 61 | ) 62 | 63 | fun renderText( 64 | world: World, 65 | position: Vector, 66 | init: (TextDisplay) -> Unit = {}, 67 | update: (TextDisplay) -> Unit = {}, 68 | ) = renderText( 69 | location = position.toLocation(world), 70 | init = init, 71 | update = update 72 | ) 73 | 74 | 75 | fun renderItem( 76 | location: Location, 77 | init: (ItemDisplay) -> Unit = {}, 78 | update: (ItemDisplay) -> Unit = {}, 79 | ) = RenderEntity( 80 | clazz = ItemDisplay::class.java, 81 | location = location, 82 | init = init, 83 | update = update 84 | ) 85 | 86 | fun renderItem( 87 | world: World, 88 | position: Vector, 89 | init: (ItemDisplay) -> Unit = {}, 90 | update: (ItemDisplay) -> Unit = {}, 91 | ) = renderItem( 92 | location = position.toLocation(world), 93 | init = init, 94 | update = update 95 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/SpiderAnimationPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation 2 | 3 | import com.heledron.spideranimation.AppState.ecs 4 | import com.heledron.spideranimation.kinematic_chain_visualizer.KinematicChainVisualizer 5 | import com.heledron.spideranimation.kinematic_chain_visualizer.setupChainVisualizer 6 | import com.heledron.spideranimation.spider.components.body.SpiderBody 7 | import com.heledron.spideranimation.spider.components.rendering.SpiderRenderer 8 | import com.heledron.spideranimation.spider.setupSpider 9 | import com.heledron.spideranimation.laser.setupLaserPointer 10 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 11 | import com.heledron.spideranimation.utilities.events.onSpawnEntity 12 | import com.heledron.spideranimation.utilities.events.onTick 13 | import com.heledron.spideranimation.utilities.setupCoreUtils 14 | import com.heledron.spideranimation.utilities.shutdownCoreUtils 15 | import org.bukkit.plugin.java.JavaPlugin 16 | 17 | @Suppress("unused") 18 | class SpiderAnimationPlugin : JavaPlugin() { 19 | fun writeAndSaveConfig() { 20 | // for ((key, value) in options) { 21 | // instance.config.set(key, Serializer.toMap(value())) 22 | // } 23 | // instance.saveConfig() 24 | } 25 | 26 | override fun onDisable() { 27 | logger.info("Disabling Spider Animation plugin") 28 | shutdownCoreUtils() 29 | } 30 | 31 | override fun onEnable() { 32 | logger.info("Enabling Spider Animation plugin") 33 | 34 | setupCoreUtils() 35 | 36 | setupCommands(this) 37 | setupItems() 38 | setupSpider(ecs) 39 | setupChainVisualizer(ecs) 40 | setupLaserPointer(ecs) 41 | 42 | ecs.start() 43 | onTick { 44 | // sync AppState properties 45 | ecs.query().forEach { (entity, spider) -> 46 | entity.query()?.renderDebugVisuals = AppState.renderDebugVisuals 47 | spider.gallop = AppState.gallop 48 | } 49 | 50 | ecs.update() 51 | ecs.render() 52 | } 53 | 54 | 55 | onSpawnEntity { entity -> 56 | // Use this command to spawn a chain visualizer 57 | // /summon minecraft:area_effect_cloud ~ ~ ~ {Tags:["spider.chain_visualizer"]} 58 | if (!entity.scoreboardTags.contains("spider.chain_visualizer")) return@onSpawnEntity 59 | 60 | val oldVisualizer = ecs.query().firstOrNull()?.first 61 | if (oldVisualizer == null) { 62 | AppState.createChainVisualizer(entity.location) 63 | } else { 64 | oldVisualizer.remove() 65 | } 66 | 67 | entity.remove() 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/colors/oklab.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.colors 2 | 3 | import com.heledron.spideranimation.utilities.maths.lerp 4 | import com.heledron.spideranimation.utilities.maths.lerpSafely 5 | import org.bukkit.Color 6 | import kotlin.math.cbrt 7 | import kotlin.math.pow 8 | import kotlin.math.sqrt 9 | 10 | class Oklab( 11 | val l: Float, 12 | val a: Float, 13 | val b: Float, 14 | val alpha: Int, 15 | ) { 16 | fun lerp(other: Oklab, t: Float): Oklab { 17 | return Oklab( 18 | l = l.lerp(other.l, t), 19 | a = a.lerp(other.a, t), 20 | b = b.lerp(other.b, t), 21 | alpha = alpha.lerpSafely(other.alpha, t), 22 | ) 23 | } 24 | 25 | fun toRGB(): Color { 26 | val L = (l * 0.99999999845051981432 + 27 | 0.39633779217376785678 * a + 28 | 0.21580375806075880339 * b).pow(3); 29 | val M = (l * 1.0000000088817607767 - 30 | 0.1055613423236563494 * a - 31 | 0.063854174771705903402 * b).pow(3); 32 | val S = (l * 1.0000000546724109177 - 33 | 0.089484182094965759684 * a - 34 | 1.2914855378640917399 * b).pow(3); 35 | 36 | val r = +4.076741661347994 * L - 37 | 3.307711590408193 * M + 38 | 0.230969928729428 * S 39 | val g = -1.2684380040921763 * L + 40 | 2.6097574006633715 * M - 41 | 0.3413193963102197 * S 42 | val b = -0.004196086541837188 * L - 43 | 0.7034186144594493 * M + 44 | 1.7076147009309444 * S 45 | 46 | return Color.fromARGB( 47 | alpha, 48 | (r.coerceIn(0.0, 1.0) * 255).toInt(), 49 | (g.coerceIn(0.0, 1.0) * 255).toInt(), 50 | (b.coerceIn(0.0, 1.0) * 255).toInt(), 51 | ) 52 | } 53 | } 54 | 55 | fun Oklab.distanceTo(other: Oklab): Float { 56 | return sqrt((l - other.l).pow(2) + (a - other.a).pow(2) + (b - other.b).pow(2)) 57 | } 58 | 59 | fun Color.toOklab(): Oklab { 60 | val r = this.red / 255.0; 61 | val g = this.green / 255.0; 62 | val b = this.blue / 255.0; 63 | 64 | val L = cbrt( 65 | 0.41222147079999993 * r + 0.5363325363 * g + 0.0514459929 * b 66 | ) 67 | val M = cbrt( 68 | 0.2119034981999999 * r + 0.6806995450999999 * g + 0.1073969566 * b 69 | ); 70 | val S = cbrt( 71 | 0.08830246189999998 * r + 0.2817188376 * g + 0.6299787005000002 * b 72 | ) 73 | 74 | return Oklab( 75 | l = (0.2104542553 * L + 0.793617785 * M - 0.0040720468 * S).toFloat(), 76 | a = (1.9779984951 * L - 2.428592205 * M + 0.4505937099 * S).toFloat(), 77 | b = (0.0259040371 * L + 0.7827717662 * M - 0.808675766 * S).toFloat(), 78 | alpha = this.alpha, 79 | ) 80 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/laser/LaserPoint.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.laser 2 | 3 | import com.heledron.spideranimation.utilities.rendering.renderBlock 4 | import com.heledron.spideranimation.kinematic_chain_visualizer.KinematicChainVisualizer 5 | import com.heledron.spideranimation.spider.components.SpiderBehaviour 6 | import com.heledron.spideranimation.spider.components.TargetBehaviour 7 | import com.heledron.spideranimation.spider.components.body.SpiderBody 8 | import com.heledron.spideranimation.utilities.ecs.ECS 9 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 10 | import com.heledron.spideranimation.utilities.centredTransform 11 | import org.bukkit.Material 12 | import org.bukkit.World 13 | import org.bukkit.entity.Display 14 | import org.bukkit.util.Vector 15 | 16 | class LaserPoint( 17 | var world: World, 18 | var position: Vector, 19 | var isVisible: Boolean, 20 | ) 21 | 22 | fun setupLaserPointer(app: ECS) { 23 | app.onTick { 24 | val lasers = app.query() 25 | 26 | // get spiders to follow the laser 27 | for ((spiderEntity, spider) in app.query()) { 28 | val nearestLaser = lasers 29 | .filter { it.world == spider.world } 30 | .minByOrNull { it.position.distanceSquared(spider.position) } 31 | ?: continue 32 | 33 | val distance = spider.walkGait.stationary.bodyHeight * 2 34 | val behaviour = TargetBehaviour(nearestLaser.position, distance) 35 | spiderEntity.replaceComponent(behaviour) 36 | } 37 | 38 | // update chain visualizer target 39 | for (chain in app.query()) { 40 | val nearestLaser = lasers 41 | .minByOrNull { it.position.distanceSquared(chain.root) } 42 | ?: continue 43 | 44 | chain.target = nearestLaser.position 45 | } 46 | } 47 | 48 | 49 | app.onRender { 50 | val size = .25f 51 | for (laser in app.query()) { 52 | if (!laser.isVisible) continue 53 | renderLaserPoint(laser.world, laser.position, size).submit(laser) 54 | } 55 | 56 | for (chain in app.query()) { 57 | renderLaserPoint(chain.world, chain.target ?: continue, size - 0.001f) 58 | .submit(chain to "laser") 59 | } 60 | } 61 | } 62 | 63 | private fun renderLaserPoint( 64 | world: World, 65 | position: Vector, 66 | size: Float, 67 | ) = renderBlock( 68 | world = world, 69 | position = position, 70 | init = { 71 | it.block = Material.REDSTONE_BLOCK.createBlockData() 72 | it.teleportDuration = 1 73 | it.brightness = Display.Brightness(15, 15) 74 | it.transformation = centredTransform(size, size, size) 75 | } 76 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/Serializer.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities 2 | 3 | import com.google.gson.Gson 4 | 5 | object Serializer { 6 | val gson = Gson() 7 | 8 | fun toNullableMap(obj: Any?): Any? { 9 | if (obj == null) return null 10 | return toMap(obj) 11 | } 12 | 13 | fun toMap(obj: Any): Any { 14 | return gson.fromJson(gson.toJson(obj), Any::class.java) 15 | } 16 | 17 | fun fromMap(map: Any, clazz: Class): T { 18 | return gson.fromJson(gson.toJson(map), clazz) 19 | } 20 | 21 | fun writeFromMap(obj: Any, map: Map<*, *>) { 22 | for ((key, value) in map) { 23 | setShallow(obj, key.toString(), value) 24 | } 25 | } 26 | 27 | fun get(obj: Any, path: String): Any? { 28 | return get(obj, parsePath(path)) 29 | } 30 | 31 | private fun get(obj: Any, path: List): Any? { 32 | var current: Any? = obj 33 | for (key in path) current = getShallow(current ?: return null, key) 34 | return current 35 | } 36 | 37 | fun setMap(obj: Any, path: String, map: Any?) { 38 | val pathList = parsePath(path) 39 | val newObj = withSetMap(obj, pathList, map) 40 | set(obj, path, get(newObj, path)) 41 | } 42 | 43 | fun set(obj: Any, path: String, value: Any?) { 44 | val pathList = parsePath(path) 45 | val parent = get(obj, pathList.dropLast(1)) ?: return 46 | setShallow(parent, pathList.last(), value) 47 | } 48 | 49 | private fun parsePath(path: String): List { 50 | return path.split("[.\\[\\]]".toRegex()).map { it.trim() }.filter { it.isNotEmpty() } 51 | } 52 | 53 | private fun withSetMap(obj: T, path: List, value: Any?): T { 54 | val map = toMap(obj) 55 | val mapParent = get(map, path.dropLast(1)) ?: return obj 56 | setShallow(mapParent, path.last(), value) 57 | return fromMap(map, obj.javaClass) 58 | } 59 | 60 | private fun getShallow(current: Any, key: String): Any? { 61 | if (current is Map<*, *>) return current[key] 62 | 63 | if (current is List<*>) return current[key.toInt()] 64 | 65 | return try { 66 | val field = current.javaClass.getDeclaredField(key) 67 | field.isAccessible = true 68 | field.get(current) 69 | } catch (e: Exception) { 70 | null 71 | } 72 | } 73 | 74 | private fun setShallow(current: Any, key: String, value: Any?) { 75 | try { 76 | if (current is MutableMap<*, *>) { 77 | (current as MutableMap)[key] = value 78 | return 79 | } 80 | 81 | if (current is MutableList<*>) { 82 | val index = key.toInt() 83 | if (index == current.size) (current as MutableList).add(value) 84 | else (current as MutableList)[index] = value 85 | return 86 | } 87 | 88 | val field = current.javaClass.getDeclaredField(key) 89 | field.isAccessible = true 90 | field.set(current, value) 91 | } catch (_: Exception) { } 92 | } 93 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/rendering/renderSpiderEntities.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components.rendering 2 | 3 | import com.heledron.spideranimation.utilities.rendering.interpolateTransform 4 | import com.heledron.spideranimation.utilities.rendering.renderBlock 5 | import com.heledron.spideranimation.spider.components.body.SpiderBody 6 | import com.heledron.spideranimation.spider.components.Cloak 7 | import com.heledron.spideranimation.utilities.* 8 | import com.heledron.spideranimation.utilities.rendering.RenderGroup 9 | import org.bukkit.util.Vector 10 | import org.joml.Matrix4f 11 | import org.joml.Vector4f 12 | 13 | 14 | fun renderSpider(spider: SpiderBody, cloak: Cloak): RenderGroup { 15 | val group = RenderGroup() 16 | 17 | val transform = Matrix4f().rotate(spider.orientation) 18 | group[spider] = renderModel(spider, cloak, spider.position, spider.bodyPlan.bodyModel, transform) 19 | 20 | 21 | for ((legIndex, leg) in spider.legs.withIndex()) { 22 | val chain = leg.chain 23 | 24 | val pivot = spider.gait.legChainPivotMode.get(spider) 25 | for ((segmentIndex, rotation) in chain.getRotations(pivot).withIndex()) { 26 | val segmentPlan = spider.bodyPlan.legs.getOrNull(legIndex)?.segments?.getOrNull(segmentIndex) ?: continue 27 | 28 | val parent = chain.segments.getOrNull(segmentIndex - 1)?.position ?: chain.root 29 | 30 | val segmentTransform = Matrix4f().rotate(rotation) 31 | group[legIndex to segmentIndex] = renderModel(spider, cloak, parent, segmentPlan.model, segmentTransform) 32 | 33 | } 34 | } 35 | 36 | return group 37 | } 38 | 39 | private fun renderModel( 40 | spider: SpiderBody, 41 | cloak: Cloak, 42 | position: Vector, 43 | model: DisplayModel, 44 | transformation: Matrix4f 45 | ): RenderGroup { 46 | val group = RenderGroup() 47 | 48 | for ((index, piece) in model.pieces.withIndex()) { 49 | group[index] = renderModelPiece(spider, cloak, position, piece, transformation) 50 | } 51 | 52 | return group 53 | } 54 | 55 | 56 | private fun renderModelPiece( 57 | spider: SpiderBody, 58 | cloak: Cloak, 59 | position: Vector, 60 | piece: BlockDisplayModelPiece, 61 | transformation: Matrix4f, 62 | // cloakID: Any 63 | ) = renderBlock( 64 | location = position.toLocation(spider.world), 65 | init = { 66 | it.teleportDuration = 1 67 | it.interpolationDuration = 1 68 | }, 69 | update = { 70 | val transform = Matrix4f(transformation).mul(piece.transform) 71 | it.interpolateTransform(transform) 72 | 73 | val cloak = if (piece.tags.contains("cloak")) { 74 | val relative = transform.transform(Vector4f(.5f, .5f, .5f, 1f)) 75 | val piecePosition = position.clone() 76 | piecePosition.x += relative.x 77 | piecePosition.y += relative.y 78 | piecePosition.z += relative.z 79 | 80 | cloak.getPiece(piece, spider.world, piecePosition, piece.block, piece.brightness) 81 | } else null 82 | 83 | if (cloak != null) { 84 | it.block = cloak.first 85 | it.brightness = cloak.second 86 | } else { 87 | it.block = piece.block 88 | it.brightness = piece.brightness 89 | } 90 | } 91 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/custom_items/customItems.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.custom_items 2 | 3 | import com.heledron.spideranimation.utilities.events.onGestureUseItem 4 | import com.heledron.spideranimation.utilities.events.onTick 5 | import com.heledron.spideranimation.utilities.namespacedID 6 | import com.heledron.spideranimation.utilities.requireCommand 7 | import org.bukkit.Bukkit 8 | import org.bukkit.Bukkit.createInventory 9 | import org.bukkit.ChatColor 10 | import org.bukkit.Material 11 | import org.bukkit.entity.Entity 12 | import org.bukkit.entity.Player 13 | import org.bukkit.inventory.ItemStack 14 | import org.bukkit.persistence.PersistentDataType 15 | 16 | val customItemRegistry = mutableListOf() 17 | 18 | fun openCustomItemInventory(player: Player) { 19 | val inventory = createInventory(null, 9 * 3, "Items") 20 | customItemRegistry.forEach { inventory.addItem(it) } 21 | player.openInventory(inventory) 22 | } 23 | 24 | fun setupCustomItemCommand() { 25 | requireCommand("items").apply { 26 | setExecutor { sender, _, _, _ -> 27 | if (sender !is Player) { 28 | sender.sendMessage("This command can only be used by players.") 29 | return@setExecutor true 30 | } 31 | openCustomItemInventory(sender) 32 | true 33 | } 34 | } 35 | } 36 | 37 | class CustomItemComponent(val id: String) { 38 | fun isAttached(item: ItemStack): Boolean { 39 | return item.itemMeta?.persistentDataContainer?.get(namespacedID("item_component_$id"), PersistentDataType.BOOLEAN) == true 40 | } 41 | 42 | fun attach(item: ItemStack) { 43 | val itemMeta = item.itemMeta ?: return 44 | itemMeta.persistentDataContainer.set(namespacedID("item_component_$id"), PersistentDataType.BOOLEAN, true) 45 | item.itemMeta = itemMeta 46 | } 47 | 48 | fun getPlayersHoldingItem() = Bukkit.getOnlinePlayers().filter { player -> 49 | val itemInMainHand = player.inventory.itemInMainHand 50 | val itemInOffHand = player.inventory.itemInOffHand 51 | isAttached(itemInMainHand) || isAttached(itemInOffHand) 52 | } 53 | 54 | fun onGestureUse(action: (Player, ItemStack) -> Unit) { 55 | onGestureUseItem { player, item -> 56 | if (isAttached(item)) action(player, item) 57 | } 58 | } 59 | 60 | fun onInteractEntity(action: (Player, Entity, ItemStack) -> Unit) { 61 | com.heledron.spideranimation.utilities.events.onInteractEntity(fun(player, entity, hand) { 62 | val item = player.inventory.getItem(hand) ?: return 63 | if (isAttached(item)) action(player, entity, item) 64 | }) 65 | } 66 | 67 | fun onHeldTick(action: (Player, ItemStack) -> Unit) { 68 | onTick { 69 | for (player in Bukkit.getServer().onlinePlayers) { 70 | val itemInMainHand = player.inventory.itemInMainHand 71 | val itemInOffHand = player.inventory.itemInOffHand 72 | if (isAttached(itemInMainHand)) action(player, itemInMainHand) 73 | if (isAttached(itemInOffHand)) action(player, itemInOffHand) 74 | } 75 | } 76 | } 77 | } 78 | 79 | fun createNamedItem(material: Material, name: String): ItemStack { 80 | val item = ItemStack(material) 81 | val itemMeta = item.itemMeta ?: throw Exception("ItemMeta is null") 82 | itemMeta.setItemName(ChatColor.RESET.toString() + name) 83 | item.itemMeta = itemMeta 84 | return item 85 | } 86 | 87 | fun ItemStack.attach(component: CustomItemComponent): ItemStack { 88 | component.attach(this) 89 | return this 90 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/splay.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components 2 | 3 | import com.heledron.spideranimation.AppState 4 | import com.heledron.spideranimation.spider.components.body.SpiderBody 5 | import com.heledron.spideranimation.utilities.* 6 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 7 | import com.heledron.spideranimation.utilities.events.interval 8 | import com.heledron.spideranimation.utilities.events.runLater 9 | import com.heledron.spideranimation.utilities.maths.eased 10 | import com.heledron.spideranimation.utilities.maths.moveTowards 11 | import com.heledron.spideranimation.utilities.overloads.position 12 | import com.heledron.spideranimation.utilities.rendering.RenderEntityTracker 13 | import org.bukkit.entity.BlockDisplay 14 | import org.bukkit.util.Transformation 15 | import org.joml.Quaternionf 16 | import org.joml.Vector3f 17 | import kotlin.random.Random 18 | 19 | private fun Transformation.lerp(newTransform: Transformation, lerpAmount: Float): Transformation { 20 | 21 | this.translation.lerp(newTransform.translation, lerpAmount) 22 | this.scale.lerp(newTransform.scale, lerpAmount) 23 | this.leftRotation.slerp(newTransform.leftRotation, lerpAmount) 24 | this.rightRotation.slerp(newTransform.rightRotation, lerpAmount) 25 | 26 | return this 27 | } 28 | 29 | fun Transformation.clone() = Transformation( 30 | Vector3f(translation), 31 | Quaternionf(leftRotation), 32 | Vector3f(scale), 33 | Quaternionf(rightRotation) 34 | ) 35 | 36 | 37 | fun splay() { 38 | val (entity, spider) = AppState.ecs.query().firstOrNull() ?: return 39 | entity.remove() 40 | 41 | // detach and get entities 42 | val entities = mutableListOf() 43 | for ((id, entity) in RenderEntityTracker.getAll()) { 44 | if (entity !is BlockDisplay) continue 45 | entities += entity 46 | RenderEntityTracker.detach(id) 47 | } 48 | 49 | onPluginShutdown { 50 | for (entity in entities) entity.remove() 51 | } 52 | 53 | 54 | val pieces = mutableListOf() 55 | for (piece in spider.bodyPlan.bodyModel.pieces) { 56 | pieces += piece 57 | } 58 | 59 | for ((legIndex, leg) in spider.legs.withIndex()) { 60 | for ((segmentIndex, segment) in leg.chain.segments.withIndex()) { 61 | val model = spider.bodyPlan.legs[legIndex].segments[segmentIndex].model 62 | for (piece in model.pieces) pieces += piece 63 | } 64 | } 65 | 66 | for ((i, entity) in entities.withIndex().shuffled()) { 67 | val offset = entity.position.subtract(spider.position).toVector3f() 68 | runLater(3L + i / 4) { 69 | splay(entity, offset) 70 | } 71 | } 72 | } 73 | 74 | private fun splay(entity: BlockDisplay, offset: Vector3f) { 75 | val start = entity.transformation 76 | 77 | val end = entity.transformation 78 | end.translation.apply { 79 | this 80 | .add(offset) 81 | .normalize() 82 | .mul(Random.nextDouble(1.0, 3.0).toFloat()) 83 | .sub(offset) 84 | } 85 | end.scale.set(.35f) 86 | end.leftRotation.identity() 87 | end.rightRotation.identity() 88 | 89 | entity.interpolationDuration = 1 90 | entity.interpolationDelay = 0 91 | 92 | var t = .0f 93 | interval(0, 1) { 94 | t = t.moveTowards(1f, .07f) 95 | 96 | entity.transformation = start.lerp(end, t.eased()) 97 | entity.interpolationDelay = 0 98 | 99 | if (t >= 1) it.close() 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/rendering/SpiderRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components.rendering 2 | 3 | import com.heledron.spideranimation.spider.components.body.SpiderBody 4 | import com.heledron.spideranimation.spider.components.Cloak 5 | import com.heledron.spideranimation.spider.components.PointDetector 6 | import com.heledron.spideranimation.utilities.ecs.ECS 7 | import com.heledron.spideranimation.utilities.events.interval 8 | import org.bukkit.Location 9 | import org.bukkit.Particle 10 | import org.bukkit.util.Vector 11 | import kotlin.random.Random 12 | 13 | class SpiderRenderer { 14 | var renderDebugVisuals = false 15 | var useParticles = false 16 | } 17 | 18 | fun setupRenderer(app: ECS) { 19 | // apply eye blinking effect 20 | interval(0,10) { 21 | for (spider in app.query()) { 22 | val pieces = spider.bodyPlan.bodyModel.pieces.filter { it.tags.contains("eye") } 23 | 24 | if (Random.nextBoolean()) return@interval 25 | for (piece in pieces) { 26 | val block = spider.bodyPlan.eyePalette.random() 27 | piece.block = block.first 28 | piece.brightness = block.second 29 | } 30 | } 31 | } 32 | 33 | // apply blinking lights effect 34 | interval(0,5) { 35 | for (spider in app.query()) { 36 | val pieces = spider.bodyPlan.bodyModel.pieces.filter { it.tags.contains("blinking_lights") } 37 | 38 | if (Random.nextBoolean()) return@interval 39 | for (piece in pieces) { 40 | val block = spider.bodyPlan.blinkingPalette.random() 41 | piece.block = block.first 42 | piece.brightness = block.second 43 | } 44 | } 45 | } 46 | 47 | app.onRender { 48 | for ((spider, cloak, pointDetector, renderer) in app.query()) { 49 | if (renderer.useParticles) { 50 | SpiderParticleRenderer.renderSpider(spider) 51 | } else { 52 | renderSpider(spider, cloak).submit(spider) 53 | } 54 | 55 | 56 | if (renderer.renderDebugVisuals) spiderDebugRenderEntities(spider, pointDetector).submit(spider to "debug") 57 | } 58 | } 59 | } 60 | 61 | private object SpiderParticleRenderer { 62 | // fun renderTarget(location: Location) { 63 | // location.world?.spawnParticle(Particle.DUST, location, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1f)) 64 | // } 65 | 66 | fun renderSpider(spider: SpiderBody) { 67 | for (leg in spider.legs) { 68 | val world = leg.spider.world 69 | val chain = leg.chain 70 | var current = chain.root.toLocation(world) 71 | 72 | for ((i, segment) in chain.segments.withIndex()) { 73 | val thickness = (chain.segments.size - i - 1) * 0.025 74 | renderLine(current, segment.position, thickness) 75 | current = segment.position.toLocation(world) 76 | } 77 | } 78 | } 79 | 80 | fun renderLine(point1: Location, point2: Vector, thickness: Double) { 81 | val gap = .05 82 | 83 | val amount = point1.toVector().distance(point2) / gap 84 | val step = point2.clone().subtract(point1.toVector()).multiply(1 / amount) 85 | 86 | val current = point1.clone() 87 | 88 | for (i in 0..amount.toInt()) { 89 | point1.world?.spawnParticle(Particle.BUBBLE, current, 1, thickness, thickness, thickness, 0.0) 90 | current.add(step) 91 | } 92 | } 93 | } 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/KinematicChain.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities 2 | 3 | import com.heledron.spideranimation.utilities.maths.rotate 4 | import org.bukkit.util.Vector 5 | import org.joml.Quaternionf 6 | 7 | 8 | class KinematicChain( 9 | val root: Vector, 10 | val segments: List 11 | ) { 12 | var maxIterations = 20 13 | var tolerance = 0.01 14 | 15 | fun fabrik(target: Vector) { 16 | for (i in 0 until maxIterations) { 17 | fabrikForward(target) 18 | fabrikBackward() 19 | 20 | if (getEndEffector().distanceSquared(target) < tolerance) { 21 | break 22 | } 23 | } 24 | } 25 | 26 | fun straightenDirection(rotation: Quaternionf) { 27 | val position = root.clone() 28 | for (segment in segments) { 29 | val initDirection = segment.initDirection.clone().rotate(rotation) 30 | position.add(initDirection.multiply(segment.length)) 31 | segment.position.copy(position) 32 | } 33 | } 34 | 35 | fun fabrikForward(newPosition: Vector) { 36 | val lastSegment = segments.last() 37 | lastSegment.position.copy(newPosition) 38 | 39 | for (i in segments.size - 1 downTo 1) { 40 | val previousSegment = segments[i] 41 | val segment = segments[i - 1] 42 | 43 | moveSegment(segment.position, previousSegment.position, previousSegment.length) 44 | } 45 | } 46 | 47 | fun fabrikBackward() { 48 | moveSegment(segments.first().position, root, segments.first().length) 49 | 50 | for (i in 1 until segments.size) { 51 | val previousSegment = segments[i - 1] 52 | val segment = segments[i] 53 | 54 | moveSegment(segment.position, previousSegment.position, segment.length) 55 | } 56 | } 57 | 58 | fun moveSegment(point: Vector, pullTowards: Vector, segment: Double) { 59 | val direction = pullTowards.clone().subtract(point).normalize() 60 | point.copy(pullTowards).subtract(direction.multiply(segment)) 61 | } 62 | 63 | fun getEndEffector(): Vector { 64 | return segments.last().position 65 | } 66 | 67 | fun getVectors(): List { 68 | return segments.mapIndexed { i, segment -> 69 | val previous = segments.getOrNull(i - 1)?.position ?: root 70 | segment.position.clone().subtract(previous) 71 | } 72 | } 73 | 74 | fun getRelativeRotations(pivot: Quaternionf): List { 75 | val vectors = getVectors() 76 | 77 | val firstEuler = vectors.first().getRotationAroundAxis(pivot) 78 | val firstRotation = Quaternionf(pivot).rotateYXZ(firstEuler.y, firstEuler.x, .0f) 79 | 80 | val rotations = vectors.mapIndexed { i, current -> 81 | val previous = vectors.getOrNull(i - 1) ?: return@mapIndexed firstRotation 82 | 83 | Quaternionf().rotationTo(previous.toVector3f(), current.toVector3f()) 84 | } 85 | 86 | return rotations 87 | } 88 | 89 | fun getRotations(pivot: Quaternionf): List { 90 | return getRelativeRotations(pivot).apply { cumulateRotations(this) } 91 | } 92 | 93 | private fun cumulateRotations(rotations: List) { 94 | for (i in 1 until rotations.size) { 95 | rotations[i].mul(rotations[i - 1]) 96 | } 97 | } 98 | } 99 | 100 | class ChainSegment( 101 | var position: Vector, 102 | var length: Double, 103 | var initDirection: Vector, 104 | ) { 105 | fun clone(): ChainSegment { 106 | return ChainSegment(position.clone(), length, initDirection.clone()) 107 | } 108 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/body/GaitType.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components.body 2 | 3 | enum class GaitType(val canMoveLeg: (Leg) -> Boolean, val getLegsInUpdateOrder: (SpiderBody) -> List) { 4 | WALK(WalkGaitType::canMoveLeg, WalkGaitType::getLegsInUpdateOrder), 5 | GALLOP(GallopGaitType::canMoveLeg, GallopGaitType::getLegsInUpdateOrder) 6 | } 7 | 8 | object WalkGaitType { 9 | fun getLegsInUpdateOrder(spider: SpiderBody): List { 10 | val legs = spider.legs 11 | val diagonal1 = legs.indices.filter { LegLookUp.isDiagonal1(it) } 12 | val diagonal2 = legs.indices.filter { LegLookUp.isDiagonal2(it) } 13 | val indices = diagonal1 + diagonal2 14 | return indices.map { spider.legs[it] } 15 | } 16 | 17 | fun canMoveLeg(leg: Leg): Boolean { 18 | val spider = leg.spider 19 | val index = spider.legs.indexOf(leg) 20 | 21 | // always move if the target is not on ground 22 | if (!leg.target.isGrounded) return true 23 | 24 | leg.isPrimary = true 25 | 26 | // ensure other pair is grounded 27 | // ignore if disabled, ignore if target is not grounded 28 | val crossPair = unIndexLeg(spider, LegLookUp.adjacent(index)) 29 | if (crossPair.any { !it.isGrounded() && !it.isDisabled && it.target.isGrounded }) return false 30 | 31 | // cooldown 32 | if (crossPair.any { it.target.isGrounded && it.timeSinceStopMove < spider.gait.crossPairCooldown }) return false 33 | val samePair = unIndexLeg(spider, LegLookUp.diagonal(index)) 34 | if (samePair.any { it.target.isGrounded && it.timeSinceBeginMove < spider.gait.samePairCooldown }) return false 35 | 36 | val wantsToMove = leg.isOutsideTriggerZone || !leg.touchingGround 37 | val alreadyAtTarget = leg.endEffector.distanceSquared(leg.target.position) < 0.01 38 | val onGround = spider.legs.any { it.isGrounded() } || spider.onGround 39 | 40 | return wantsToMove && !alreadyAtTarget && onGround 41 | } 42 | } 43 | 44 | object GallopGaitType { 45 | fun getLegsInUpdateOrder(spider: SpiderBody): List { 46 | return WalkGaitType.getLegsInUpdateOrder(spider) 47 | } 48 | 49 | fun canMoveLeg(leg: Leg): Boolean { 50 | val spider = leg.spider 51 | val index = spider.legs.indexOf(leg) 52 | 53 | if (!spider.isWalking) return WalkGaitType.canMoveLeg(leg) 54 | 55 | // if (spider.velocity.length() < spider.gait.maxSpeed * 0.5) return WalkGaitType.canMoveLeg(leg) 56 | 57 | // always move if the target is not on ground 58 | if (!leg.target.isGrounded) return true 59 | 60 | // only move when at least one leg is on the ground 61 | val onGround = spider.legs.any { it.isGrounded() } || spider.onGround 62 | if (!onGround) return false 63 | 64 | val pair = spider.legs[LegLookUp.horizontal(index)] 65 | 66 | leg.isPrimary = LegLookUp.isDiagonal1(index) || pair.isDisabled || !pair.target.isGrounded 67 | 68 | if (leg.isPrimary) { 69 | // check cooldown 70 | val front = spider.legs.getOrNull(LegLookUp.diagonalFront(index)) 71 | val back = spider.legs.getOrNull(LegLookUp.diagonalBack(index)) 72 | if (listOfNotNull(front).any { leg.target.isGrounded && (leg.timeSinceBeginMove < spider.gait.crossPairCooldown) }) return false 73 | 74 | return leg.isOutsideTriggerZone || !leg.touchingGround 75 | } else { 76 | val hasCooldown = pair.target.isGrounded && (pair.timeSinceBeginMove < spider.gait.samePairCooldown) 77 | return pair.isMoving && !hasCooldown 78 | } 79 | } 80 | } 81 | 82 | //fun hasCooldown(leg: Leg, cooldown: Int): Boolean { 83 | // return /*leg.isMoving && */leg.target.isGrounded && leg.timeSinceBeginMove < cooldown 84 | //} 85 | 86 | fun unIndexLeg(spider: SpiderBody, indices: List): List { 87 | return indices.mapNotNull { spider.legs.getOrNull(it) } 88 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spider 2 | ## Introduction 3 | This plugin was developed for a video series about procedural animations. 4 | 1. Procedural Walking Animation: https://youtu.be/Hc9x1e85L0w 5 | 2. Procedural Galloping Animation: https://youtu.be/r70xJytj0sw 6 | 3. Procedurally Animated Robots: https://youtu.be/PSnPOYeTW-0 7 | 8 | 9 | This plugin is very experimental and untested in multiplayer. Use at your own risk. 10 | 11 | 12 | 13 | ## Installation 14 | 1. Download the JAR from the [releases page](https://github.com/TheCymaera/minecraft-spider/releases/). 15 | 2. Set up a [Paper](https://papermc.io/downloads) or [Spigot](https://getbukkit.org/download/spigot) server. (Instructions below) 16 | 3. Add the JAR to the `plugins` folder. 17 | 4. Download the world folder from [Planet Minecraft](https://www.planetminecraft.com/project/spider-garden/). 18 | 5. Place the world folder in the server directory. Name it `world`. 19 | 20 | ## Running a Server 21 | 1. Download a server JAR from [Paper](https://papermc.io/downloads) or [Spigot](https://getbukkit.org/download/spigot). 22 | 2. Run the following command `java -Xmx1024M -Xms1024M -jar server.jar nogui`. 23 | 3. I typically use the Java runtime bundled with my Minecraft installation so as to avoid version conflicts. 24 | - In Modrinth, you can find the Java runtime location inside the profile options menu. 25 | 4. Accept the EULA by changing `eula=false` to `eula=true` in the `eula.txt` file. 26 | 5. Join the server with `localhost` as the IP address. 27 | 28 | 29 | ## Commands 30 | Autocomplete will show available options. 31 | 32 | Get control items: 33 | ``` 34 | /items 35 | ``` 36 | 37 | Load preset: 38 | ``` 39 | /preset 40 | /preset hexbot 1 4 41 | ``` 42 | 43 | Load torso or leg model 44 | ``` 45 | /torso_model 46 | /leg_model 47 | ``` 48 | 49 | Modify or get options 50 | ``` 51 | /options gait maxSpeed 3 52 | 53 | /options gait maxSpeed 54 | ``` 55 | 56 | Scale the spider 57 | ``` 58 | /scale 2 59 | ``` 60 | 61 | Play splay animation (Spider must be spawned) 62 | ``` 63 | /splay 64 | ``` 65 | 66 | Change eye or blinking lights palette 67 | ``` 68 | /animated_palette eye cyan_blinking_lights 69 | /animated_palette blinking_lights red_blinking_lights 70 | 71 | # Custom palette (block_id, block_brightness, sky_brightness)+ 72 | /animated_palette eye custom minecraft:stone 15 15 minecraft:redstone_block 15 15 73 | ``` 74 | 75 | Fine-grained model modification 76 | ``` 77 | /modify_model <...selectors> <...operations> 78 | 79 | # Selectors can be either a block id or a tag 80 | # e.g. Select all cloaks in the torso 81 | /modify_model cloak torso ... 82 | 83 | # e.g. Select diamond blocks and netherite blocks 84 | /modify_model minecraft:diamond_block or minecraft:netherite_block ... 85 | 86 | # Set block 87 | /modify_model cloak set_block minecraft:gray_concrete 88 | 89 | # Set brightness 90 | /modify_model cloak brightness 0 7 91 | 92 | # Copy block from the world 93 | /modify_model cloak copy_block ~ ~1 ~ 94 | ``` 95 | 96 | Stealth Variant Example: 97 | ``` 98 | /torso_model stealth 99 | /scale 1.3 100 | /modify_model cloak set_block minecraft:gray_concrete 101 | /modify_model cloak or minecraft:netherite_block or minecraft:cauldron or minecraft:anvil or minecraft:gray_shulker_box brightness 0 7 102 | 103 | # You may need to pick a different brightness depending on your shaders 104 | ``` 105 | 106 | Copy block examples used in the video: 107 | ``` 108 | /modify_model cloak torso copy_block ~ ~1 ~ 109 | /modify_model cloak tibia copy_block ~ ~1 ~ 110 | /modify_model cloak tip copy_block ~ ~1 ~ 111 | ``` 112 | 113 | ## Development 114 | 1. Clone or download the repo. 115 | 2. Run Gradle `build` or `buildDependents` to build the plugin. The JAR will be created in `build/libs`. 116 | 3. To develop on an existing server, create a symlink and add it to the server `plugins` folder. 117 | - Windows: `mklink /D newFile.jar originalFile.jar` 118 | - Mac/Linux: `ln -s originalFile.jar newFile.jar ` 119 | 4. You can configure hot swapping by following this [tutorial](https://www.youtube.com/watch?v=yQVLT6sDg68). 120 | 121 | ## License 122 | You may use the plugin and source code for both commercial or non-commercial purposes. 123 | 124 | Attribution is appreciated but not due. 125 | 126 | Do not resell without making substantial changes. -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/rendering/RenderItem.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | 3 | package com.heledron.spideranimation.utilities.rendering 4 | 5 | import com.heledron.spideranimation.utilities.custom_entities.CustomEntityComponent 6 | import com.heledron.spideranimation.utilities.custom_entities.attach 7 | import com.heledron.spideranimation.utilities.custom_entities.detach 8 | import com.heledron.spideranimation.utilities.events.onTickEnd 9 | import com.heledron.spideranimation.utilities.onPluginShutdown 10 | import org.bukkit.Location 11 | import org.bukkit.entity.Entity 12 | import kotlin.collections.iterator 13 | 14 | interface RenderItem { 15 | fun submit(handle: Any) 16 | } 17 | 18 | object EmptyRenderItem : RenderItem { 19 | override fun submit(handle: Any) { 20 | // No operation 21 | } 22 | } 23 | 24 | class RenderGroup: RenderItem { 25 | private val children = mutableMapOf() 26 | 27 | // fun add(handle: Any, item: RenderItem) { 28 | // children[handle] = item 29 | // } 30 | 31 | operator fun set(handle: Any, item: RenderItem) { 32 | children[handle] = item 33 | } 34 | 35 | operator fun get(handle: Any): RenderItem? { 36 | return children[handle] 37 | } 38 | 39 | override fun submit(handle: Any) { 40 | for ((childHandle, item) in children) { 41 | item.submit(handle to childHandle) 42 | } 43 | } 44 | } 45 | 46 | class RenderEntity ( 47 | val clazz : Class, 48 | val location : Location, 49 | val init : (T) -> Unit = {}, 50 | val update : (T) -> Unit = {}, 51 | ): RenderItem { 52 | override fun submit(handle: Any) { 53 | val entity = RenderEntityTracker.get(handle, clazz) 54 | 55 | if (entity != null) { 56 | if (!entity.isInsideVehicle) entity.teleport(location) 57 | update(entity) 58 | } else { 59 | RenderEntityTracker.put(handle, location.world!!.spawn(location, clazz) { 60 | init(it) 61 | update(it) 62 | }) 63 | } 64 | } 65 | } 66 | 67 | object RenderEntityTracker { 68 | private val rendered = mutableMapOf() 69 | private val used = mutableSetOf() 70 | 71 | private val component = CustomEntityComponent.fromString("RenderEntity") 72 | 73 | init { 74 | onTickEnd { 75 | removeUnused() 76 | } 77 | 78 | onPluginShutdown { 79 | removeAll() 80 | } 81 | 82 | // remove dangling entities from previous crashes / failed shutdowns 83 | removeAllByTag() 84 | } 85 | 86 | fun detach(id: Any) { 87 | rendered[id]?.detach(component) 88 | rendered.remove(id) 89 | used.remove(id) 90 | } 91 | 92 | fun get(handle: Any, clazz: Class): T? { 93 | val existing = rendered[handle] 94 | val isValid = existing != null && existing.type.entityClass == clazz && existing.isValid 95 | if (!isValid) { 96 | remove(handle) 97 | return null 98 | } 99 | used.add(handle) 100 | return existing as T 101 | } 102 | 103 | fun getAll(): List> { 104 | return rendered.toList() 105 | } 106 | 107 | fun put(handle: Any, entity: T): T { 108 | @Suppress("UNCHECKED_CAST") 109 | rendered.putIfAbsent(handle, entity) 110 | entity.attach(component) 111 | used.add(handle) 112 | return entity 113 | } 114 | 115 | fun remove(handle: Any) { 116 | val entity = rendered[handle] ?: return 117 | entity.remove() 118 | rendered.remove(handle) 119 | } 120 | 121 | private fun removeUnused() { 122 | val toRemove = rendered.keys - used 123 | for (key in toRemove) { 124 | val entity = rendered[key]!! 125 | entity.remove() 126 | rendered.remove(key) 127 | } 128 | used.clear() 129 | } 130 | 131 | fun removeAll() { 132 | for (entity in rendered.values) { 133 | if (!component.isAttached(entity)) continue 134 | entity.remove() 135 | } 136 | rendered.clear() 137 | } 138 | 139 | fun removeAllByTag() { 140 | component.entities().forEach { 141 | it.remove() 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/presets/SpiderLegModels.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.presets 2 | 3 | import com.heledron.spideranimation.utilities.parseModelFromCommand 4 | import org.bukkit.Material 5 | 6 | object SpiderLegModel { 7 | val BASE = parseModelFromCommand( 8 | """/summon block_display ~-0.5 ~ ~-0.5 {Passengers:[{id:"minecraft:block_display",block_state:{Name:"minecraft:anvil",Properties:{facing:"east"}},transformation:[0f,0f,0.25f,-0.125f,0.1f,0f,0f,-0.125f,0f,1f,0f,0f,0f,0f,0f,1f]},{id:"minecraft:block_display",block_state:{Name:"minecraft:cauldron",Properties:{}},transformation:[-0.25f,0f,0f,0.125f,0f,0f,0.205f,-0.0625f,0f,1.1875f,0f,-0.1875f,0f,0f,0f,1f]}]}""" 9 | ).apply { 10 | pieces.forEach { 11 | it.tags += "base" 12 | it.tags += "leg" 13 | } 14 | } 15 | 16 | val FEMUR = parseModelFromCommand( 17 | """/summon block_display ~-0.5 ~ ~-0.5 {Passengers:[{id:"minecraft:block_display",block_state:{Name:"minecraft:anvil",Properties:{facing:"east"}},transformation:[0f,0f,0.125f,-0.125f,-0.1f,0f,0f,0f,0f,-1f,0f,1f,0f,0f,0f,1f]},{id:"minecraft:block_display",block_state:{Name:"minecraft:cauldron",Properties:{}},transformation:[-0.205f,0f,0f,0.1025f,0f,0f,0.125f,0f,0f,1f,0f,0f,0f,0f,0f,1f]},{id:"minecraft:block_display",block_state:{Name:"minecraft:anvil",Properties:{facing:"east"}},transformation:[0f,0f,0.125f,0f,-0.1f,0f,0f,0f,0f,-1f,0f,1f,0f,0f,0f,1f]},{id:"minecraft:block_display",block_state:{Name:"minecraft:anvil",Properties:{facing:"east"}},transformation:[0f,0f,-0.25f,0.125f,0.1f,0f,0f,0.0125f,0f,-1f,0f,0.9981f,0f,0f,0f,1f]}]}""" 18 | ).apply { 19 | pieces.forEach { 20 | it.tags += "femur" 21 | it.tags += "leg" 22 | } 23 | } 24 | 25 | val TIBIA = parseModelFromCommand( 26 | """/summon block_display ~-0.5 ~ ~-0.5 {Passengers:[{id:"minecraft:block_display",block_state:{Name:"minecraft:smooth_quartz",Properties:{}},transformation:[0f,-0.248f,0f,0.124f,0.0739f,0f,-0.0739f,0.1494f,0.4722f,0f,0.4722f,-0.1541f,0f,0f,0f,1f],Tags:["cloak"]},{id:"minecraft:block_display",block_state:{Name:"minecraft:smooth_quartz",Properties:{}},transformation:[0f,0f,0.25f,-0.125f,-0.0012f,0.1185f,0f,0.0325f,-0.9457f,-0.0827f,0f,0.875f,0f,0f,0f,1f],Tags:["cloak"]},{id:"minecraft:block_display",block_state:{Name:"minecraft:anvil",Properties:{facing:"east"}},transformation:[-0.1425f,0f,0f,0.0625f,0f,0f,0.16f,-0.0925f,0f,0.375f,0f,0.625f,0f,0f,0f,1f]},{id:"minecraft:block_display",block_state:{Name:"minecraft:gray_shulker_box",Properties:{}},transformation:[-0.08f,0f,0f,0.04f,0f,0f,0.08f,-0.155f,0f,0.5f,0f,0.25f,0f,0f,0f,1f]},{id:"minecraft:block_display",block_state:{Name:"minecraft:anvil",Properties:{facing:"east"}},transformation:[0.0566f,0f,-0.0566f,-0.0119f,-0.0566f,0f,-0.0566f,0.0756f,0f,0.5f,0f,0f,0f,0f,0f,1f]},{id:"minecraft:block_display",block_state:{Name:"minecraft:anvil",Properties:{facing:"east"}},transformation:[0.1425f,0f,0f,-0.0712f,0f,0f,0.16f,-0.1325f,0f,-0.3125f,0f,0.3125f,0f,0f,0f,1f]},{id:"minecraft:block_display",block_state:{Name:"minecraft:anvil",Properties:{facing:"east"}},transformation:[0.0566f,0f,0.0566f,-0.0688f,-0.0566f,0f,0.0566f,0.0187f,0f,-0.3125f,0f,0.8125f,0f,0f,0f,1f]}]}""" 27 | ).apply { 28 | pieces.forEach { 29 | it.tags += "tibia" 30 | it.tags += "leg" 31 | if (it.block.material == Material.SMOOTH_QUARTZ) it.tags += "cloak" 32 | if (it.tags.contains("cloak")) it.block = Material.WHITE_CONCRETE.createBlockData() 33 | } 34 | } 35 | 36 | val TIP = parseModelFromCommand( 37 | """/summon block_display ~-0.5 ~ ~-0.5 {Passengers:[{id:"minecraft:block_display",block_state:{Name:"minecraft:smooth_quartz",Properties:{}},transformation:[0f,0f,0.1875f,-0.0938f,-0.0008f,0.1185f,0f,0.0193f,-0.619f,-0.0827f,0f,0.5627f,0f,0f,0f,1f],Tags:["cloak"]},{id:"minecraft:block_display",block_state:{Name:"minecraft:gray_shulker_box",Properties:{}},transformation:[0f,0f,0.0813f,-0.0406f,0.0813f,0f,0f,-0.0744f,0f,0.9375f,0f,0.0625f,0f,0f,0f,1f]},{id:"minecraft:block_display",block_state:{Name:"minecraft:netherite_block",Properties:{}},transformation:[0f,0f,0.1705f,-0.085f,0f,-0.125f,0f,0.0625f,0.5f,0f,0f,0f,0f,0f,0f,1f]}]}""" 38 | ).apply { 39 | pieces.forEach { 40 | it.tags += "tip" 41 | it.tags += "leg" 42 | if (it.block.material == Material.SMOOTH_QUARTZ) it.tags += "cloak" 43 | if (it.tags.contains("cloak")) it.block = Material.WHITE_CONCRETE.createBlockData() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/configuration/Gait.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.configuration 2 | 3 | import com.heledron.spideranimation.spider.components.body.GaitType 4 | import com.heledron.spideranimation.spider.components.body.SpiderBody 5 | import com.heledron.spideranimation.utilities.SplitDistance 6 | import com.heledron.spideranimation.utilities.maths.lerp 7 | import com.heledron.spideranimation.utilities.maths.toRadians 8 | import org.joml.Quaternionf 9 | 10 | 11 | class LerpGait( 12 | var bodyHeight: Double, 13 | var triggerZone: SplitDistance 14 | ) { 15 | fun scale(scale: Double): LerpGait { 16 | bodyHeight *= scale 17 | triggerZone = triggerZone.scale(scale) 18 | return this 19 | } 20 | 21 | fun clone() = LerpGait( 22 | bodyHeight = bodyHeight, 23 | triggerZone = triggerZone 24 | ) 25 | 26 | fun lerp(target: LerpGait, factor: Double): LerpGait { 27 | this.bodyHeight = bodyHeight.lerp(target.bodyHeight, factor) 28 | this.triggerZone = triggerZone.lerp(target.triggerZone, factor) 29 | return this 30 | } 31 | } 32 | 33 | 34 | class Gait( 35 | walkSpeed: Double, 36 | val type: GaitType, 37 | ) { 38 | companion object { 39 | fun defaultWalk() = Gait(.15, GaitType.WALK) 40 | 41 | fun defaultGallop() = Gait(.4, GaitType.GALLOP).apply { 42 | moving.bodyHeight = 1.6 43 | legMoveSpeed = .5 44 | rotateAcceleration = .25f / 4 45 | uncomfortableSpeedMultiplier = .6 46 | samePairCooldown = 2 47 | crossPairCooldown = 4 48 | polygonLeeway = .5 49 | } 50 | } 51 | 52 | fun scale(scale: Double) { 53 | stationary.scale(scale) 54 | moving.scale(scale) 55 | maxBodyDistanceFromGround *= scale 56 | maxSpeed *= scale 57 | moveAcceleration *= scale 58 | legMoveSpeed *= scale 59 | legLiftHeight *= scale 60 | legDropDistance *= scale 61 | comfortZone = comfortZone.scale(scale) 62 | legScanHeightBias *= scale 63 | tridentRotationalKnockBack /= scale 64 | } 65 | 66 | var stationary = LerpGait( 67 | bodyHeight = 1.1, 68 | triggerZone = SplitDistance(.25, 1.5) 69 | ) 70 | 71 | var moving = LerpGait( 72 | bodyHeight = 1.1, 73 | triggerZone = SplitDistance(.8,1.5) 74 | ) 75 | 76 | var maxBodyDistanceFromGround = .25 77 | 78 | var maxSpeed = walkSpeed 79 | var moveAcceleration = .15 / 4 80 | 81 | var rotateAcceleration = .15f / 4 82 | var rotationalDragCoefficient = .2f 83 | 84 | var legMoveSpeed = walkSpeed * 2.5 85 | 86 | var legLiftHeight = .35 87 | var legDropDistance = legLiftHeight 88 | 89 | var comfortZone = SplitDistance(1.2, 1.6) 90 | 91 | var gravityAcceleration = .08 92 | var airDragCoefficient = .02 93 | var bounceFactor = .5 94 | 95 | var bodyHeightCorrectionAcceleration = gravityAcceleration * 4 96 | var bodyHeightCorrectionFactor = .25 97 | 98 | var legScanAlternativeGround = true 99 | var legScanHeightBias = .5 100 | 101 | var tridentKnockBack = .3 102 | var tridentRotationalKnockBack = tridentKnockBack / 4 103 | var legLookAheadFraction = .6 104 | var groundDragCoefficient = .2 105 | 106 | var samePairCooldown = 1 107 | var crossPairCooldown = 1 108 | 109 | var useLegacyNormalForce = false 110 | var polygonLeeway = .0 111 | 112 | // TODO: Consider removing this 113 | var stabilizationFactor = .0 //0.7 114 | 115 | var uncomfortableSpeedMultiplier = 0.0 116 | 117 | var disableAdvancedRotation = false 118 | var preferredPitchLeeway = 10f.toRadians() 119 | 120 | var straightenLegs = true 121 | var legStraightenRotation = (-80f).toRadians() 122 | 123 | var scanPivotMode = PivotMode.YAxis 124 | var legChainPivotMode = PivotMode.SpiderOrientation 125 | 126 | var preferLevelBreakpoint = 45f.toRadians() 127 | var preferLevelBias = .0f //.2f 128 | var preferredRotationLerpFraction = .3f 129 | 130 | var rotationLerp = .3f 131 | } 132 | 133 | 134 | enum class PivotMode(val get: (spider: SpiderBody) -> Quaternionf) { 135 | YAxis({ spider -> Quaternionf().rotateY(spider.orientation.getEulerAnglesYXZ(org.joml.Vector3f()).y) }), 136 | SpiderOrientation({ spider -> spider.orientation }), 137 | GroundOrientation({ spider -> spider.preferredOrientation }) 138 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/maths.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities 2 | 3 | import com.heledron.spideranimation.utilities.maths.DOWN_VECTOR 4 | import com.heledron.spideranimation.utilities.maths.FORWARD_VECTOR 5 | import com.heledron.spideranimation.utilities.maths.lerp 6 | import org.bukkit.FluidCollisionMode 7 | import org.bukkit.World 8 | import org.bukkit.util.RayTraceResult 9 | import org.bukkit.util.Transformation 10 | import org.bukkit.util.Vector 11 | import org.joml.* 12 | import kotlin.math.abs 13 | import kotlin.math.sqrt 14 | 15 | fun Vector.rotateAroundY(angle: Double, origin: Vector) { 16 | this.subtract(origin).rotateAroundY(angle).add(origin) 17 | } 18 | 19 | fun Quaternionf.getYXZRelative(pivot: Quaternionf): Vector3f { 20 | val relative = Quaternionf(pivot).difference(this) 21 | return relative.getEulerAnglesYXZ(Vector3f()) 22 | } 23 | 24 | fun Vector.getRotationAroundAxis(pivot: Quaternionf): Vector3f { 25 | val orientation = Quaternionf().rotationTo(FORWARD_VECTOR.toVector3f(), this.toVector3f()) 26 | return orientation.getYXZRelative(pivot) 27 | } 28 | 29 | fun Vector.verticalDistance(other: Vector): Double { 30 | return abs(this.y - other.y) 31 | } 32 | 33 | fun Vector.horizontalDistance(other: Vector): Double { 34 | val x = this.x - other.x 35 | val z = this.z - other.z 36 | return sqrt(x * x + z * z) 37 | } 38 | 39 | fun Vector.horizontalLength(): Double { 40 | return sqrt(x * x + z * z) 41 | } 42 | 43 | fun List.average(): Vector { 44 | val out = Vector(0, 0, 0) 45 | for (vector in this) out.add(vector) 46 | out.multiply(1.0 / this.size) 47 | return out 48 | } 49 | 50 | class SplitDistance( 51 | val horizontal: Double, 52 | val vertical: Double 53 | ) { 54 | fun clone(): SplitDistance { 55 | return SplitDistance(horizontal, vertical) 56 | } 57 | 58 | fun scale(factor: Double): SplitDistance { 59 | return SplitDistance(horizontal * factor, vertical * factor) 60 | } 61 | 62 | fun lerp(target: SplitDistance, factor: Double): SplitDistance { 63 | return SplitDistance(horizontal.lerp(target.horizontal, factor), vertical.lerp(target.vertical, factor)) 64 | } 65 | } 66 | 67 | class SplitDistanceZone( 68 | val center: Vector, 69 | val size: SplitDistance 70 | ) { 71 | fun contains(point: Vector): Boolean { 72 | return center.horizontalDistance(point) <= size.horizontal && center.verticalDistance(point) <= size.vertical 73 | } 74 | 75 | val horizontal: Double; get() = size.horizontal 76 | val vertical: Double; get() = size.vertical 77 | } 78 | 79 | 80 | fun World.raycastGround(position: Vector, direction: Vector, maxDistance: Double): RayTraceResult? { 81 | val location = position.toLocation(this) 82 | return this.rayTraceBlocks(location, direction, maxDistance, FluidCollisionMode.NEVER, true) 83 | } 84 | 85 | fun World.isOnGround(position: Vector, downVector: Vector = DOWN_VECTOR): Boolean { 86 | return this.raycastGround(position, downVector, 0.001) != null 87 | } 88 | 89 | data class CollisionResult(val position: Vector, val offset: Vector) 90 | 91 | fun World.resolveCollision(position: Vector, direction: Vector): CollisionResult? { 92 | val location = position.toLocation(this) 93 | val ray = this.rayTraceBlocks(location.subtract(direction), direction, direction.length(), FluidCollisionMode.NEVER, true) 94 | if (ray != null) { 95 | return CollisionResult(ray.hitPosition, ray.hitPosition.clone().subtract(position)) 96 | } 97 | 98 | return null 99 | } 100 | 101 | fun lookingAtPoint(eye: Vector, direction: Vector, point: Vector, tolerance: Double): Boolean { 102 | val pointDistance = eye.distance(point) 103 | val lookingAtPoint = eye.clone().add(direction.clone().multiply(pointDistance)) 104 | return lookingAtPoint.distance(point) < tolerance 105 | } 106 | 107 | fun centredTransform(xSize: Float, ySize: Float, zSize: Float): Transformation { 108 | return Transformation( 109 | Vector3f(-xSize / 2, -ySize / 2, -zSize / 2), 110 | AxisAngle4f(0f, 0f, 0f, 1f), 111 | Vector3f(xSize, ySize, zSize), 112 | AxisAngle4f(0f, 0f, 0f, 1f) 113 | ) 114 | } 115 | 116 | fun matrixFromTransform(transformation: Transformation): Matrix4f { 117 | val matrix = Matrix4f() 118 | matrix.translate(transformation.translation) 119 | matrix.rotate(transformation.leftRotation) 120 | matrix.scale(transformation.scale) 121 | matrix.rotate(transformation.rightRotation) 122 | return matrix 123 | } 124 | -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/ecs/ecs.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.ecs 2 | 3 | /** 4 | * Bevy-style Entity Component System 5 | */ 6 | class ECS { 7 | var entities = mutableListOf() 8 | val eventListeners = mutableListOf<(Any) -> Unit>() 9 | 10 | private val startSystems = mutableListOf<(ECS) -> Unit>() 11 | private val tickSystems = mutableListOf<(ECS) -> Unit>() 12 | private val renderSystems = mutableListOf<(ECS) -> Unit>() 13 | 14 | fun onStart(func: (ECS) -> Unit) { startSystems += func } 15 | fun onTick(func: (ECS) -> Unit) { tickSystems += func } 16 | fun onRender(func: (ECS) -> Unit) { renderSystems += func } 17 | 18 | inline fun onEvent(listener: (T) -> Unit) { 19 | eventListeners += { event -> 20 | if (event is T) listener(event) 21 | } 22 | } 23 | 24 | fun emit(message: T) { 25 | for (listener in eventListeners) listener(message) 26 | } 27 | 28 | fun spawn(vararg components: Any): ECSEntity { 29 | val entity = ECSEntity() 30 | for (component in components) { 31 | entity.addComponent(component) 32 | } 33 | entities.add(entity) 34 | return entity 35 | } 36 | 37 | @JvmName("query1") 38 | inline fun query(): Iterable { 39 | // if (!inSystem) throw Error("Cannot query outside of a system") 40 | return entities.mapNotNull { it.query() } 41 | } 42 | 43 | @JvmName("query2") 44 | inline fun query(): Iterable> { 45 | // if (!inSystem) throw Error("Cannot query outside of a system") 46 | return entities.mapNotNull { entity -> 47 | val comp1 = entity.query() ?: return@mapNotNull null 48 | val comp2 = entity.query() ?: return@mapNotNull null 49 | Pair(comp1, comp2) 50 | } 51 | } 52 | 53 | @JvmName("query3") 54 | inline fun query(): Iterable> { 55 | // if (!inSystem) throw Error("Cannot query outside of a system") 56 | return entities.mapNotNull { entity -> 57 | val comp1 = entity.query() ?: return@mapNotNull null 58 | val comp2 = entity.query() ?: return@mapNotNull null 59 | val comp3 = entity.query() ?: return@mapNotNull null 60 | Triple(comp1, comp2, comp3) 61 | } 62 | } 63 | 64 | @JvmName("query4") 65 | inline fun query(): Iterable> { 66 | // if (!inSystem) throw Error("Cannot query outside of a system") 67 | return entities.mapNotNull { entity -> 68 | val comp1 = entity.query() ?: return@mapNotNull null 69 | val comp2 = entity.query() ?: return@mapNotNull null 70 | val comp3 = entity.query() ?: return@mapNotNull null 71 | val comp4 = entity.query() ?: return@mapNotNull null 72 | Quadruple(comp1, comp2, comp3, comp4) 73 | } 74 | } 75 | 76 | fun start() { 77 | for (system in startSystems) system(this) 78 | } 79 | 80 | fun update() { 81 | for (system in tickSystems) system(this) 82 | 83 | entities.removeIf { it.scheduledForRemoval } 84 | } 85 | 86 | fun render() { 87 | for (system in renderSystems) system(this) 88 | } 89 | } 90 | 91 | 92 | class ECSEntity { 93 | val components = mutableListOf() 94 | 95 | var scheduledForRemoval = false 96 | 97 | fun remove() { 98 | this.scheduledForRemoval = true 99 | } 100 | 101 | inline fun addComponent(component: T) { 102 | components.add(component) 103 | } 104 | 105 | inline fun removeComponent() { 106 | components.removeIf { it is T } 107 | } 108 | 109 | inline fun replaceComponent(component: Any) { 110 | removeComponent() 111 | addComponent(component) 112 | } 113 | 114 | inline fun query(): T? { 115 | if (this is T) return this 116 | return components.find { it is T } as T? 117 | } 118 | } 119 | 120 | 121 | class Quadruple( 122 | val first: A, 123 | val second: B, 124 | val third: C, 125 | val forth: D, 126 | ) { 127 | operator fun component1() = first 128 | operator fun component2() = second 129 | operator fun component3() = third 130 | operator fun component4() = forth 131 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/rendering/textDisplays.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.rendering 2 | 3 | import com.heledron.spideranimation.utilities.currentPlugin 4 | import com.heledron.spideranimation.utilities.maths.normal 5 | import com.heledron.spideranimation.utilities.maths.shear 6 | import com.heledron.spideranimation.utilities.maths.toRadians 7 | import com.heledron.spideranimation.utilities.overloads.eyePosition 8 | import com.heledron.spideranimation.utilities.overloads.position 9 | import org.bukkit.Bukkit 10 | import org.bukkit.entity.TextDisplay 11 | import org.joml.Matrix4f 12 | import org.joml.Quaternionf 13 | import org.joml.Vector3f 14 | import kotlin.math.abs 15 | import kotlin.math.sign 16 | 17 | val textDisplayUnitSquare: Matrix4f; get() = Matrix4f() 18 | .translate(-0.1f + .5f,-0.5f + .5f,0f) 19 | .scale(8.0f,4.0f,1f) // + 0.003f + 0.001f 20 | 21 | // Left aligned 22 | val textDisplayUnitTriangle; get() = listOf( 23 | // Left 24 | Matrix4f().scale(.5f).mul(textDisplayUnitSquare), 25 | // Right 26 | Matrix4f().scale(.5f).translate(1f, 0f, 0f).shear(yx = -1f).mul(textDisplayUnitSquare), 27 | // Top 28 | Matrix4f().scale(.5f).translate(0f,1f,0f).shear(xy = -1f).mul(textDisplayUnitSquare), 29 | ) 30 | 31 | // Right aligned 32 | //private val textDisplayUnitTriangleMutable = listOf( 33 | // // Right 34 | // Matrix4f().scale(.5f).shear(1f, 0f, 0f).mul(textDisplayUnitSquare), 35 | // // Left 36 | // Matrix4f().scale(.5f).translate(1f, 0f, 0f).mul(textDisplayUnitSquare), 37 | // // Top 38 | // Matrix4f().scale(.5f).translate(1f,0f,0f).shear(0f, 1f, 0f).mul(textDisplayUnitSquare), 39 | //) 40 | 41 | class TextDisplayTriangleResult( 42 | val transforms: List, 43 | val xAxis: Vector3f, 44 | val yAxis: Vector3f, 45 | val zAxis: Vector3f, 46 | val height: Float, 47 | val width: Float, 48 | val rotation: Quaternionf, 49 | val shear: Float, 50 | ) 51 | 52 | fun textDisplayTriangle( 53 | point1: Vector3f, 54 | point2: Vector3f, 55 | point3: Vector3f, 56 | ): TextDisplayTriangleResult { 57 | val p2 = Vector3f(point2).sub(point1) 58 | val p3 = Vector3f(point3).sub(point1) 59 | 60 | val zAxis = Vector3f(p2).cross(p3).normalize() 61 | val xAxis = Vector3f(p2).normalize() 62 | val yAxis = Vector3f(zAxis).cross(xAxis).normalize() 63 | 64 | val width = p2.length() 65 | val height = Vector3f(p3).dot(yAxis) 66 | val p3Width = Vector3f(p3).dot(xAxis) 67 | 68 | val rotation = Quaternionf().lookAlong(Vector3f(zAxis).mul(-1f), yAxis).conjugate() 69 | 70 | val shear = p3Width / width 71 | // val shear = - (1f - p3Width / width) 72 | 73 | val transform = Matrix4f() 74 | .translate(point1) 75 | .rotate(rotation) 76 | .scale(width, height, 1f) 77 | .shear(yx = shear) 78 | 79 | return TextDisplayTriangleResult( 80 | transforms = textDisplayUnitTriangle.map { unit -> Matrix4f(transform).mul(unit) }, 81 | xAxis = xAxis, 82 | yAxis = yAxis, 83 | zAxis = zAxis, 84 | height = height, 85 | width = width, 86 | rotation = rotation, 87 | shear = shear, 88 | ) 89 | } 90 | 91 | 92 | 93 | fun TextDisplay.cull() { 94 | val visiblePosition = position.toVector3f().add(transformation.translation) 95 | val normal = transformation.normal() 96 | 97 | Bukkit.getOnlinePlayers().forEach { player -> 98 | val direction = player.eyePosition.toVector3f().sub(visiblePosition) 99 | val angle = direction.angle(normal) 100 | 101 | val isFacing = angle < 95.0f.toRadians() 102 | if (isFacing) { 103 | player.showEntity(currentPlugin, this) 104 | } else { 105 | player.hideEntity(currentPlugin, this) 106 | } 107 | } 108 | } 109 | 110 | fun TextDisplay.interpolateTriangleTransform(matrix: Matrix4f) { 111 | val oldTransformation = this.transformation 112 | setTransformationMatrix(matrix) 113 | 114 | val rightRotationChange = Quaternionf(oldTransformation.rightRotation).difference(transformation.rightRotation) 115 | .getEulerAnglesXYZ(Vector3f()) 116 | 117 | if (abs(rightRotationChange.z) >= 45f.toRadians()) { 118 | this.transformation = this.transformation.apply { 119 | val rot = (-90f).toRadians() * rightRotationChange.z.sign 120 | 121 | leftRotation.rotateZ(-rot) 122 | scale.set(scale.y, scale.x, scale.z) 123 | rightRotation.set(Quaternionf().rotateZ(rot).mul(rightRotation)) 124 | } 125 | } 126 | 127 | if (oldTransformation == this.transformation) return 128 | this.interpolationDelay = 0 129 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/colors/colors.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.colors 2 | 3 | import com.heledron.spideranimation.utilities.maths.lerpSafely 4 | import net.md_5.bungee.api.ChatColor 5 | import org.bukkit.Color 6 | import kotlin.math.abs 7 | import kotlin.math.pow 8 | import kotlin.math.sqrt 9 | 10 | 11 | fun Color.blendAlpha(top: Color): Color { 12 | val bottom = this 13 | 14 | val alpha = top.alpha / 255.0 15 | val topAlpha = bottom.alpha / 255.0 16 | val blendedAlpha = alpha + topAlpha * (1 - alpha) 17 | val r = (top.red * alpha + bottom.red * topAlpha * (1 - alpha)) / blendedAlpha 18 | val g = (top.green * alpha + bottom.green * topAlpha * (1 - alpha)) / blendedAlpha 19 | val b = (top.blue * alpha + bottom.blue * topAlpha * (1 - alpha)) / blendedAlpha 20 | return Color.fromARGB((blendedAlpha * 255).toInt(), r.toInt(), g.toInt(), b.toInt()) 21 | } 22 | 23 | fun Color.lerpRGB(other: Color, t: Float): Color { 24 | return Color.fromARGB( 25 | this.alpha.lerpSafely(other.alpha, t), 26 | this.red.lerpSafely(other.red, t), 27 | this.green.lerpSafely(other.green, t), 28 | this.blue.lerpSafely(other.blue, t), 29 | ) 30 | } 31 | 32 | fun Color.value(): Float { 33 | return ((red + green + blue) / 3f) / 255f 34 | } 35 | 36 | fun Color.scaleRGB(value: Float): Color { 37 | val r = (red * value).toInt().coerceIn(0, 255) 38 | val g = (green * value).toInt().coerceIn(0, 255) 39 | val b = (blue * value).toInt().coerceIn(0, 255) 40 | return Color.fromARGB(alpha, r, g, b) 41 | } 42 | 43 | fun Color.scaleAlpha(value: Float): Color { 44 | val a = (alpha * value).toInt().coerceIn(0, 255) 45 | return Color.fromARGB(a, red, green, blue) 46 | } 47 | 48 | fun Color.toHSV(): Triple { 49 | val r = red / 255f 50 | val g = green / 255f 51 | val b = blue / 255f 52 | 53 | val max = maxOf(r, g, b) 54 | val min = minOf(r, g, b) 55 | 56 | val delta = max - min 57 | 58 | val h = when { 59 | delta == 0f -> 0f 60 | max == r -> 60 * (((g - b) / delta) % 6) 61 | max == g -> 60 * ((b - r) / delta + 2) 62 | max == b -> 60 * ((r - g) / delta + 4) 63 | else -> error("Unreachable") 64 | } 65 | 66 | val s = if (max == 0f) 0f else delta / max 67 | val v = max 68 | 69 | return Triple(h, s, v) 70 | } 71 | 72 | typealias ColorGradient = List> 73 | 74 | fun ColorGradient.interpolate(t: Float, lerpFunction: (Color, Color, Float) -> Color): Color { 75 | val index = this.indexOfLast { it.first <= t } 76 | if (index == this.size - 1) return this.last().second 77 | val start = this[index] 78 | val end = this[index + 1] 79 | // return start.second.lerp(end.second, (t - start.first) / (end.first - start.first)) 80 | return lerpFunction(start.second, end.second, (t - start.first) / (end.first - start.first)) 81 | } 82 | 83 | fun ColorGradient.interpolateRGB(t: Float): Color { 84 | return interpolate(t) { start, end, fraction -> start.lerpRGB(end, fraction) } 85 | } 86 | 87 | fun ColorGradient.interpolateOkLab(t: Float): Color { 88 | return interpolate(t) { start, end, fraction -> start.lerpOkLab(end, fraction) } 89 | } 90 | 91 | fun Color.lerpOkLab(other: Color, t: Float): Color { 92 | val start = this.toOklab() 93 | val end = other.toOklab() 94 | val result = start.lerp(end, t).toRGB() 95 | return result 96 | } 97 | 98 | fun Color.distanceTo(other: Color): Float { 99 | return sqrt((red - other.red).toFloat().pow(2) + (green - other.green).toFloat().pow(2) + (blue - other.blue).toFloat().pow(2)) 100 | } 101 | 102 | fun Color.toChatColor(): ChatColor { 103 | return ChatColor.of(java.awt.Color(red, green, blue)) 104 | } 105 | 106 | //fun Color.hsvLerp(other: Color, t: Double): Color { 107 | // val (h1, s1, v1) = this.toHSV() 108 | // val (h2, s2, v2) = other.toHSV() 109 | // 110 | // val h = h1.lerp(h2, t) 111 | // val s = s1.lerp(s2, t) 112 | // val v = v1.lerp(v2, t) 113 | // val a = this.alpha.toDouble().lerp(other.alpha.toDouble(), t).toInt() 114 | // 115 | // return hsv(h, s, v).setAlpha(a) 116 | //} 117 | 118 | /** 119 | * Converts an HSV color to RGB. 120 | * @param h The hue in degrees. (0 - 360) 121 | * @param s The saturation as a percentage. (0 - 1) 122 | * @param v The value as a percentage. (0 - 1) 123 | * @return The RGB color. 124 | */ 125 | fun hsv(h: Float, s: Float, v: Float): Color { 126 | val c = v * s 127 | val x = c * (1 - abs((h / 60) % 2 - 1)) 128 | val m = v - c 129 | val (r, g, b) = when { 130 | h < 60 -> Triple(c, x, 0f) 131 | h < 120 -> Triple(x, c, 0f) 132 | h < 180 -> Triple(0f, c, x) 133 | h < 240 -> Triple(0f, x, c) 134 | h < 300 -> Triple(x, 0f, c) 135 | else -> Triple(c, 0f, x) 136 | } 137 | return Color.fromRGB(((r + m) * 255).toInt(), ((g + m) * 255).toInt(), ((b + m) * 255).toInt()) 138 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/block_colors/blockColorMaps.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.block_colors 2 | 3 | import com.google.gson.Gson 4 | import com.heledron.spideranimation.utilities.colors.Oklab 5 | import com.heledron.spideranimation.utilities.colors.toOklab 6 | import com.heledron.spideranimation.utilities.requireResource 7 | import org.bukkit.Color 8 | import org.bukkit.Material 9 | 10 | private val blocks = requireResource("block_colors.json") 11 | .bufferedReader() 12 | .use { it.readText() } 13 | .let { 14 | val colorMap = mutableMapOf() 15 | 16 | @Suppress("UNCHECKED_CAST") 17 | val json = Gson().fromJson(it, Map::class.java) as Map> 18 | 19 | for ((key, value) in json) { 20 | val material = Material.matchMaterial("minecraft:$key") ?: continue 21 | colorMap[material] = Color.fromRGB(value[0], value[1], value[2]) 22 | } 23 | 24 | colorMap 25 | } 26 | .apply { 27 | // replace missing/incorrect colors 28 | this[Material.GRASS_BLOCK] = this[Material.MOSS_BLOCK]!! 29 | this[Material.OAK_LEAVES] = this[Material.MOSS_BLOCK]!!.withBrightness(10) 30 | this[Material.BIRCH_LEAVES] = this[Material.MOSS_BLOCK]!!.withBrightness(8) 31 | this[Material.SPRUCE_LEAVES] = this[Material.MOSS_BLOCK]!!.withBrightness(6) 32 | this[Material.WARPED_TRAPDOOR] = this[Material.WARPED_PLANKS]!! 33 | this[Material.BARREL] = this[Material.SPRUCE_PLANKS]!! 34 | this[Material.RESPAWN_ANCHOR] = this[Material.CRYING_OBSIDIAN]!! 35 | 36 | // add wood from logs 37 | for ((material, color) in this.toList()) { 38 | val woodMaterial = getWoodVariant(material) 39 | if (woodMaterial != null) this[woodMaterial] = color 40 | } 41 | } 42 | .toMap() 43 | 44 | internal val colorToBlock = run { 45 | val out = mutableMapOf() 46 | 47 | for (brightness in 15 downTo 0) { 48 | for ((material, color) in blocks) { 49 | // skip non occluding blocks 50 | if (!material.isOccluding) continue 51 | 52 | // skip logs if there is a wood variant 53 | val woodVariant = getWoodVariant(material) 54 | if (woodVariant != null && blocks.containsKey(woodVariant)) continue 55 | 56 | // skip spawners 57 | if (material == Material.SPAWNER || material == Material.TRIAL_SPAWNER) continue 58 | 59 | 60 | // skip shulker boxes 61 | val id = material.idString() 62 | if (id.endsWith("shulker_box")) continue 63 | 64 | val newColor = color.withBrightness(brightness) 65 | if (newColor in out) continue 66 | 67 | out[newColor] = BlockColorInfo( 68 | material = material, 69 | brightness = brightness, 70 | rgb = newColor, 71 | oklab = newColor.toOklab(), 72 | ) 73 | } 74 | } 75 | 76 | out.values.toList() 77 | } 78 | 79 | internal val blockToColor = run { 80 | val out = blocks.toMutableMap() 81 | 82 | // infer color of partial blocks from their full block counterparts 83 | for ((material, color) in blocks) { 84 | val id = material.idString() 85 | 86 | val variations = listOf( 87 | id, 88 | id.replaceEnd("_planks", ""), 89 | id.replaceEnd("_wood", ""), 90 | id.replaceEnd("s", "") 91 | ).flatMap { baseId -> 92 | listOf( 93 | "${baseId}_slab", 94 | "${baseId}_stairs", 95 | "${baseId}_wall", 96 | "${baseId}_trapdoor", 97 | "waxed_$baseId", 98 | ) 99 | } 100 | 101 | for (variantId in variations) { 102 | val variantMaterial = Material.matchMaterial(variantId) ?: continue 103 | if (out.containsKey(variantMaterial)) continue 104 | out[variantMaterial] = color 105 | // println("Inferred: ${variantId.padEnd(40)} from $id") 106 | } 107 | } 108 | 109 | out[Material.CAMPFIRE] = out[Material.OAK_LOG]!! 110 | 111 | out 112 | } 113 | 114 | internal class BlockColorInfo( 115 | val material: Material, 116 | val brightness: Int, 117 | val rgb: Color, 118 | val oklab: Oklab, 119 | ) 120 | 121 | private fun String.replaceEnd(suffix: String, with: String): String { 122 | return if (endsWith(suffix)) { 123 | substring(0, length - suffix.length) + with 124 | } else { 125 | this 126 | } 127 | } 128 | 129 | private fun getWoodVariant(material: Material): Material? { 130 | val id = material.idString() 131 | 132 | if (id.endsWith("_log")) { 133 | return Material.matchMaterial(id.replaceEnd("_log", "_wood")) 134 | } 135 | 136 | if (id.endsWith("_stem")) { 137 | return Material.matchMaterial(id.replaceEnd("_stem", "_hyphae")) 138 | } 139 | 140 | return null 141 | } 142 | 143 | private fun Material.idString(): String { 144 | return this.keyOrThrow.toString() 145 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/Behaviour.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components 2 | 3 | import com.heledron.spideranimation.spider.components.body.SpiderBody 4 | import com.heledron.spideranimation.utilities.* 5 | import com.heledron.spideranimation.utilities.ecs.ECS 6 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 7 | import com.heledron.spideranimation.utilities.maths.FORWARD_VECTOR 8 | import com.heledron.spideranimation.utilities.maths.moveTowards 9 | import org.bukkit.util.Vector 10 | import org.joml.Quaternionf 11 | import org.joml.Vector3f 12 | import kotlin.math.PI 13 | 14 | 15 | interface SpiderBehaviour 16 | 17 | class StayStillBehaviour() : SpiderBehaviour 18 | 19 | class TargetBehaviour(val target: Vector, val distance: Double) : SpiderBehaviour 20 | 21 | class DirectionBehaviour(val targetDirection: Vector, val walkDirection: Vector) : SpiderBehaviour 22 | 23 | fun setupBehaviours(app: ECS) { 24 | // Stay still behaviour 25 | app.onTick { 26 | for ((entity, spider, _) in app.query()) { 27 | val tridentDetector = entity.query() 28 | spider.walkAt(Vector(0.0, 0.0, 0.0), tridentDetector) 29 | spider.rotateTowards(spider.forwardDirection().setY(0.0)) 30 | } 31 | } 32 | 33 | // Target behaviour 34 | app.onTick { 35 | for ((entity, spider, behaviour) in app.query()) { 36 | val direction = behaviour.target.clone().subtract(spider.position).normalize() 37 | spider.rotateTowards(direction) 38 | 39 | val currentSpeed = spider.velocity.length() 40 | 41 | val decelerateDistance = (currentSpeed * currentSpeed) / (2 * spider.gait.moveAcceleration) 42 | 43 | val currentDistance = spider.position.horizontalDistance(behaviour.target) 44 | 45 | val tridentDetector = entity.query() 46 | if (currentDistance > behaviour.distance + decelerateDistance) { 47 | spider.walkAt(direction.clone().multiply(spider.gait.maxSpeed), tridentDetector) 48 | } else { 49 | spider.walkAt(Vector(0.0, 0.0, 0.0), tridentDetector) 50 | } 51 | } 52 | } 53 | 54 | // Direction behaviour 55 | app.onTick { 56 | for ((entity, spider, behaviour) in app.query()) { 57 | spider.rotateTowards(behaviour.targetDirection) 58 | 59 | 60 | val tridentDetector = entity.query() 61 | spider.walkAt( 62 | behaviour.walkDirection.clone().multiply(spider.gait.maxSpeed), 63 | tridentDetector 64 | ) 65 | } 66 | } 67 | } 68 | 69 | 70 | 71 | private fun SpiderBody.rotateTowards(targetVector: Vector) { 72 | // val maxAcceleration = moveGait.rotateAcceleration * body.legs.filter { it.isGrounded() }.size / body.legs.size 73 | // yawVelocity = yawVelocity.moveTowards(.0f, maxAcceleration) 74 | // pitchVelocity = pitchVelocity.moveTowards(.0f, maxAcceleration) 75 | // rollVelocity = rollVelocity.moveTowards(.0f, maxAcceleration) 76 | 77 | val currentEuler = orientation.getEulerAnglesYXZ(Vector3f()) 78 | 79 | val targetEuler = Quaternionf() 80 | .rotationTo(FORWARD_VECTOR.toVector3f(), targetVector.toVector3f()) 81 | .getEulerAnglesYXZ(Vector3f()) 82 | 83 | // clamp pitch 84 | targetEuler.x = targetEuler.x.coerceIn(preferredPitch - gait.preferredPitchLeeway, preferredPitch + gait.preferredPitchLeeway) 85 | 86 | // clamp roll 87 | targetEuler.z = preferredRoll 88 | 89 | // clamp yaw if uncomfortable 90 | if (legs.any { it.isUncomfortable && !it.isMoving }) targetEuler.y = currentEuler.y 91 | 92 | // get diff 93 | val diffEuler = Vector3f(targetEuler).sub(currentEuler) 94 | if (diffEuler.y > PI) diffEuler.y -= 2 * PI.toFloat() 95 | if (diffEuler.y < -PI) diffEuler.y += 2 * PI.toFloat() 96 | 97 | isRotatingYaw = (diffEuler.x + diffEuler.y + diffEuler.z) > 0.001f 98 | diffEuler.lerp(Vector3f(), gait.rotationLerp) 99 | 100 | 101 | val diff = Quaternionf().rotationYXZ(diffEuler.y, diffEuler.x, diffEuler.z) 102 | 103 | // convert to premultiplied 104 | val conjugated = Quaternionf(orientation).mul(diff).mul(Quaternionf(orientation).invert()) 105 | 106 | val conjugatedEuler = conjugated.getEulerAnglesYXZ(Vector3f()) 107 | val maxAcceleration = gait.rotateAcceleration * legs.filter { it.isGrounded() }.size / legs.size 108 | rotationalVelocity.moveTowards(conjugatedEuler, maxAcceleration) 109 | } 110 | 111 | private fun SpiderBody.walkAt(targetVelocity: Vector, tridentDetector: TridentHitDetector?) { 112 | val acceleration = gait.moveAcceleration// * body.legs.filter { it.isGrounded() }.size / body.legs.size 113 | val target = targetVelocity.clone() 114 | 115 | if (legs.any { it.isUncomfortable && !it.isMoving }) { // && !it.targetOutsideComfortZone 116 | val scaled = target.setY(velocity.y).multiply(gait.uncomfortableSpeedMultiplier) 117 | velocity.moveTowards(scaled, acceleration) 118 | isWalking = targetVelocity.x != 0.0 && targetVelocity.z != 0.0 119 | } else { 120 | velocity.moveTowards(target.setY(velocity.y), acceleration) 121 | isWalking = velocity.x != 0.0 && velocity.z != 0.0 122 | } 123 | 124 | if (tridentDetector != null && tridentDetector.stunned && targetVelocity.isZero) isWalking = false 125 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/presets/presets.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.presets 2 | 3 | import com.heledron.spideranimation.spider.configuration.BodyPlan 4 | import com.heledron.spideranimation.spider.configuration.LegPlan 5 | import com.heledron.spideranimation.spider.configuration.SegmentPlan 6 | import com.heledron.spideranimation.spider.configuration.SpiderOptions 7 | import com.heledron.spideranimation.utilities.maths.FORWARD_VECTOR 8 | import org.bukkit.Material 9 | import org.bukkit.util.Vector 10 | 11 | 12 | private fun equalLength(segmentCount: Int, length: Double): List { 13 | return List(segmentCount) { SegmentPlan(length, FORWARD_VECTOR) } 14 | } 15 | 16 | private fun BodyPlan.addLegPair(root: Vector, rest: Vector, segments: List) { 17 | legs += LegPlan(Vector( root.x, root.y, root.z), Vector( rest.x, rest.y, rest.z), segments) 18 | legs += LegPlan(Vector(-root.x, root.y, root.z), Vector(-rest.x, rest.y, rest.z), segments.map { it.clone() }) 19 | } 20 | 21 | fun biped(segmentCount: Int, segmentLength: Double): SpiderOptions { 22 | val options = SpiderOptions() 23 | options.bodyPlan.addLegPair(Vector(.0, .0, .0), Vector(1.0, .0, .0), equalLength(segmentCount, 1.0 * segmentLength)) 24 | applyLineLegModel(options.bodyPlan, Material.NETHERITE_BLOCK.createBlockData()) 25 | return options 26 | } 27 | 28 | fun quadruped(segmentCount: Int, segmentLength: Double): SpiderOptions { 29 | val options = SpiderOptions() 30 | options.bodyPlan.addLegPair(Vector(.0, .0, .0), Vector(0.9,.0, 0.9), equalLength(segmentCount, 0.9 * segmentLength)) 31 | options.bodyPlan.addLegPair(Vector(.0, .0, .0), Vector(1.0, .0, -1.1), equalLength(segmentCount, 1.2 * segmentLength)) 32 | applyLineLegModel(options.bodyPlan, Material.NETHERITE_BLOCK.createBlockData()) 33 | return options 34 | } 35 | 36 | fun hexapod(segmentCount: Int, segmentLength: Double): SpiderOptions { 37 | val options = SpiderOptions() 38 | options.bodyPlan.addLegPair(Vector(.0,.0,0.1), Vector(1.0,.0, 1.1), equalLength(segmentCount, 1.1 * segmentLength)) 39 | options.bodyPlan.addLegPair(Vector(.0,.0,0.0), Vector(1.3,.0,-0.3), equalLength(segmentCount, 1.1 * segmentLength)) 40 | options.bodyPlan.addLegPair(Vector(.0,.0,-.1), Vector(1.2,.0,-2.0), equalLength(segmentCount, 1.6 * segmentLength)) 41 | applyLineLegModel(options.bodyPlan, Material.NETHERITE_BLOCK.createBlockData()) 42 | return options 43 | } 44 | 45 | fun octopod(segmentCount: Int, segmentLength: Double): SpiderOptions { 46 | val options = SpiderOptions() 47 | options.bodyPlan.addLegPair(Vector(.0,.0, .1), Vector(1.0, .0, 1.6), equalLength(segmentCount, 1.1 * segmentLength)) 48 | options.bodyPlan.addLegPair(Vector(.0,.0, .0), Vector(1.3, .0, 0.4), equalLength(segmentCount, 1.0 * segmentLength)) 49 | options.bodyPlan.addLegPair(Vector(.0,.0, -.1), Vector(1.3, .0, -0.9), equalLength(segmentCount, 1.1 * segmentLength)) 50 | options.bodyPlan.addLegPair(Vector(.0,.0, -.2), Vector(1.1, .0, -2.5), equalLength(segmentCount, 1.6 * segmentLength)) 51 | applyLineLegModel(options.bodyPlan, Material.NETHERITE_BLOCK.createBlockData()) 52 | return options 53 | } 54 | 55 | 56 | private fun createRobotSegments(segmentCount: Int, lengthScale: Double) = List(segmentCount) { index -> 57 | var length = lengthScale.toFloat() 58 | var initDirection = FORWARD_VECTOR 59 | 60 | if (index == 0) { 61 | length *= .5f 62 | initDirection = initDirection.rotateAroundX(Math.PI / 3) 63 | } 64 | 65 | if (index == 1) length *= .8f 66 | 67 | SegmentPlan(length.toDouble(), initDirection) 68 | } 69 | 70 | 71 | fun quadBot(segmentCount: Int, segmentLength: Double): SpiderOptions { 72 | val options = SpiderOptions() 73 | options.bodyPlan.bodyModel = SpiderTorsoModels.FLAT.model.clone() 74 | options.bodyPlan.addLegPair(root = Vector(.2,-.2 - .15, .2), rest = Vector(1.3 * 1.0,.0, 1.0), createRobotSegments(segmentCount, .9 * .7 * segmentLength)) 75 | options.bodyPlan.addLegPair(root = Vector(.2,-.2 - .15,-.2), rest = Vector(1.3 * 1.1,.0,-1.2), createRobotSegments(segmentCount, 1.2 * .7 * segmentLength)) 76 | applyMechanicalLegModel(options.bodyPlan) 77 | return options 78 | } 79 | 80 | fun hexBot(segmentCount: Int, segmentLength: Double): SpiderOptions { 81 | val options = SpiderOptions() 82 | options.bodyPlan.bodyModel = SpiderTorsoModels.FLAT.model.clone() 83 | options.bodyPlan.addLegPair(root = Vector(.2,-.2 - .15, .2), rest = Vector(1.3 * 1.0,.0, 1.3), createRobotSegments(segmentCount, 1.1 * .7 * segmentLength)) 84 | options.bodyPlan.addLegPair(root = Vector(.2,-.2 - .15, .0), rest = Vector(1.3 * 1.2,.0,-0.1), createRobotSegments(segmentCount, 1.1 * .7 * segmentLength)) 85 | options.bodyPlan.addLegPair(root = Vector(.2,-.2 - .15,-.2), rest = Vector(1.3 * 1.1,.0,-1.6), createRobotSegments(segmentCount, 1.3 * .7 * segmentLength)) 86 | applyMechanicalLegModel(options.bodyPlan) 87 | return options 88 | } 89 | 90 | fun octoBot(segmentCount: Int, segmentLength: Double): SpiderOptions { 91 | val options = SpiderOptions() 92 | options.bodyPlan.bodyModel = SpiderTorsoModels.FLAT.model.clone() 93 | options.bodyPlan.addLegPair(root = Vector(.2,-.2 - .15, .3), rest = Vector(1.3 * 1.0,.0, 1.3), createRobotSegments(segmentCount, 1.1 * .7 * segmentLength)) 94 | options.bodyPlan.addLegPair(root = Vector(.2,-.2 - .15, .1), rest = Vector(1.3 * 1.2,.0, 0.5), createRobotSegments(segmentCount, 1.0 * .7 * segmentLength)) 95 | options.bodyPlan.addLegPair(root = Vector(.2,-.2 - .15, .1), rest = Vector(1.3 * 1.2,.0,-0.7), createRobotSegments(segmentCount, 1.1 * .7 * segmentLength)) 96 | options.bodyPlan.addLegPair(root = Vector(.2,-.2 - .15,-.3), rest = Vector(1.3 * 1.1,.0,-1.6), createRobotSegments(segmentCount, 1.3 * .7 * segmentLength)) 97 | applyMechanicalLegModel(options.bodyPlan) 98 | return options 99 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/SoundsAndParticles.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components 2 | 3 | import com.heledron.spideranimation.spider.components.body.Leg 4 | import com.heledron.spideranimation.spider.components.body.LegStepEvent 5 | import com.heledron.spideranimation.spider.components.body.SpiderBody 6 | import com.heledron.spideranimation.spider.components.body.SpiderBodyHitGroundEvent 7 | import com.heledron.spideranimation.spider.configuration.SoundOptions 8 | import com.heledron.spideranimation.spider.configuration.SoundPlayer 9 | import com.heledron.spideranimation.utilities.ecs.ECS 10 | import com.heledron.spideranimation.utilities.overloads.playSound 11 | import org.bukkit.Particle 12 | import org.bukkit.Sound 13 | import org.bukkit.World 14 | import org.bukkit.block.data.Waterlogged 15 | import org.bukkit.util.Vector 16 | import java.util.* 17 | import kotlin.collections.set 18 | import kotlin.random.Random 19 | 20 | class SoundsAndParticles(var options: SoundOptions) { 21 | var timeSinceLastSound = 0 22 | var wetness = WeakHashMap() 23 | val maxWetness = 20 * 3 24 | 25 | fun underwaterStepSound() = SoundPlayer( 26 | sound = options.step.sound, 27 | volume = options.step.volume * .5f, 28 | pitch = options.step.pitch * .75f, 29 | volumeVary = options.step.volumeVary, 30 | pitchVary = options.step.pitchVary 31 | ) 32 | } 33 | 34 | fun setupSoundAndParticles(app: ECS) { 35 | app.onEvent { event -> 36 | event.spider.world.playSound(event.spider.position, Sound.BLOCK_NETHERITE_BLOCK_FALL, 1.0f, .8f) 37 | } 38 | 39 | app.onEvent { event -> 40 | event.spider.world.playSound(event.spider.position, Sound.ENTITY_ZOMBIE_ATTACK_IRON_DOOR, .5f, 1.0f) 41 | } 42 | 43 | app.onEvent { event -> 44 | event.spider.world.playSound(event.spider.position, Sound.BLOCK_LODESTONE_PLACE, 1.0f, 0.0f) 45 | } 46 | 47 | app.onEvent { event -> 48 | event.spider.world.playSound(event.spider.position, Sound.BLOCK_RESPAWN_ANCHOR_DEPLETE, .5f, 1.5f) 49 | event.spider.world.playSound(event.spider.position, Sound.BLOCK_LODESTONE_PLACE, 0.1f, 0.0f) 50 | event.spider.world.playSound(event.spider.position, Sound.ENTITY_ZOMBIE_VILLAGER_CURE, .02f, 1.5f) 51 | } 52 | 53 | app.onEvent { event -> 54 | val isUnderWater = event.spider.world.getBlockAt(event.leg.endEffector.toLocation(event.spider.world)).isLiquid 55 | 56 | val sounds = event.entity.query() ?: return@onEvent 57 | 58 | val sound = if (isUnderWater) sounds.underwaterStepSound() else sounds.options.step 59 | 60 | sound.play(event.spider.world, event.leg.endEffector) 61 | } 62 | 63 | app.onTick { 64 | for ((spider, sounds) in app.query()) { 65 | sounds.timeSinceLastSound++ 66 | 67 | for (leg in spider.legs) { 68 | spawnLegParticles(sounds, spider.world, leg) 69 | } 70 | } 71 | } 72 | } 73 | 74 | private fun isInWater(world: World, position: Vector): Boolean { 75 | val block = world.getBlockAt(position.toLocation(world)) 76 | return block.isLiquid || (block is Waterlogged && block.isWaterlogged) 77 | } 78 | 79 | 80 | private fun spawnLegParticles(sounds: SoundsAndParticles, world: World, leg: Leg) { 81 | val justBegunMoving = leg.isMoving && leg.timeSinceBeginMove < 1 82 | val wasUnderWater = isInWater(world, leg.previousEndEffector) 83 | val isUnderWater = isInWater(world, leg.endEffector) 84 | val justEnteredWater = isUnderWater && !wasUnderWater 85 | val justExitedWater = !isUnderWater && wasUnderWater 86 | 87 | if (isUnderWater) sounds.wetness[leg] = sounds.maxWetness 88 | else sounds.wetness[leg] = (sounds.wetness[leg] ?: 1) - 1 89 | 90 | // sound 91 | if (sounds.timeSinceLastSound > 20) { 92 | if (justEnteredWater) { 93 | val volume = .3f 94 | val pitch = 1.0f + Random.nextFloat() * 0.1f 95 | world.playSound(leg.endEffector, Sound.ENTITY_PLAYER_SPLASH, volume, pitch) 96 | sounds.timeSinceLastSound = 0 97 | } 98 | else if (justExitedWater) { 99 | val volume = .3f 100 | val pitch = 1f + Random.nextFloat() * 0.1f 101 | world.playSound(leg.endEffector, Sound.AMBIENT_UNDERWATER_EXIT, volume, pitch) 102 | sounds.timeSinceLastSound = 0 103 | } 104 | else if (justBegunMoving && isUnderWater) { 105 | val volume = .3f 106 | val pitch = .7f + Random.nextFloat() * 0.1f 107 | world.playSound(leg.endEffector, Sound.ENTITY_PLAYER_SWIM, volume, pitch) 108 | sounds.timeSinceLastSound = 0 109 | } 110 | } 111 | 112 | 113 | // particles 114 | val wetness = sounds.wetness[leg] ?: 0 115 | for (segment in leg.chain.segments) { 116 | val segmentIsUnderWater = isInWater(world, segment.position) 117 | 118 | val location = segment.position.toLocation(world).add(.0, -.1, .0) 119 | 120 | if (segmentIsUnderWater) { 121 | if (justEnteredWater || justExitedWater) { 122 | val offset = .3 123 | world.spawnParticle(Particle.SPLASH, location, 40, offset, .1, offset) 124 | } else if (leg.isMoving) { 125 | val offset = .1 126 | world.spawnParticle(Particle.BUBBLE, location, 1, offset, .0, offset, .0) 127 | } 128 | } 129 | 130 | if (wetness > 0) { 131 | if (Random.nextInt(0, sounds.maxWetness) > wetness) continue 132 | 133 | val offset = .0 134 | world.spawnParticle(Particle.FALLING_WATER, location, 1, offset, offset, offset, .1) 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/Mountable.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components 2 | 3 | import com.heledron.spideranimation.spider.components.body.SpiderBody 4 | import com.heledron.spideranimation.utilities.* 5 | import com.heledron.spideranimation.utilities.ecs.ECS 6 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 7 | import com.heledron.spideranimation.utilities.events.addEventListener 8 | import com.heledron.spideranimation.utilities.events.onInteractEntity 9 | import com.heledron.spideranimation.utilities.maths.rotate 10 | import com.heledron.spideranimation.utilities.overloads.direction 11 | import com.heledron.spideranimation.utilities.overloads.playSound 12 | import com.heledron.spideranimation.utilities.overloads.position 13 | import com.heledron.spideranimation.utilities.overloads.yawRadians 14 | import com.heledron.spideranimation.utilities.rendering.RenderEntity 15 | import org.bukkit.Material 16 | import org.bukkit.Sound 17 | import org.bukkit.entity.ArmorStand 18 | import org.bukkit.entity.Pig 19 | import org.bukkit.entity.Player 20 | import org.bukkit.event.EventHandler 21 | import org.bukkit.event.Listener 22 | import org.bukkit.event.vehicle.VehicleEnterEvent 23 | import org.bukkit.inventory.EquipmentSlot 24 | import org.bukkit.util.Vector 25 | import org.joml.Quaternionf 26 | 27 | class Mountable { 28 | var currentMarker: ArmorStand? = null 29 | var currentPig: Pig? = null 30 | fun getRider() = currentMarker?.passengers?.firstOrNull() as? Player 31 | } 32 | 33 | fun setupMountable(app: ECS) { 34 | onInteractEntity { player, entity, hand -> 35 | for (mountable in app.query()) { 36 | val currentPig = mountable.currentPig ?: continue 37 | if (entity != currentPig) continue 38 | if (hand != EquipmentSlot.HAND) continue 39 | 40 | // if right click with saddle, add saddle (automatic) 41 | if (player.inventory.itemInMainHand.type == Material.SADDLE && !currentPig.hasSaddle()) { 42 | currentPig.world.playSound(currentPig.position, Sound.ENTITY_PIG_SADDLE, 1.0f, 1.0f) 43 | } 44 | 45 | // if right click with empty hand, remove saddle 46 | if (player.inventory.itemInMainHand.type.isAir && mountable.getRider() == null) { 47 | if (player.isSneaking) { 48 | currentPig.setSaddle(false) 49 | } 50 | } 51 | } 52 | } 53 | 54 | // when player mounts the pig, switch them to the marker entity 55 | addEventListener(object : Listener { 56 | @EventHandler 57 | fun onMount(event: VehicleEnterEvent) { 58 | for (mountable in app.query()) { 59 | val currentPig = mountable.currentPig ?: continue 60 | val currentMarker = mountable.currentMarker ?: continue 61 | 62 | if (event.vehicle != currentPig) continue 63 | val player = event.entered 64 | 65 | event.isCancelled = true 66 | currentMarker.addPassenger(player) 67 | } 68 | } 69 | }) 70 | 71 | // Handle user input 72 | @Suppress("UnstableApiUsage") 73 | app.onTick { 74 | for ((mountable, _, entity) in app.query()) { 75 | val player = mountable.getRider() ?: continue 76 | 77 | val input = Vector() 78 | if (player.currentInput.isLeft) input.x += 1.0 79 | if (player.currentInput.isRight) input.x -= 1.0 80 | if (player.currentInput.isForward) input.z += 1.0 81 | if (player.currentInput.isBackward) input.z -= 1.0 82 | 83 | val rotation = Quaternionf().rotationYXZ(player.yawRadians(), .0f, .0f) 84 | val direction = if (input.isZero) input else input.rotate(rotation).normalize() 85 | 86 | val behaviour = DirectionBehaviour(player.direction, direction) 87 | entity.replaceComponent(behaviour) 88 | } 89 | } 90 | 91 | // Render pig and marker 92 | app.onRender { 93 | for ((spider, mountable) in app.query()) { 94 | val location = spider.location().add(spider.velocity) 95 | 96 | val pigLocation = location.clone().add(Vector(.0, -.6, .0)) 97 | val markerLocation = location.clone().add(Vector(.0, .3, .0)) 98 | 99 | RenderEntity( 100 | clazz = Pig::class.java, 101 | location = pigLocation, 102 | init = { 103 | it.setGravity(false) 104 | it.setAI(false) 105 | it.isInvisible = true 106 | it.isInvulnerable = true 107 | it.isSilent = true 108 | it.isCollidable = false 109 | }, 110 | update = { 111 | mountable.currentPig = it 112 | } 113 | ).submit(spider to "mountable.pig") 114 | 115 | RenderEntity( 116 | clazz = ArmorStand::class.java, 117 | location = markerLocation, 118 | init = { 119 | it.setGravity(false) 120 | it.isInvisible = true 121 | it.isInvulnerable = true 122 | it.isSilent = true 123 | it.isCollidable = false 124 | it.isMarker = true 125 | }, 126 | update = update@{ 127 | mountable.currentMarker = it 128 | if (mountable.getRider() == null) return@update 129 | 130 | // This is the only way to preserve passengers when teleporting. 131 | // Paper has a TeleportFlag, but it is not supported by Spigot. 132 | // https://jd.papermc.io/paper/1.21/io/papermc/paper/entity/TeleportFlag.EntityState.html 133 | runCommandSilently("execute as ${it.uniqueId} at @s run tp ${markerLocation.x} ${markerLocation.y} ${markerLocation.z}") 134 | } 135 | ).submit(spider to "mountable.marker") 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/utilities/maths/maths.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.utilities.maths 2 | 3 | import org.bukkit.Location 4 | import org.bukkit.util.Transformation 5 | import org.bukkit.util.Vector 6 | import org.joml.* 7 | import java.lang.Math 8 | import kotlin.math.abs 9 | import kotlin.math.atan2 10 | import kotlin.math.sign 11 | import kotlin.math.sqrt 12 | 13 | val DOWN_VECTOR; get () = Vector(0, -1, 0) 14 | val UP_VECTOR; get () = Vector(0, 1, 0) 15 | val FORWARD_VECTOR; get () = Vector(0, 0, 1) 16 | val BACKWARD_VECTOR; get () = Vector(0, 0, -1) 17 | val LEFT_VECTOR; get () = Vector(-1, 0, 0) 18 | val RIGHT_VECTOR; get () = Vector(1, 0, 0) 19 | 20 | fun Vector.toVector4f() = Vector4f(x.toFloat(), y.toFloat(), z.toFloat(), 1f) 21 | fun Vector3f.toVector4f() = Vector4f(x, y, z, 1f) 22 | fun Vector4f.toVector3f() = Vector3f(x, y, z) 23 | 24 | fun Vector3f.toVector() = Vector(x.toDouble(), y.toDouble(), z.toDouble()) 25 | fun Vector3d.toVector() = Vector(x.toFloat(), y.toFloat(), z.toFloat()) 26 | fun Vector4f.toVector() = Vector(x.toDouble(), y.toDouble(), z.toDouble()) 27 | 28 | fun Vector.copy(vector: Vector3d): Vector { 29 | this.x = vector.x 30 | this.y = vector.y 31 | this.z = vector.z 32 | return this 33 | } 34 | 35 | fun Vector.copy(vector: Vector3f): Vector { 36 | this.x = vector.x.toDouble() 37 | this.y = vector.y.toDouble() 38 | this.z = vector.z.toDouble() 39 | return this 40 | } 41 | 42 | fun Vector.pitch(): Float { 43 | return -atan2(y, sqrt(x * x + z * z)).toFloat() 44 | } 45 | 46 | fun Vector.yaw(): Float { 47 | return -atan2(-x, z).toFloat() 48 | } 49 | 50 | fun Vector.rotate(quaternion: Quaterniond) = copy(Vector3d(x, y, z).rotate(quaternion)) 51 | 52 | fun Vector.rotate(quaternion: Quaternionf) = copy(Vector3d(x, y, z).rotate(Quaterniond(quaternion))) 53 | 54 | fun Vector.lerp(other: Vector, t: Double): Vector { 55 | this.x = x + (other.x - x) * t 56 | this.y = y + (other.y - y) * t 57 | this.z = z + (other.z - z) * t 58 | return this 59 | } 60 | 61 | fun Vector.moveTowards(target: Vector, speed: Double): Vector { 62 | val diff = target.clone().subtract(this) 63 | val distance = diff.length() 64 | if (distance <= speed) { 65 | this.copy(target) 66 | } else { 67 | this.add(diff.multiply(speed / distance)) 68 | } 69 | return this 70 | } 71 | 72 | fun Vector3f.moveTowards(target: Vector3f, speed: Float): Vector3f { 73 | val diff = Vector3f(target).sub(this) 74 | val distance = diff.length() 75 | if (distance <= speed) { 76 | this.set(target) 77 | } else { 78 | this.add(diff.mul(speed / distance)) 79 | } 80 | return this 81 | } 82 | 83 | fun Location.yawRadians() = -yaw.toRadians() 84 | fun Location.pitchRadians() = pitch.toRadians() 85 | 86 | fun Location.getQuaternion(): Quaternionf { 87 | return Quaternionf().rotateYXZ(yawRadians(), pitchRadians(), 0f) 88 | } 89 | 90 | fun Quaterniond.transform(vector: Vector): Vector { 91 | vector.copy(this.transform(vector.toVector3d())) 92 | return vector 93 | } 94 | 95 | fun Double.lerp(other: Double, t: Double): Double { 96 | return this * (1 - t) + other * t 97 | } 98 | 99 | fun Float.lerp(other: Float, t: Float): Float { 100 | return this * (1 - t) + other * t 101 | } 102 | 103 | fun Int.lerpSafely(other: Int, t: Float): Int { 104 | if (other == this) return this 105 | val result = this.toFloat().lerp(other.toFloat(), t).toInt() 106 | if (result == this && t != 0f) return this.moveTowards(other, 1) 107 | return result 108 | } 109 | 110 | fun Double.moveTowards(target: Double, speed: Double): Double { 111 | val distance = target - this 112 | return if (abs(distance) < speed) target else this + speed * distance.sign 113 | } 114 | 115 | fun Float.moveTowards(target: Float, speed: Float): Float { 116 | val distance = target - this 117 | return if (abs(distance) < speed) target else this + speed * distance.sign 118 | } 119 | 120 | fun Int.moveTowards(target: Int, speed: Int): Int { 121 | val distance = target - this 122 | return if (abs(distance) < speed) target else this + speed * distance.sign 123 | } 124 | 125 | fun Double.toRadians(): Double { 126 | return Math.toRadians(this) 127 | } 128 | 129 | fun Float.toRadians(): Float { 130 | return Math.toRadians(this.toDouble()).toFloat() 131 | } 132 | 133 | fun Double.toDegrees(): Double { 134 | return Math.toDegrees(this) 135 | } 136 | 137 | fun Float.toDegrees(): Float { 138 | return Math.toDegrees(this.toDouble()).toFloat() 139 | } 140 | 141 | 142 | fun Double.normalize(min: Double, max: Double): Double { 143 | return (this - min) / (max - min) 144 | } 145 | 146 | fun Double.denormalize(min: Double, max: Double): Double { 147 | return this * (max - min) + min 148 | } 149 | 150 | fun Float.normalize(min: Float, max: Float): Float { 151 | return (this - min) / (max - min) 152 | } 153 | 154 | fun Float.denormalize(min: Float, max: Float): Float { 155 | return this * (max - min) + min 156 | } 157 | 158 | fun Transformation.normal(): Vector3f { 159 | val rotation = Quaternionf(leftRotation).mul(rightRotation) 160 | val forward = Vector3f(0f, 0f, 1f).rotate(rotation) 161 | return forward.normalize() 162 | } 163 | 164 | fun shearMatrix( 165 | xy: Float, 166 | xz: Float, 167 | yx: Float, 168 | yz: Float, 169 | zx: Float, 170 | zy: Float, 171 | ): Matrix4f { 172 | return Matrix4f( 173 | 1f, xy, xz, 0f, 174 | yx, 1f, yz, 0f, 175 | zx, zy, 1f, 0f, 176 | 0f, 0f, 0f, 1f 177 | ) 178 | } 179 | 180 | fun Matrix4f.shear( 181 | xy: Float = 0f, 182 | xz: Float = 0f, 183 | yx: Float = 0f, 184 | yz: Float = 0f, 185 | zx: Float = 0f, 186 | zy: Float = 0f, 187 | ): Matrix4f = this.mul(shearMatrix( 188 | xy = xy, 189 | xz = xz, 190 | yx = yx, 191 | yz = yz, 192 | zx = zx, 193 | zy = zy, 194 | )) 195 | 196 | fun Float.eased(): Float { 197 | return this * this * (3 - 2 * this) 198 | } 199 | 200 | 201 | fun List.average(): Vector { 202 | val out = Vector(0.0, 0.0, 0.0) 203 | for (vector in this) out.add(vector) 204 | return out.multiply(1.0 / this.size) 205 | } 206 | 207 | fun List.average(): Vector3f { 208 | val out = Vector3f() 209 | for (vector in this) out.add(vector) 210 | return out.mul(1f / this.size) 211 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/Cloak.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components 2 | 3 | import com.heledron.spideranimation.spider.components.body.SpiderBody 4 | import com.heledron.spideranimation.spider.configuration.CloakOptions 5 | import com.heledron.spideranimation.utilities.ecs.ECS 6 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 7 | import com.heledron.spideranimation.utilities.block_colors.findBlockWithColor 8 | import com.heledron.spideranimation.utilities.block_colors.getBlockColor 9 | import com.heledron.spideranimation.utilities.colors.Oklab 10 | import com.heledron.spideranimation.utilities.colors.distanceTo 11 | import com.heledron.spideranimation.utilities.colors.toOklab 12 | import com.heledron.spideranimation.utilities.raycastGround 13 | import com.heledron.spideranimation.utilities.events.SeriesScheduler 14 | import com.heledron.spideranimation.utilities.events.runLater 15 | import com.heledron.spideranimation.utilities.maths.DOWN_VECTOR 16 | import com.heledron.spideranimation.utilities.overloads.eyePosition 17 | import org.bukkit.* 18 | import org.bukkit.block.data.BlockData 19 | import org.bukkit.entity.Display 20 | import org.bukkit.util.RayTraceResult 21 | import org.bukkit.util.Vector 22 | import java.util.WeakHashMap 23 | 24 | class CloakDamageEvent(val entity: ECSEntity, val spider: SpiderBody, val cloak: Cloak) 25 | 26 | class CloakToggleEvent(val entity: ECSEntity, val spider: SpiderBody) 27 | 28 | class Cloak(var options: CloakOptions) { 29 | var active = false 30 | private var cloakColor = WeakHashMap() 31 | private var cloakOverride = WeakHashMap() 32 | private var cloakGlitching = false 33 | 34 | fun toggleCloak(app: ECS, entity: ECSEntity) { 35 | val spider = entity.query() ?: return 36 | active = !active 37 | app.emit(CloakToggleEvent(entity = entity, spider = spider)) 38 | } 39 | 40 | fun getPiece(id: Any, world: World, position: Vector, originalBlock: BlockData, originalBrightness: Display.Brightness?): Pair { 41 | applyCloak(id, world, position, originalBlock, originalBrightness?.skyLight ?: 15) 42 | 43 | val override = cloakOverride[id] 44 | if (override != null) return override to Display.Brightness(0, 15) 45 | 46 | val cloakColor = cloakColor[id] ?: return originalBlock to originalBrightness 47 | val match = findBlockWithColor(cloakColor.toRGB(), options.allowCustomBrightness) 48 | return match.block to Display.Brightness(0, match.brightness) 49 | } 50 | 51 | private fun applyCloak(id: Any, world: World, position: Vector, originalBlock: BlockData, originalBrightness: Int) { 52 | if (cloakGlitching) return 53 | 54 | fun groundCast(): RayTraceResult? { 55 | return world.raycastGround(position, DOWN_VECTOR, 5.0) 56 | } 57 | 58 | fun cast(): RayTraceResult? { 59 | val targetPlayer = Bukkit.getOnlinePlayers().firstOrNull() ?: return groundCast() 60 | 61 | val direction = position.clone().subtract(targetPlayer.eyePosition) 62 | return world.raycastGround(position, direction, 30.0) 63 | } 64 | 65 | val originalColor = getBlockColor(originalBlock, originalBrightness)?.toOklab() ?: return 66 | val currentColor = cloakColor[id] ?: originalColor 67 | 68 | val targetColor = run getTargetColor@{ 69 | if (!active) return@getTargetColor originalColor 70 | 71 | val rayTrace = cast() ?: return@getTargetColor currentColor 72 | val block = rayTrace.hitBlock?.blockData ?: return@getTargetColor currentColor 73 | val lightLevel = 15 74 | getBlockColor(block, lightLevel)?.toOklab() ?: currentColor 75 | } 76 | 77 | 78 | val lerpAmount = options.lerpSpeed.toFloat() + (Math.random().toFloat() - 0.5f) * options.lerpRandomness.toFloat() 79 | val newColor = currentColor 80 | .lerp(targetColor, lerpAmount.coerceIn(0f, 1f)) 81 | .moveTowards(targetColor, options.moveSpeed.toFloat()) 82 | 83 | if (newColor == originalColor) cloakColor.remove(id) 84 | else cloakColor[id] = newColor 85 | } 86 | 87 | 88 | fun breakCloak() { 89 | cloakGlitching = true 90 | 91 | val originalColors = cloakColor.values.toList() 92 | 93 | val glitch = listOf( 94 | { id: Any -> cloakOverride[id] = Material.LIGHT_BLUE_GLAZED_TERRACOTTA.createBlockData() }, 95 | { id: Any -> cloakOverride[id] = Material.CYAN_GLAZED_TERRACOTTA.createBlockData() }, 96 | { id: Any -> cloakOverride[id] = Material.WHITE_GLAZED_TERRACOTTA.createBlockData() }, 97 | { id: Any -> cloakOverride[id] = Material.GRAY_GLAZED_TERRACOTTA.createBlockData() }, 98 | 99 | { id: Any -> cloakOverride[id] = null }, 100 | { id: Any -> cloakColor[id] = originalColors.random() }, 101 | ) 102 | 103 | var maxTime = 0 104 | 105 | for ((id) in cloakColor) { 106 | val scheduler = SeriesScheduler() 107 | 108 | fun randomSleep(min: Int, max: Int) { 109 | scheduler.sleep((min + Math.random() * (max - min)).toLong()) 110 | } 111 | 112 | randomSleep(0, 3) 113 | for (i in 0 until (Math.random() * 4).toInt()) { 114 | scheduler.run { glitch.random()(id) } 115 | scheduler.sleep(2L) 116 | } 117 | 118 | scheduler.run { 119 | cloakColor[id] = null 120 | cloakOverride[id] = null 121 | } 122 | 123 | if (Math.random() < 1.0 / 6) continue 124 | 125 | randomSleep(0, 3) 126 | 127 | for (i in 0 until (Math.random() * 3).toInt()) { 128 | scheduler.run { 129 | cloakOverride[id] = findBlockWithColor(originalColors.random().toRGB(), options.allowCustomBrightness).block 130 | } 131 | 132 | randomSleep(5, 15) 133 | 134 | scheduler.run { 135 | cloakOverride[id] = null 136 | } 137 | scheduler.sleep(2L) 138 | } 139 | 140 | if (scheduler.time > maxTime) maxTime = scheduler.time.toInt() 141 | } 142 | 143 | runLater(maxTime.toLong()) { 144 | cloakGlitching = false 145 | } 146 | } 147 | } 148 | 149 | private fun Oklab.moveTowards(target: Oklab, maxDelta: Float): Oklab { 150 | val delta = this.distanceTo(target) 151 | if (delta <= maxDelta) return target 152 | val t = maxDelta / delta 153 | return this.lerp(target, t) 154 | } 155 | 156 | fun setupCloak(app: ECS) { 157 | app.onEvent { event -> 158 | val cloak = event.entity.query() ?: return@onEvent 159 | if (cloak.active) { 160 | app.emit(CloakDamageEvent(entity = event.entity, spider = event.spider, cloak = cloak)) 161 | cloak.breakCloak() 162 | } 163 | cloak.active = false 164 | } 165 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/setupItems.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation 2 | 3 | import com.heledron.spideranimation.AppState.ecs 4 | import com.heledron.spideranimation.kinematic_chain_visualizer.KinematicChainVisualizer 5 | import com.heledron.spideranimation.spider.components.body.SpiderBody 6 | import com.heledron.spideranimation.spider.components.Cloak 7 | import com.heledron.spideranimation.spider.components.PointDetector 8 | import com.heledron.spideranimation.spider.components.rendering.SpiderRenderer 9 | import com.heledron.spideranimation.laser.LaserPoint 10 | import com.heledron.spideranimation.utilities.custom_items.CustomItemComponent 11 | import com.heledron.spideranimation.utilities.custom_items.attach 12 | import com.heledron.spideranimation.utilities.custom_items.createNamedItem 13 | import com.heledron.spideranimation.utilities.custom_items.customItemRegistry 14 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 15 | import com.heledron.spideranimation.utilities.raycastGround 16 | import com.heledron.spideranimation.utilities.events.onTick 17 | import com.heledron.spideranimation.utilities.overloads.direction 18 | import com.heledron.spideranimation.utilities.overloads.eyePosition 19 | import com.heledron.spideranimation.utilities.overloads.playSound 20 | import com.heledron.spideranimation.utilities.overloads.position 21 | import com.heledron.spideranimation.utilities.overloads.sendActionBar 22 | import com.heledron.spideranimation.utilities.overloads.yaw 23 | import org.bukkit.Material 24 | import org.bukkit.Sound 25 | import org.bukkit.entity.Player 26 | import org.bukkit.util.Vector 27 | import kotlin.math.roundToInt 28 | 29 | 30 | fun setupItems() { 31 | val spiderComponent = CustomItemComponent("spider") 32 | customItemRegistry += createNamedItem(Material.NETHERITE_INGOT, "Spider").attach(spiderComponent) 33 | spiderComponent.onGestureUse { player, _ -> 34 | val spiderEntity = AppState.ecs.query().firstOrNull()?.first 35 | if (spiderEntity == null) { 36 | val yawIncrements = 45.0f 37 | val yaw = (player.yaw / yawIncrements).roundToInt() * yawIncrements 38 | 39 | val hitPosition = player.world.raycastGround(player.eyePosition, player.direction, 100.0)?.hitPosition ?: return@onGestureUse 40 | 41 | player.world.playSound(hitPosition, Sound.BLOCK_NETHERITE_BLOCK_PLACE, 1.0f, 1.0f) 42 | AppState.createSpider(hitPosition.toLocation(player.world).apply { this.yaw = yaw }) 43 | player.sendActionBar("Spider created") 44 | } else { 45 | player.world.playSound(player.position, Sound.ENTITY_ITEM_FRAME_REMOVE_ITEM, 1.0f, 0.0f) 46 | spiderEntity.remove() 47 | player.sendActionBar("Spider removed") 48 | } 49 | } 50 | 51 | 52 | val disableLegComponent = CustomItemComponent("disableLeg") 53 | customItemRegistry += createNamedItem(Material.SHEARS, "Toggle Leg").attach(disableLegComponent) 54 | onTick { 55 | val players = disableLegComponent.getPlayersHoldingItem().toSet() 56 | for (pointDetector in AppState.ecs.query()) { 57 | pointDetector.checkPlayers = players 58 | } 59 | } 60 | disableLegComponent.onGestureUse { player, _ -> 61 | for (pointDetector in AppState.ecs.query()) { 62 | val selectedLeg = pointDetector.selectedLeg[player] 63 | if (selectedLeg == null) { 64 | player.world.playSound(player.position, Sound.BLOCK_DISPENSER_FAIL, 1.0f, 2.0f) 65 | return@onGestureUse 66 | } 67 | 68 | selectedLeg.isDisabled = !selectedLeg.isDisabled 69 | player.world.playSound(player.position, Sound.BLOCK_LANTERN_PLACE, 1.0f, 1.0f) 70 | } 71 | } 72 | 73 | val toggleDebugComponent = CustomItemComponent("toggleDebug") 74 | customItemRegistry += createNamedItem(Material.BLAZE_ROD, "Toggle Debug Graphics").attach(toggleDebugComponent) 75 | toggleDebugComponent.onGestureUse { player, _ -> 76 | AppState.renderDebugVisuals = !AppState.renderDebugVisuals 77 | 78 | AppState.ecs.query().forEach { 79 | it.detailed = AppState.renderDebugVisuals 80 | } 81 | 82 | val pitch = if (AppState.renderDebugVisuals) 2.0f else 1.5f 83 | player.world.playSound(player.position, Sound.BLOCK_DISPENSER_FAIL, 1.0f, pitch) 84 | } 85 | 86 | 87 | val switchRendererComponent = CustomItemComponent("switchRenderer") 88 | customItemRegistry += createNamedItem(Material.LIGHT_BLUE_DYE, "Switch Renderer").attach(switchRendererComponent) 89 | switchRendererComponent.onGestureUse { player, _ -> 90 | AppState.ecs.query().forEach { renderer -> 91 | renderer.useParticles = !renderer.useParticles 92 | 93 | if (renderer.useParticles) { 94 | player.world.playSound(player.position, Sound.ENTITY_AXOLOTL_ATTACK, 1.0f, 1.0f) 95 | } else { 96 | player.world.playSound(player.position, Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1.0f, 1.0f) 97 | } 98 | } 99 | } 100 | 101 | val toggleCloakComponent = CustomItemComponent("toggleCloak") 102 | customItemRegistry += createNamedItem(Material.GREEN_DYE, "Toggle Cloak").attach(toggleCloakComponent) 103 | toggleCloakComponent.onGestureUse { _, _ -> 104 | val (cloak, entity) = AppState.ecs.query().firstOrNull() ?: return@onGestureUse 105 | cloak.toggleCloak(AppState.ecs, entity) 106 | } 107 | 108 | val chainVisualizerStep = CustomItemComponent("chainVisualizerStep") 109 | customItemRegistry += createNamedItem(Material.PURPLE_DYE, "Chain Visualizer Step").attach(chainVisualizerStep) 110 | chainVisualizerStep.onGestureUse { player, _ -> 111 | AppState.ecs.query().forEach { 112 | player.world.playSound(player.position, Sound.BLOCK_DISPENSER_FAIL, 1.0f, 2.0f) 113 | it.step() 114 | } 115 | } 116 | 117 | val chainVisualizerStraighten = CustomItemComponent("chainVisualizerStraighten") 118 | customItemRegistry += createNamedItem(Material.MAGENTA_DYE, "Chain Visualizer Straighten").attach(chainVisualizerStraighten) 119 | chainVisualizerStraighten.onGestureUse { player, _ -> 120 | ecs.query().forEach { 121 | player.world.playSound(player.position, Sound.BLOCK_DISPENSER_FAIL, 1.0f, 2.0f) 122 | it.straighten(it.target ?: return@onGestureUse) 123 | } 124 | } 125 | 126 | val switchGaitComponent = CustomItemComponent("switchGait") 127 | customItemRegistry += createNamedItem(Material.BREEZE_ROD, "Switch Gait").attach(switchGaitComponent) 128 | switchGaitComponent.onGestureUse { player, _ -> 129 | player.world.playSound(player.position, Sound.BLOCK_DISPENSER_FAIL, 1.0f, 2.0f) 130 | AppState.gallop = !AppState.gallop 131 | player.sendActionBar(if (!AppState.gallop) "Walk mode" else "Gallop mode") 132 | } 133 | 134 | val laserPointerComponent = CustomItemComponent("laserPointer") 135 | customItemRegistry += createNamedItem(Material.ARROW, "Laser Pointer").attach(laserPointerComponent) 136 | 137 | val comeHereComponent = CustomItemComponent("comeHere") 138 | customItemRegistry += createNamedItem(Material.CARROT_ON_A_STICK, "Come Here").attach(comeHereComponent) 139 | 140 | class LaserPointExpire(val owner: Player) { 141 | var expired = false 142 | } 143 | 144 | ecs.onTick { 145 | // mark laser for removal 146 | ecs.query().forEach { expire -> 147 | expire.expired = true 148 | } 149 | } 150 | 151 | ecs.onTick { 152 | val lasers = ecs.query() 153 | 154 | fun spawnLaser(player: Player, position: Vector, isVisible: Boolean) { 155 | val existing = lasers.find { it.third.owner == player } 156 | 157 | if (existing != null) { 158 | // update existing laser 159 | existing.second.world = player.world 160 | existing.second.position = position 161 | existing.second.isVisible = isVisible 162 | existing.third.expired = false 163 | } else { 164 | // create new laser 165 | ecs.spawn( 166 | LaserPointExpire(player), 167 | LaserPoint( 168 | world = player.world, 169 | position = position, 170 | isVisible = isVisible, 171 | ) 172 | ) 173 | } 174 | } 175 | 176 | // handle laser pointer 177 | for (player in laserPointerComponent.getPlayersHoldingItem()) { 178 | val direction = player.direction 179 | val result = player.world.raycastGround(player.eyePosition, direction, 100.0) 180 | 181 | val hitPosition = result?.hitPosition ?: 182 | player.eyePosition.add(direction.multiply(200)) 183 | 184 | 185 | val isUsingFallback = result == null 186 | val isVisible = !isUsingFallback && AppState.miscOptions.showLaser 187 | 188 | spawnLaser(player, hitPosition, isVisible) 189 | } 190 | 191 | // handle carrot on a stick 192 | for (player in comeHereComponent.getPlayersHoldingItem()) { 193 | spawnLaser(player, player.eyePosition, isVisible = false) 194 | } 195 | } 196 | 197 | ecs.onTick { 198 | ecs.query().forEach { (entity, expire) -> 199 | if (expire.expired) entity.remove() 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/rendering/spiderDebugRenderEntities.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components.rendering 2 | 3 | import com.heledron.spideranimation.utilities.rendering.interpolateTransform 4 | import com.heledron.spideranimation.utilities.rendering.renderBlock 5 | import com.heledron.spideranimation.spider.components.body.SpiderBody 6 | import com.heledron.spideranimation.spider.components.PointDetector 7 | import com.heledron.spideranimation.utilities.* 8 | import com.heledron.spideranimation.utilities.centredTransform 9 | import com.heledron.spideranimation.utilities.maths.FORWARD_VECTOR 10 | import com.heledron.spideranimation.utilities.maths.RIGHT_VECTOR 11 | import com.heledron.spideranimation.utilities.maths.UP_VECTOR 12 | import com.heledron.spideranimation.utilities.rendering.RenderGroup 13 | import org.bukkit.Material 14 | import org.bukkit.World 15 | import org.bukkit.entity.BlockDisplay 16 | import org.bukkit.entity.Display 17 | import org.bukkit.util.Vector 18 | import org.joml.Matrix4f 19 | import org.joml.Quaternionf 20 | import org.joml.Vector3f 21 | 22 | 23 | fun spiderDebugRenderEntities(spider: SpiderBody, pointDetector: PointDetector): RenderGroup { 24 | val group = RenderGroup() 25 | 26 | val scale = spider.bodyPlan.scale.toFloat() 27 | 28 | for ((legIndex, leg) in spider.legs.withIndex()) { 29 | // Render scan bars 30 | if (spider.debug.scanBars) group["scanBar" to legIndex] = renderLine( 31 | world = spider.world, 32 | position = leg.scanStartPosition, 33 | vector = leg.scanVector, 34 | thickness = .05f * scale, 35 | init = { 36 | it.brightness = Display.Brightness(15, 15) 37 | }, 38 | update = { 39 | val material = if (leg.isPrimary) Material.GOLD_BLOCK else Material.IRON_BLOCK 40 | it.block = material.createBlockData() 41 | } 42 | ) 43 | 44 | // Render trigger zone 45 | if (spider.debug.triggerZones) group["triggerZoneVertical" to legIndex] = renderBlock( 46 | world = spider.world, 47 | position = leg.triggerZone.center, 48 | init = { 49 | it.teleportDuration = 1 50 | it.interpolationDuration = 1 51 | it.brightness = Display.Brightness(15, 15) 52 | }, 53 | update = { 54 | val material = if (leg.isUncomfortable) Material.RED_STAINED_GLASS else Material.CYAN_STAINED_GLASS 55 | it.block = material.createBlockData() 56 | 57 | val thickness = .07f * scale 58 | val transform = Matrix4f() 59 | .rotate(spider.gait.scanPivotMode.get(spider)) 60 | .scale(thickness, 2 * leg.triggerZone.vertical.toFloat(), thickness) 61 | .translate(-.5f,-.5f,-.5f) 62 | 63 | it.interpolateTransform(transform) 64 | } 65 | ) 66 | 67 | // Render trigger zone 68 | if (spider.debug.triggerZones) group["triggerZoneHorizontal" to legIndex] = renderBlock( 69 | world = spider.world, 70 | position = run { 71 | val pos = leg.triggerZone.center.clone() 72 | pos.y = leg.target.position.y.coerceIn(pos.y - leg.triggerZone.vertical, pos.y + leg.triggerZone.vertical) 73 | pos 74 | }, 75 | init = { 76 | it.teleportDuration = 1 77 | it.interpolationDuration = 1 78 | it.brightness = Display.Brightness(15, 15) 79 | }, 80 | update = { 81 | val material = if (leg.isUncomfortable) Material.RED_STAINED_GLASS else Material.CYAN_STAINED_GLASS 82 | it.block = material.createBlockData() 83 | 84 | val size = 2 * leg.triggerZone.horizontal.toFloat() 85 | val ySize = 0.02f 86 | val transform = Matrix4f() 87 | .rotate(spider.gait.scanPivotMode.get(spider)) 88 | .scale(size, ySize, size) 89 | .translate(-.5f,-.5f,-.5f) 90 | 91 | it.interpolateTransform(transform) 92 | } 93 | ) 94 | 95 | // Render end effector 96 | if (spider.debug.endEffectors) group["endEffector" to legIndex] = renderBlock( 97 | world = spider.world, 98 | position = leg.endEffector, 99 | init = { 100 | it.teleportDuration = 1 101 | it.brightness = Display.Brightness(15, 15) 102 | }, 103 | update = { 104 | val size = (if (pointDetector.selectedLeg.containsValue(leg)) .2f else .15f) * scale 105 | it.transformation = centredTransform(size, size, size) 106 | it.block = when { 107 | leg.isDisabled -> Material.BLACK_CONCRETE.createBlockData() 108 | leg.isGrounded() -> Material.DIAMOND_BLOCK.createBlockData() 109 | leg.touchingGround -> Material.LAPIS_BLOCK.createBlockData() 110 | else -> Material.REDSTONE_BLOCK.createBlockData() 111 | } 112 | } 113 | ) 114 | 115 | // Render target position 116 | if (spider.debug.targetPositions) group["targetPosition" to legIndex] = renderBlock( 117 | location = leg.target.position.toLocation(spider.world), 118 | init = { 119 | it.teleportDuration = 1 120 | it.brightness = Display.Brightness(15, 15) 121 | 122 | val size = 0.2f * scale 123 | it.transformation = centredTransform(size, size, size) 124 | }, 125 | update = { 126 | val material = if (leg.target.isGrounded) Material.LIME_STAINED_GLASS else Material.RED_STAINED_GLASS 127 | it.block = material.createBlockData() 128 | } 129 | ) 130 | } 131 | 132 | // Render spider direction 133 | if (spider.debug.orientation) group["direction"] = renderBlock( 134 | location = spider.position.toLocation(spider.world), 135 | init = { 136 | it.teleportDuration = 1 137 | it.interpolationDuration = 1 138 | it.brightness = Display.Brightness(15, 15) 139 | }, 140 | update = { 141 | it.block = if (spider.gallop) Material.REDSTONE_BLOCK.createBlockData() else Material.EMERALD_BLOCK.createBlockData() 142 | 143 | val size = .1f * scale 144 | val displacement = 1f * scale 145 | val transform = Matrix4f() 146 | .rotate(spider.orientation) 147 | .translate(FORWARD_VECTOR.toVector3f().mul(displacement)) 148 | .scale(size, size, size) 149 | .translate(-.5f,-.5f, -.5f) 150 | 151 | it.interpolateTransform(transform) 152 | } 153 | ) 154 | 155 | // Render preferred orientation 156 | if (spider.debug.preferredOrientation) { 157 | fun renderEntity(orientation: Quaternionf, direction: Vector, thickness: Float, length: Float, material: Material) = run { 158 | val mTranslation = Vector3f(-1f, -1f, -1f).add(direction.toVector3f()).mul(.5f) 159 | val mScale = Vector3f(thickness, thickness, thickness).add(direction.toVector3f().mul(length)) 160 | 161 | renderBlock( 162 | world = spider.world, 163 | position = spider.position, 164 | init = { 165 | it.block = material.createBlockData() 166 | it.teleportDuration = 1 167 | it.interpolationDuration = 1 168 | }, 169 | update = { 170 | val transform = Matrix4f() 171 | .rotate(orientation) 172 | .scale(mScale) 173 | .translate(mTranslation) 174 | 175 | it.interpolateTransform(transform) 176 | } 177 | ) 178 | } 179 | 180 | val thickness = .025f * scale 181 | group["preferredForwards"] = renderEntity(spider.preferredOrientation, FORWARD_VECTOR, thickness, 2.0f * scale, Material.DIAMOND_BLOCK) 182 | group["preferredRight" ] = renderEntity(spider.preferredOrientation, RIGHT_VECTOR , thickness, 1.0f * scale, Material.DIAMOND_BLOCK) 183 | group["preferredUp" ] = renderEntity(spider.preferredOrientation, UP_VECTOR , thickness, 1.0f * scale, Material.DIAMOND_BLOCK) 184 | } 185 | 186 | 187 | val normal = spider.normal ?: return group 188 | if (spider.debug.legPolygons && normal.contactPolygon != null) { 189 | val points = normal.contactPolygon//.map { it.toLocation(spider.world)} 190 | for (i in points.indices) { 191 | val a = points[i] 192 | val b = points[(i + 1) % points.size] 193 | 194 | group["polygon" to i] = renderLine( 195 | world = spider.world, 196 | position = a, 197 | vector = b.clone().subtract(a), 198 | thickness = .05f * scale, 199 | interpolation = 0, 200 | init = { it.brightness = Display.Brightness(15, 15) }, 201 | update = { it.block = Material.EMERALD_BLOCK.createBlockData() } 202 | ) 203 | } 204 | } 205 | 206 | if (spider.debug.centreOfMass && normal.centreOfMass != null) group["centreOfMass"] = renderBlock( 207 | world = spider.world, 208 | position = normal.centreOfMass, 209 | init = { 210 | it.teleportDuration = 1 211 | it.brightness = Display.Brightness(15, 15) 212 | 213 | val size = 0.1f * scale 214 | it.transformation = centredTransform(size, size, size) 215 | }, 216 | update = { 217 | val material = if (normal.normal.horizontalLength() == .0) Material.LAPIS_BLOCK else Material.REDSTONE_BLOCK 218 | it.block = material.createBlockData() 219 | } 220 | ) 221 | 222 | 223 | if (spider.debug.normalForce && normal.centreOfMass != null && normal.origin !== null) group["acceleration"] = renderLine( 224 | world = spider.world, 225 | position = normal.origin, 226 | vector = normal.centreOfMass.clone().subtract(normal.origin), 227 | thickness = .02f * scale, 228 | interpolation = 1, 229 | init = { it.brightness = Display.Brightness(15, 15) }, 230 | update = { 231 | val material = if (spider.normalAcceleration.isZero) Material.BLACK_CONCRETE else Material.WHITE_CONCRETE 232 | it.block = material.createBlockData() 233 | } 234 | ) 235 | 236 | return group 237 | } 238 | 239 | fun renderLine( 240 | world: World, 241 | position: Vector, 242 | vector: Vector, 243 | upVector: Vector = if (vector.x + vector.z != 0.0) UP_VECTOR else FORWARD_VECTOR, 244 | thickness: Float = .1f, 245 | interpolation: Int = 1, 246 | init: (BlockDisplay) -> Unit = {}, 247 | update: (BlockDisplay) -> Unit = {} 248 | ) = renderBlock( 249 | world = world, 250 | position = position, 251 | init = { 252 | it.teleportDuration = interpolation 253 | it.interpolationDuration = interpolation 254 | init(it) 255 | }, 256 | update = { 257 | val matrix = Matrix4f().rotateTowards(vector.toVector3f(), upVector.toVector3f()) 258 | .translate(-thickness / 2, -thickness / 2, 0f) 259 | .scale(thickness, thickness, vector.length().toFloat()) 260 | 261 | it.interpolateTransform(matrix) 262 | update(it) 263 | } 264 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/body/Leg.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components.body 2 | 3 | import com.heledron.spideranimation.utilities.ChainSegment 4 | import com.heledron.spideranimation.utilities.KinematicChain 5 | import com.heledron.spideranimation.spider.configuration.LegPlan 6 | import com.heledron.spideranimation.utilities.* 7 | import com.heledron.spideranimation.utilities.ecs.ECS 8 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 9 | import com.heledron.spideranimation.utilities.isOnGround 10 | import com.heledron.spideranimation.utilities.raycastGround 11 | import com.heledron.spideranimation.utilities.resolveCollision 12 | import com.heledron.spideranimation.utilities.maths.DOWN_VECTOR 13 | import com.heledron.spideranimation.utilities.maths.UP_VECTOR 14 | import com.heledron.spideranimation.utilities.maths.lerp 15 | import com.heledron.spideranimation.utilities.maths.moveTowards 16 | import com.heledron.spideranimation.utilities.maths.rotate 17 | import org.bukkit.util.Vector 18 | import org.joml.Quaterniond 19 | import org.joml.Quaternionf 20 | import kotlin.math.ceil 21 | import kotlin.math.floor 22 | 23 | 24 | class LegStepEvent(val entity: ECSEntity, val spider: SpiderBody, val leg: Leg) 25 | 26 | class Leg( 27 | val ecs: ECS, 28 | val entity: ECSEntity, 29 | val spider: SpiderBody, 30 | var legPlan: LegPlan 31 | ) { 32 | // memo 33 | lateinit var triggerZone: SplitDistanceZone; private set 34 | lateinit var comfortZone: SplitDistanceZone; private set 35 | 36 | var groundPosition: Vector? = null; private set 37 | lateinit var restPosition: Vector; private set 38 | lateinit var lookAheadPosition: Vector; private set 39 | lateinit var scanStartPosition: Vector; private set 40 | lateinit var scanVector: Vector; private set 41 | 42 | lateinit var attachmentPosition: Vector; private set 43 | 44 | init { 45 | updateMemo() 46 | } 47 | 48 | // state 49 | var target = locateGround() ?: strandedTarget() 50 | var endEffector = target.position.clone() 51 | var previousEndEffector = endEffector.clone() 52 | var chain = KinematicChain(Vector(0, 0, 0), listOf()) 53 | 54 | var touchingGround = true; private set 55 | var isMoving = false; private set 56 | var timeSinceBeginMove = 0; private set 57 | var timeSinceStopMove = 0; private set 58 | 59 | var isDisabled = false 60 | var isPrimary = false 61 | var canMove = false 62 | 63 | // utils 64 | val isOutsideTriggerZone: Boolean; get () { return !triggerZone.contains(endEffector) } 65 | val isUncomfortable: Boolean; get () { return !comfortZone.contains(endEffector) } 66 | 67 | fun isGrounded(): Boolean { 68 | return touchingGround && !isMoving && !isDisabled 69 | } 70 | 71 | fun updateMemo() { 72 | val lerpedGait = spider.lerpedGait() 73 | val orientation = spider.gait.scanPivotMode.get(spider) 74 | 75 | val upVector = UP_VECTOR.rotate(Quaterniond(orientation)) 76 | val scanStartAxis = upVector.clone().multiply(lerpedGait.bodyHeight * 1.6) 77 | val scanAxis = upVector.clone().multiply(-lerpedGait.bodyHeight * 3.5) 78 | 79 | // rest position 80 | restPosition = legPlan.restPosition.clone() 81 | restPosition.add(upVector.clone().multiply(-lerpedGait.bodyHeight)) 82 | restPosition.rotate(orientation).add(spider.position) 83 | 84 | // trigger zone 85 | triggerZone = SplitDistanceZone(restPosition, lerpedGait.triggerZone) 86 | 87 | // comfort zone 88 | // we want the comfort zone to extend above the spider's body 89 | // and below the rest position 90 | val comfortZoneCenter = restPosition.clone() 91 | comfortZoneCenter.y = restPosition.y.lerp(spider.position.y, .5) 92 | val comfortZoneSize = SplitDistance( 93 | horizontal = spider.gait.comfortZone.horizontal, 94 | vertical = spider.gait.comfortZone.vertical + (spider.position.y - restPosition.y).coerceAtLeast(.0) 95 | ) 96 | comfortZone = SplitDistanceZone(comfortZoneCenter, comfortZoneSize) 97 | 98 | // lookahead 99 | lookAheadPosition = lookAheadPosition(restPosition, triggerZone.size.horizontal) 100 | 101 | // scan 102 | scanStartPosition = lookAheadPosition.clone().add(scanStartAxis) 103 | scanVector = scanAxis 104 | 105 | // attachment position 106 | attachmentPosition = legPlan.attachmentPosition.clone().rotate(spider.orientation).add(spider.position) 107 | } 108 | 109 | fun update() { 110 | legPlan = spider.bodyPlan.legs.getOrNull(spider.legs.indexOf(this)) ?: legPlan 111 | updateMovement() 112 | chain = chain() 113 | } 114 | 115 | private fun updateMovement() { 116 | previousEndEffector = endEffector.clone() 117 | 118 | val gait = spider.gait 119 | var didStep = false 120 | 121 | timeSinceBeginMove += 1 122 | timeSinceStopMove += 1 123 | 124 | // update target 125 | val ground = locateGround() 126 | groundPosition = locateGround()?.position 127 | 128 | if (isDisabled) { 129 | target = disabledTarget() 130 | } else { 131 | if (ground != null) target = ground 132 | 133 | if (!target.isGrounded || !comfortZone.contains(target.position)) { 134 | target = strandedTarget() 135 | } 136 | } 137 | 138 | // inherit parent velocity 139 | if (!isGrounded()) { 140 | endEffector.add(spider.velocity) 141 | endEffector.rotateAroundY(spider.rotationalVelocity.y.toDouble(), spider.position) 142 | } 143 | 144 | // resolve ground collision 145 | if (!touchingGround) { 146 | val collision = spider.world.resolveCollision(endEffector, DOWN_VECTOR) 147 | if (collision != null) { 148 | didStep = true 149 | touchingGround = true 150 | endEffector.y = collision.position.y 151 | } 152 | } 153 | 154 | if (isMoving) { 155 | val legMoveSpeed = gait.legMoveSpeed 156 | 157 | endEffector.moveTowards(target.position, legMoveSpeed) 158 | 159 | val targetY = target.position.y + gait.legLiftHeight 160 | val hDistance = endEffector.horizontalDistance(target.position) 161 | if (hDistance > gait.legDropDistance) { 162 | endEffector.y = endEffector.y.moveTowards(targetY, legMoveSpeed) 163 | } 164 | 165 | if (endEffector.distance(target.position) < 0.0001) { 166 | isMoving = false 167 | 168 | touchingGround = touchingGround() 169 | didStep = touchingGround 170 | } 171 | 172 | } else { 173 | canMove = spider.gait.type.canMoveLeg(this) 174 | 175 | if (canMove) { 176 | isMoving = true 177 | timeSinceBeginMove = 0 178 | } 179 | } 180 | 181 | if (didStep) ecs.emit(LegStepEvent(entity = entity, spider = spider, leg = this)) 182 | } 183 | 184 | private fun chain(): KinematicChain { 185 | if (chain.segments.size != legPlan.segments.size) { 186 | var stride = 0.0 187 | chain = KinematicChain(attachmentPosition, legPlan.segments.map { 188 | stride += it.length 189 | val position = spider.position.clone().add(legPlan.restPosition.clone().normalize().multiply(stride)) 190 | ChainSegment(position, it.length, it.initDirection) 191 | }) 192 | } 193 | 194 | chain.root.copy(attachmentPosition) 195 | 196 | if (spider.gait.straightenLegs) { 197 | val pivot = Quaternionf(spider.gait.legChainPivotMode.get(spider)) 198 | 199 | val direction = endEffector.clone().subtract(attachmentPosition) 200 | val rotation = direction.getRotationAroundAxis(pivot) 201 | 202 | rotation.x += spider.gait.legStraightenRotation 203 | val orientation = pivot.rotateYXZ(rotation.y, rotation.x, .0f) 204 | 205 | chain.straightenDirection(orientation) 206 | } 207 | 208 | if (!spider.debug.disableFabrik) { 209 | chain.fabrik(endEffector) 210 | 211 | // the spider might be falling while the leg is still grounded 212 | // if (endEffector.distance(chain.getEndEffector()) > .3) { 213 | // endEffector.copy(chain.getEndEffector()) 214 | // 215 | // if (!isMoving) { 216 | // println("Updated end effector") 217 | // isMoving = true 218 | //// timeSinceBeginMove = 0 219 | // } 220 | // 221 | // } 222 | } 223 | 224 | return chain 225 | } 226 | 227 | private fun touchingGround(): Boolean { 228 | return spider.world.isOnGround(endEffector, DOWN_VECTOR.rotate(spider.orientation)) 229 | } 230 | 231 | private fun lookAheadPosition(restPosition: Vector, triggerZoneRadius: Double): Vector { 232 | if (!spider.isWalking) return restPosition 233 | 234 | val direction = if (spider.velocity.isZero) spider.forwardDirection() else spider.velocity.clone().normalize() 235 | 236 | val lookAhead = direction.multiply(triggerZoneRadius * spider.gait.legLookAheadFraction).add(restPosition) 237 | lookAhead.rotateAroundY(spider.rotationalVelocity.y.toDouble(), spider.position) 238 | return lookAhead 239 | } 240 | 241 | private fun locateGround(): LegTarget? { 242 | val lookAhead = lookAheadPosition.toLocation(spider.world) 243 | val scanLength = scanVector.length() 244 | 245 | fun candidateAllowed(id: Int): Boolean { 246 | return true 247 | } 248 | 249 | var id = 0 250 | val world = spider.world 251 | fun rayCast(x: Double, z: Double): LegTarget? { 252 | id += 1 253 | 254 | if (!candidateAllowed(id)) return null 255 | 256 | val start = Vector(x, scanStartPosition.y, z) 257 | val hit = world.raycastGround(start, scanVector, scanLength) ?: return null 258 | 259 | return LegTarget(position = hit.hitPosition, isGrounded = true, id = id) 260 | } 261 | 262 | val x = scanStartPosition.x 263 | val z = scanStartPosition.z 264 | 265 | val mainCandidate = rayCast(x, z) 266 | 267 | if (!spider.gait.legScanAlternativeGround) return mainCandidate 268 | 269 | if (mainCandidate != null) { 270 | if (mainCandidate.position.y in lookAhead.y - .24 .. lookAhead.y + 1.5) { 271 | return mainCandidate 272 | } 273 | } 274 | 275 | val margin = 2 / 16.0 276 | val nx = floor(x) - margin 277 | val nz = floor(z) - margin 278 | val pz = ceil(z) + margin 279 | val px = ceil(x) + margin 280 | 281 | val candidates = listOf( 282 | rayCast(nx, nz), rayCast(nx, z), rayCast(nx, pz), 283 | rayCast(x, nz), mainCandidate, rayCast(x, pz), 284 | rayCast(px, nz), rayCast(px, z), rayCast(px, pz), 285 | ) 286 | 287 | val preferredPosition = lookAhead.toVector() 288 | 289 | val frontBlock = lookAhead.clone().add(spider.forwardDirection().clone().multiply(1)).block 290 | if (!frontBlock.isPassable) preferredPosition.y += spider.gait.legScanHeightBias 291 | 292 | val best = candidates 293 | .filterNotNull() 294 | .minByOrNull { it.position.distanceSquared(preferredPosition) } 295 | 296 | if (best != null && !comfortZone.contains(best.position)) { 297 | return null 298 | } 299 | 300 | return best 301 | } 302 | 303 | private fun strandedTarget(): LegTarget { 304 | return LegTarget(position = lookAheadPosition.clone(), isGrounded = false, id = -1) 305 | } 306 | 307 | private fun disabledTarget(): LegTarget { 308 | val lerpedGait = spider.lerpedGait() 309 | val upVector = UP_VECTOR.rotate(spider.orientation) 310 | 311 | val target = strandedTarget() 312 | target.position.add(upVector.clone().multiply(lerpedGait.bodyHeight * .5)) 313 | 314 | val minY = (groundPosition?.y ?: -Double.MAX_VALUE) + lerpedGait.bodyHeight * .1 315 | target.position.y = target.position.y.coerceAtLeast(minY) 316 | 317 | return target 318 | } 319 | } 320 | 321 | class LegTarget( 322 | val position: Vector, 323 | val isGrounded: Boolean, 324 | val id: Int, 325 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/kinematic_chain_visualizer/KinematicChainVisualizer.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.kinematic_chain_visualizer 2 | 3 | import com.heledron.spideranimation.spider.components.rendering.renderLine 4 | import com.heledron.spideranimation.utilities.rendering.interpolateTransform 5 | import com.heledron.spideranimation.utilities.rendering.renderBlock 6 | import com.heledron.spideranimation.utilities.rendering.renderText 7 | import com.heledron.spideranimation.spider.configuration.SegmentPlan 8 | import com.heledron.spideranimation.utilities.* 9 | import com.heledron.spideranimation.utilities.centredTransform 10 | import com.heledron.spideranimation.utilities.ecs.ECS 11 | import com.heledron.spideranimation.utilities.maths.FORWARD_VECTOR 12 | import com.heledron.spideranimation.utilities.maths.UP_VECTOR 13 | import com.heledron.spideranimation.utilities.maths.pitch 14 | import com.heledron.spideranimation.utilities.maths.toRadians 15 | import com.heledron.spideranimation.utilities.maths.yaw 16 | import com.heledron.spideranimation.utilities.rendering.RenderGroup 17 | import com.heledron.spideranimation.utilities.rendering.RenderItem 18 | import org.bukkit.Material 19 | import org.bukkit.World 20 | import org.bukkit.entity.Display 21 | import org.bukkit.util.Vector 22 | import org.joml.Matrix4f 23 | import org.joml.Quaternionf 24 | import kotlin.math.sqrt 25 | 26 | 27 | class KinematicChainVisualizer( 28 | val world: World, 29 | val root: Vector, 30 | val segments: List, 31 | val segmentPlans: List, 32 | val straightenRotation: Float 33 | ) { 34 | enum class Stage { 35 | Backwards, 36 | Forwards 37 | } 38 | 39 | var iterator = 0 40 | var previous: Triple>? = null 41 | var stage = Stage.Forwards 42 | var target: Vector? = null 43 | 44 | var detailed = false 45 | set(value) { 46 | field = value 47 | subStage = 0 48 | } 49 | 50 | init { 51 | reset() 52 | } 53 | 54 | companion object { 55 | fun create( 56 | segmentPlans: List, 57 | root: Vector, 58 | world: World, 59 | straightenRotation: Float 60 | ): KinematicChainVisualizer { 61 | val segments = segmentPlans.map { ChainSegment(root.clone(), it.length, it.initDirection) } 62 | return KinematicChainVisualizer(world, root, segments, segmentPlans, straightenRotation) 63 | } 64 | } 65 | 66 | fun resetIterator() { 67 | subStage = 0 68 | iterator = segments.size - 1 69 | previous = null 70 | stage = Stage.Forwards 71 | } 72 | 73 | fun reset() { 74 | resetIterator() 75 | 76 | target = null 77 | 78 | val direction = Vector(0, 1, 0) 79 | val rotAxis = Vector(1, 0, -1) 80 | val rotation = -0.2 81 | val pos = root.clone() 82 | for (segment in segments) { 83 | direction.rotateAroundAxis(rotAxis, rotation) 84 | pos.add(direction.clone().multiply(segment.length)) 85 | segment.position.copy(pos) 86 | } 87 | } 88 | 89 | 90 | private var subStage = 0; 91 | fun step() { 92 | val target = target ?: return 93 | 94 | val isMovingRoot = previous?.first == Stage.Forwards && previous?.second == segments.size - 1 95 | 96 | if (detailed && previous != null && !isMovingRoot) { 97 | subStage++ 98 | 99 | if (subStage <= 4) return 100 | subStage = 0 101 | } 102 | 103 | previous = Triple(stage, iterator, segments.map { it.clone() }) 104 | 105 | if (stage == Stage.Forwards) { 106 | val segment = segments[iterator] 107 | val nextSegment = segments.getOrNull(iterator + 1) 108 | 109 | if (nextSegment == null) { 110 | segment.position.copy(target) 111 | } else { 112 | fabrik_moveSegment(segment.position, nextSegment.position, nextSegment.length) 113 | } 114 | 115 | if (iterator == 0) stage = Stage.Backwards 116 | else iterator-- 117 | } else { 118 | val segment = segments[iterator] 119 | val prevPosition = segments.getOrNull(iterator - 1)?.position ?: root 120 | 121 | fabrik_moveSegment(segment.position, prevPosition, segment.length) 122 | 123 | if (iterator == segments.size - 1) stage = Stage.Forwards 124 | else iterator++ 125 | } 126 | } 127 | 128 | fun straighten(target: Vector) { 129 | resetIterator() 130 | 131 | val pivot = Quaternionf() 132 | 133 | val direction = target.clone().subtract(root).normalize() 134 | val rotation = direction.getRotationAroundAxis(pivot) 135 | 136 | rotation.x += straightenRotation 137 | val orientation = pivot.rotateYXZ(rotation.y, rotation.x, .0f) 138 | 139 | KinematicChain(root, segments).straightenDirection(orientation) 140 | } 141 | 142 | fun fabrik_moveSegment(point: Vector, pullTowards: Vector, segment: Double) { 143 | val direction = pullTowards.clone().subtract(point).normalize() 144 | point.copy(pullTowards).subtract(direction.multiply(segment)) 145 | } 146 | 147 | fun render(): RenderItem { 148 | return if (detailed) { 149 | renderDetailed() 150 | } else { 151 | renderNormal() 152 | } 153 | } 154 | 155 | private fun renderNormal(): RenderGroup { 156 | val group = RenderGroup() 157 | 158 | val pivot = Quaternionf() 159 | 160 | val previous = previous 161 | for (i in segments.indices) { 162 | val segmentPlan = segmentPlans[i] 163 | val segment = segments[i] 164 | 165 | val list = if (previous == null || i == previous.second) segments else previous.third 166 | 167 | val prev = list.getOrNull(i - 1)?.position ?: root.clone() 168 | val vector = segment.position.clone().subtract(prev) 169 | if (!vector.isZero) vector.normalize().multiply(segment.length) 170 | val position = segment.position.clone().subtract(vector.clone()) 171 | 172 | val rotation = KinematicChain(root, list).getRotations(pivot)[i] 173 | val transform = Matrix4f().rotate(rotation) 174 | 175 | for (piece in segmentPlan.model.pieces) { 176 | group[i to piece] = renderBlock( 177 | world = world, 178 | position = position, 179 | init = { 180 | it.teleportDuration = 3 181 | it.interpolationDuration = 3 182 | }, 183 | update = { 184 | val pieceTransform = Matrix4f(transform).mul(piece.transform) 185 | it.interpolateTransform(pieceTransform) 186 | it.block = piece.block 187 | it.brightness = piece.brightness 188 | } 189 | ) 190 | } 191 | } 192 | 193 | return group 194 | } 195 | 196 | private fun renderDetailed(): RenderGroup { 197 | val group = RenderGroup() 198 | 199 | val previous = previous 200 | 201 | var renderedSegments = segments 202 | 203 | if (previous != null) run arrow@{ 204 | val (stage, iterator, segments) = previous 205 | 206 | val arrowStart = if (stage == Stage.Forwards) 207 | segments.getOrNull(iterator + 1)?.position else 208 | segments.getOrNull(iterator - 1)?.position ?: root 209 | 210 | if (arrowStart == null) return@arrow 211 | renderedSegments = segments 212 | 213 | // stage 0: subtract vector 214 | val arrow = segments[iterator].position.clone().subtract(arrowStart) 215 | 216 | // stage 1: normalise vector 217 | if (subStage >= 1) arrow.normalize() 218 | 219 | // stage 2: multiply by length 220 | if (subStage >= 2) arrow.multiply(segments[iterator].length) 221 | 222 | // stage 3: move segment 223 | if (subStage >= 3) renderedSegments = this.segments 224 | 225 | // stage 4: hide arrow 226 | if (subStage >= 4) return@arrow 227 | 228 | 229 | val crossProduct = if (arrow == UP_VECTOR) FORWARD_VECTOR else 230 | arrow.clone().crossProduct(UP_VECTOR).normalize() 231 | 232 | val arrowCenter = arrowStart.clone() 233 | .add(arrow.clone().multiply(0.5)) 234 | .add(crossProduct.rotateAroundAxis(arrow, Math.toRadians(-90.0)).multiply(.5)) 235 | 236 | group["arrow_length"] = renderText( 237 | world = world, 238 | position = arrowCenter, 239 | init = { 240 | it.teleportDuration = 3 241 | it.billboard = Display.Billboard.CENTER 242 | }, 243 | update = { 244 | it.text = String.format("%.2f", arrow.length()) 245 | } 246 | ) 247 | 248 | group["arrow"] = renderArrow( 249 | world = world, 250 | position = arrowStart, 251 | length = arrow.length().toFloat(), 252 | matrix = Matrix4f().rotateYXZ(arrow.yaw(), arrow.pitch(), 90f.toRadians()), 253 | arrowHeadLength = .4f, 254 | thickness = .101f, 255 | blockData = Material.GOLD_BLOCK.createBlockData(), 256 | interpolation = 3, 257 | ) 258 | } 259 | 260 | group["root"] = renderPoint(world, root, Material.DIAMOND_BLOCK) 261 | 262 | for (i in renderedSegments.indices) { 263 | val segment = renderedSegments[i] 264 | group["p$i"] = renderPoint(world, segment.position, Material.EMERALD_BLOCK) 265 | 266 | val prev = renderedSegments.getOrNull(i - 1)?.position ?: root 267 | 268 | val (a,b) = prev to segment.position 269 | 270 | group[i] = renderLine( 271 | world = world, 272 | position = a, 273 | vector = b.clone().subtract(a), 274 | thickness = .1f, 275 | interpolation = 3, 276 | update = { 277 | it.brightness = Display.Brightness(0, 15) 278 | it.block = Material.BLACK_STAINED_GLASS.createBlockData() 279 | } 280 | ) 281 | } 282 | 283 | return group 284 | } 285 | } 286 | 287 | private fun renderPoint(world: World, position: Vector, block: Material) = renderBlock( 288 | world = world, 289 | position = position, 290 | init = { 291 | it.block = block.createBlockData() 292 | it.teleportDuration = 3 293 | it.brightness = Display.Brightness(15, 15) 294 | it.transformation = centredTransform(.26f, .26f, .26f) 295 | } 296 | ) 297 | 298 | 299 | fun renderArrow( 300 | world: World, 301 | position: Vector, 302 | blockData: org.bukkit.block.data.BlockData, 303 | matrix: Matrix4f, 304 | length: Float, 305 | thickness: Float, 306 | arrowHeadLength: Float, 307 | arrowHeadRotation: Float = 45f.toRadians(), 308 | interpolation: Int, 309 | ): RenderGroup { 310 | val group = RenderGroup() 311 | val zFightingOffset = 0.001f 312 | 313 | // render line 314 | group[0] = renderBlock( 315 | world = world, 316 | position = position, 317 | init = { 318 | it.block = blockData 319 | it.interpolationDuration = interpolation 320 | }, 321 | update = { 322 | it.interpolateTransform( 323 | Matrix4f(matrix) 324 | .scale(thickness, thickness, length - thickness * sqrt(2f)) // offset length for arrow head 325 | .translate(-.5f, -.5f, 0f) 326 | ) 327 | } 328 | ) 329 | 330 | 331 | // render arrow head 332 | for (sign in listOf(1, -1)) group[sign] = renderBlock( 333 | world = world, 334 | position = position, 335 | init = { 336 | // it.block = if (sign == 1) Material.BLACK_CONCRETE.createBlockData() else Material.WHITE_CONCRETE.createBlockData() 337 | it.block = blockData 338 | it.interpolationDuration = interpolation 339 | }, 340 | update = { 341 | // prevent z-fighting 342 | val headThickness = thickness + zFightingOffset * (2 + sign) 343 | 344 | it.interpolateTransform( 345 | Matrix4f(matrix) 346 | .translate(0f, 0f, length) 347 | .rotateY(180f.toRadians() - arrowHeadRotation * sign) 348 | .translate(0f, 0f, -zFightingOffset * sign) 349 | .scale(headThickness, headThickness, arrowHeadLength) 350 | .translate(-.5f + .5f * sign, -.5f, 0f) 351 | ) 352 | } 353 | ) 354 | 355 | return group 356 | } 357 | 358 | 359 | fun setupChainVisualizer(app: ECS) { 360 | app.onRender { 361 | for (visualizer in app.query()) { 362 | visualizer.render().submit(visualizer) 363 | } 364 | } 365 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/heledron/spideranimation/spider/components/body/SpiderBody.kt: -------------------------------------------------------------------------------- 1 | package com.heledron.spideranimation.spider.components.body 2 | 3 | import com.heledron.spideranimation.spider.configuration.BodyPlan 4 | import com.heledron.spideranimation.spider.configuration.Gait 5 | import com.heledron.spideranimation.spider.configuration.LerpGait 6 | import com.heledron.spideranimation.spider.configuration.SpiderDebugOptions 7 | import com.heledron.spideranimation.utilities.* 8 | import com.heledron.spideranimation.utilities.ecs.ECS 9 | import com.heledron.spideranimation.utilities.ecs.ECSEntity 10 | import com.heledron.spideranimation.utilities.isOnGround 11 | import com.heledron.spideranimation.utilities.raycastGround 12 | import com.heledron.spideranimation.utilities.resolveCollision 13 | import com.heledron.spideranimation.utilities.maths.DOWN_VECTOR 14 | import com.heledron.spideranimation.utilities.maths.FORWARD_VECTOR 15 | import com.heledron.spideranimation.utilities.maths.UP_VECTOR 16 | import com.heledron.spideranimation.utilities.maths.lerp 17 | import com.heledron.spideranimation.utilities.maths.pitch 18 | import com.heledron.spideranimation.utilities.maths.pitchRadians 19 | import com.heledron.spideranimation.utilities.maths.rotate 20 | import com.heledron.spideranimation.utilities.maths.yawRadians 21 | import com.heledron.spideranimation.utilities.overloads.sendDebugChatMessage 22 | import org.bukkit.Location 23 | import org.bukkit.World 24 | import org.bukkit.util.Vector 25 | import org.joml.Quaterniond 26 | import org.joml.Quaternionf 27 | import org.joml.Vector2d 28 | import org.joml.Vector3f 29 | import kotlin.math.* 30 | 31 | 32 | class SpiderBodyHitGroundEvent(val spider: SpiderBody) 33 | 34 | class SpiderBody( 35 | val world: World, 36 | val position: Vector, 37 | val orientation: Quaternionf, 38 | var bodyPlan: BodyPlan, 39 | var gallopGait: Gait, 40 | var walkGait: Gait, 41 | ) { 42 | var onGround = false; private set 43 | var legs: List = emptyList() 44 | var normal: NormalInfo? = null; private set 45 | var normalAcceleration = Vector(0.0, 0.0, 0.0); private set 46 | 47 | var debug = SpiderDebugOptions() 48 | 49 | // params 50 | var gallop = false 51 | val gait get() = if (gallop) gallopGait else walkGait 52 | 53 | 54 | // state 55 | var isWalking = false 56 | var isRotatingYaw = false 57 | 58 | fun lerpedGait(): LerpGait { 59 | if (isRotatingYaw) { 60 | return gait.moving.clone() 61 | } 62 | 63 | val speedFraction = velocity.length() / gait.maxSpeed 64 | return gait.stationary.clone().lerp(gait.moving, speedFraction) 65 | } 66 | 67 | companion object { 68 | fun fromLocation(location: Location, bodyPlan: BodyPlan, gallopGait: Gait, walkGait: Gait): SpiderBody { 69 | val world = location.world!! 70 | val position = location.toVector() 71 | val orientation = Quaternionf().rotationYXZ(location.yawRadians(), location.pitchRadians(), 0f) 72 | return SpiderBody(world, position, orientation, bodyPlan, gallopGait = gallopGait, walkGait = walkGait) 73 | } 74 | } 75 | 76 | // utils 77 | fun location(): Location { 78 | val location = position.toLocation(world) 79 | location.direction = forwardDirection() 80 | return location 81 | } 82 | 83 | fun forwardDirection() = FORWARD_VECTOR.rotate(Quaterniond(orientation)) 84 | 85 | // memo 86 | var preferredPitch = orientation.getEulerAnglesYXZ(Vector3f()).x 87 | var preferredRoll = orientation.getEulerAnglesYXZ(Vector3f()).z 88 | var preferredOrientation = Quaternionf(orientation) 89 | 90 | val velocity = Vector(0.0, 0.0, 0.0) 91 | val rotationalVelocity = Vector3f(0f,0f,0f) 92 | 93 | fun accelerateRotation(axis: Vector, angle: Float) { 94 | val acceleration = Quaternionf().rotateAxis(angle, axis.toVector3f()) 95 | val oldVelocity = Quaternionf().rotationYXZ(rotationalVelocity.y, rotationalVelocity.x, rotationalVelocity.z) 96 | 97 | val rotVelocity = acceleration.mul(oldVelocity) 98 | 99 | val rotEuler = rotVelocity.getEulerAnglesYXZ(Vector3f()) 100 | rotationalVelocity.set(rotEuler) 101 | } 102 | 103 | fun teleport(entity: ECSEntity, newPosition: Vector) { 104 | val diff = newPosition.subtract(position) 105 | 106 | position.copy(newPosition) 107 | 108 | val body = entity.query() ?: return 109 | for (leg in body.legs) leg.endEffector.add(diff) 110 | } 111 | 112 | private fun updatePreferredAngles() { 113 | val currentEuler = orientation.getEulerAnglesYXZ(Vector3f()) 114 | 115 | if (gait.disableAdvancedRotation) { 116 | preferredPitch = .0f 117 | preferredRoll = .0f 118 | preferredOrientation = Quaternionf().rotationYXZ(currentEuler.y, .0f, .0f) 119 | return 120 | } 121 | 122 | fun getPos(leg: Leg): Vector { 123 | // if (leg.isOutsideTriggerZone) return leg.endEffector 124 | return leg.groundPosition ?: leg.restPosition 125 | } 126 | 127 | val frontLeft = getPos(legs.getOrNull(0) ?: return) 128 | val frontRight = getPos(legs.getOrNull(1) ?: return) 129 | val backLeft = getPos(legs.getOrNull(legs.size - 2) ?: return) 130 | val backRight = getPos(legs.getOrNull(legs.size - 1) ?: return) 131 | 132 | val forwardLeft = frontLeft.clone().subtract(backLeft) 133 | val forwardRight = frontRight.clone().subtract(backRight) 134 | val forward = listOf(forwardLeft, forwardRight).average() 135 | 136 | val sideways = Vector(0.0,0.0,0.0) 137 | for (i in 0 until legs.size step 2) { 138 | val left = legs.getOrNull(i) ?: continue 139 | val right = legs.getOrNull(i + 1) ?: continue 140 | 141 | sideways.add(getPos(right).clone().subtract(getPos(left))) 142 | } 143 | 144 | preferredPitch = forward.pitch().lerp(preferredPitch, gait.preferredRotationLerpFraction) 145 | preferredRoll = sideways.pitch().lerp(preferredRoll, gait.preferredRotationLerpFraction) 146 | 147 | if (preferredPitch < gait.preferLevelBreakpoint) preferredPitch *= 1 - gait.preferLevelBias 148 | if (preferredRoll < gait.preferLevelBreakpoint) preferredRoll *= 1 - gait.preferLevelBias 149 | 150 | 151 | preferredOrientation = Quaternionf().rotationYXZ(currentEuler.y, preferredPitch, preferredRoll) 152 | } 153 | 154 | fun init(ecs: ECS, entity: ECSEntity) { 155 | legs = bodyPlan.legs.map { Leg( ecs, entity, this, it) } 156 | } 157 | 158 | fun update(ecs: ECS, entity: ECSEntity) { 159 | if (legs.isEmpty()) { 160 | init(ecs, entity) 161 | 162 | if (legs.isEmpty()) { 163 | sendDebugChatMessage("WARNING: No legs") 164 | return 165 | } 166 | } 167 | 168 | updatePreferredAngles() 169 | 170 | val groundedLegs = legs.filter { it.isGrounded() } 171 | val fractionOfLegsGrounded = groundedLegs.size.toDouble() / legs.size 172 | 173 | // apply gravity and air resistance 174 | velocity.y -= gait.gravityAcceleration 175 | velocity.y *= (1 - gait.airDragCoefficient) 176 | 177 | // apply rotational velocity 178 | val rotVelocity = Quaternionf().rotationYXZ(rotationalVelocity.y, rotationalVelocity.x, rotationalVelocity.z) 179 | orientation.set(rotVelocity.mul(orientation)) 180 | 181 | // apply drag while leg on ground 182 | if (!isWalking) { 183 | val legDrag = 1 - gait.groundDragCoefficient * fractionOfLegsGrounded 184 | velocity.x *= legDrag 185 | velocity.z *= legDrag 186 | } 187 | 188 | // apply rotational drag 189 | val rotDrag = 1 - gait.rotationalDragCoefficient * fractionOfLegsGrounded.toFloat() 190 | rotationalVelocity.mul(rotDrag) 191 | 192 | // apply drag while body on ground 193 | if (onGround) { 194 | val bodyDrag = .5f 195 | velocity.x *= bodyDrag 196 | velocity.z *= bodyDrag 197 | 198 | rotationalVelocity.mul(bodyDrag) 199 | } 200 | 201 | val normal = calcNormal() 202 | this.normal = normal 203 | 204 | normalAcceleration = Vector(0.0, 0.0, 0.0) 205 | if (normal != null) { 206 | val preferredY = calcPreferredY() 207 | val preferredYAcceleration = (preferredY - position.y - velocity.y).coerceAtLeast(0.0) 208 | val capableAcceleration = gait.bodyHeightCorrectionAcceleration * fractionOfLegsGrounded 209 | val accelerationMagnitude = min(preferredYAcceleration, capableAcceleration) 210 | 211 | normalAcceleration = normal.normal.clone().multiply(accelerationMagnitude) 212 | 213 | // if the horizontal acceleration is too high, 214 | // there's no point accelerating as the spider will fall over anyway 215 | if (normalAcceleration.horizontalLength() > normalAcceleration.y) normalAcceleration.multiply(0.0) 216 | 217 | velocity.add(normalAcceleration) 218 | } 219 | 220 | // apply velocity 221 | position.add(velocity) 222 | 223 | // resolve collision 224 | 225 | val collision = world.resolveCollision(position, Vector(0.0, min(-1.0, -abs(velocity.y)), 0.0)) 226 | if (collision != null) { 227 | onGround = true 228 | 229 | val didHit = collision.offset.length() > (gait.gravityAcceleration * 2) * (1 - gait.airDragCoefficient) 230 | if (didHit) ecs.emit(SpiderBodyHitGroundEvent(spider = this)) 231 | 232 | position.y = collision.position.y 233 | if (velocity.y < 0) velocity.y *= -gait.bounceFactor 234 | if (velocity.y < gait.gravityAcceleration) velocity.y = .0 235 | } else { 236 | onGround = world.isOnGround(position, DOWN_VECTOR.rotate(orientation)) 237 | } 238 | 239 | val updateOrder = gait.type.getLegsInUpdateOrder(this) 240 | for (leg in updateOrder) leg.updateMemo() 241 | for (leg in updateOrder) leg.update() 242 | 243 | updatePreferredAngles() 244 | } 245 | 246 | private fun legsInPolygonalOrder(): List { 247 | val lefts = legs.indices.filter { LegLookUp.isLeftLeg(it) } 248 | val rights = legs.indices.filter { LegLookUp.isRightLeg(it) } 249 | return lefts + rights.reversed() 250 | } 251 | 252 | 253 | private fun calcPreferredY(): Double { 254 | val lookAhead = position.clone().add(velocity) 255 | val ground = world.raycastGround(lookAhead, DOWN_VECTOR.rotate(preferredOrientation), lerpedGait().bodyHeight) 256 | val groundY = ground?.hitPosition?.y ?: -Double.MAX_VALUE 257 | 258 | val averageY = legs.map { it.target.position.y }.average() + lerpedGait().bodyHeight 259 | 260 | val pivot = gait.legChainPivotMode.get(this) 261 | val target = UP_VECTOR.rotate(pivot).multiply(gait.maxBodyDistanceFromGround) 262 | val targetY = max(averageY, groundY + target.y) 263 | val stabilizedY = position.y.lerp(targetY, gait.bodyHeightCorrectionFactor) 264 | 265 | return stabilizedY 266 | } 267 | 268 | private fun applyStabilization(normal: NormalInfo) { 269 | if (normal.origin == null) return 270 | if (normal.centreOfMass == null) return 271 | 272 | if (normal.origin.horizontalDistance(normal.centreOfMass) < gait.polygonLeeway) { 273 | normal.origin.x = normal.centreOfMass.x 274 | normal.origin.z = normal.centreOfMass.z 275 | } 276 | 277 | val stabilizationTarget = normal.origin.clone().setY(normal.centreOfMass.y) 278 | normal.centreOfMass.lerp(stabilizationTarget, gait.stabilizationFactor) 279 | 280 | normal.normal.copy(normal.centreOfMass).subtract(normal.origin).normalize() 281 | } 282 | 283 | private fun calcLegacyNormal(): NormalInfo? { 284 | val pairs = LegLookUp.diagonalPairs(legs.indices.toList()) 285 | if (pairs.any { pair -> pair.mapNotNull { legs.getOrNull(it) }.all { it.isGrounded() } }) { 286 | return NormalInfo(normal = Vector(0, 1, 0)) 287 | } 288 | 289 | return null 290 | } 291 | 292 | private fun calcNormal(): NormalInfo? { 293 | if (gait.useLegacyNormalForce) return calcLegacyNormal() 294 | 295 | val centreOfMass = legs.map { it.endEffector }.average() 296 | centreOfMass.lerp(position, 0.5) 297 | centreOfMass.y += 0.01 298 | 299 | val groundedLegs = legsInPolygonalOrder().map { legs[it] }.filter { it.isGrounded() } 300 | if (groundedLegs.isEmpty()) return null 301 | 302 | val legsPolygon = groundedLegs.map { it.endEffector.clone() } 303 | val polygonCenterY = legsPolygon.map { it.y }.average() 304 | 305 | // only 1 leg on ground 306 | if (legsPolygon.size == 1) { 307 | val origin = groundedLegs.first().endEffector.clone() 308 | return NormalInfo( 309 | normal = centreOfMass.clone().subtract(origin).normalize(), 310 | origin = origin, 311 | centreOfMass = centreOfMass, 312 | contactPolygon = legsPolygon 313 | ).apply { applyStabilization(this) } 314 | } 315 | 316 | val polygon2D = legsPolygon.map { Vector2d(it.x, it.z) } 317 | 318 | // inside polygon. accelerate upwards towards centre of mass 319 | if (pointInPolygon(Vector2d(centreOfMass.x, centreOfMass.z), polygon2D)) return NormalInfo( 320 | normal = Vector(0, 1, 0), 321 | origin = Vector(centreOfMass.x, polygonCenterY, centreOfMass.z), 322 | centreOfMass = centreOfMass, 323 | contactPolygon = legsPolygon 324 | ) 325 | 326 | // outside polygon, accelerate at an angle from within the polygon 327 | val point = nearestPointInPolygon(Vector2d(centreOfMass.x, centreOfMass.z), polygon2D) 328 | val origin = Vector(point.x, polygonCenterY, point.y) 329 | return NormalInfo( 330 | normal = centreOfMass.clone().subtract(origin).normalize(), 331 | origin = origin, 332 | centreOfMass = centreOfMass, 333 | contactPolygon = legsPolygon 334 | ).apply { applyStabilization(this)} 335 | } 336 | } 337 | 338 | class NormalInfo( 339 | // most of these fields are only used for debug rendering 340 | val normal: Vector, 341 | val origin: Vector? = null, 342 | val contactPolygon: List? = null, 343 | val centreOfMass: Vector? = null 344 | ) 345 | 346 | fun setupSpiderBody(app: ECS) { 347 | app.onTick { 348 | for ((entity, spider) in app.query()) { 349 | spider.update(app, entity) 350 | } 351 | } 352 | } --------------------------------------------------------------------------------