Object breaking

This commit is contained in:
DBotThePony 2024-04-22 20:11:16 +07:00
parent f9fe93383f
commit baf04fe447
Signed by: DBot
GPG Key ID: DCC23B5715498507
7 changed files with 214 additions and 54 deletions

View File

@ -3,28 +3,15 @@
This document briefly documents what have been added (or removed) regarding modding capabilities or engine behavior(s) This document briefly documents what have been added (or removed) regarding modding capabilities or engine behavior(s)
## JSON additions This document is non-exhaustive, engine contains way more behavior change bits than documented here,
but listing all of them will be a hassle, and will pollute actually useful changes.
--------------- ---------------
### Worldgen ## Prototypes
* Where applicable, Perlin noise now can have custom seed specified
* Change above allows to explicitly specify universe seed (as `celestial.config:systemTypePerlin:seed`)
* `treasurechests` now can specify `treasurePool` as array
#### Terrain * `treasurechests` now can specify `treasurePool` as array
* All composing terrain selectors (such as `min`, `displacement`, `rotate`, etc) now can reference other terrain selectors by name (the `.terrain` files) instead of embedding entire config inside them * `damageTable` can be defined directly, without referencing other JSON file (experimental feature)
* They can be referenced by either specifying corresponding field as string, or as object like so: `{"name": "namedselector"}`
* `min`, `max` and `minmax` terrain selectors now also accept next format: `{"name": "namedselector", "seedBias": 4}`
* `mix` terrain selector got `mixSeedBias`, `aSeedBias` and `bSeedBias` fields, whose deviate respective selectors seeds (default to `0`)
* `displacement` terrain selector has `seedBias` added, which deviate seed of `source` selector (default to `0`)
* `displacement` terrain selector has `xClamp` added, works like `yClamp`
* `rotate` terrain selector has `rotationWidth` (defaults to `0.5`) and `rotationHeight` (defaults to `0.0`) added, which are multiplied by world's size and world's height respectively to determine rotation point center
* `min` terrain selector added, opposite of existing `max` (json format is the same as `max`)
* `cache` terrain selector removed due it not being documented, and having little practical value
* `perlin` terrain selector now accepts `type`, `frequency` and `amplitude` values (naming inconsistency fix)
* `ridgeblocks` terrain selector now accepts `amplitude` and `frequency` values (naming inconsistency fix);
* `ridgeblocks` has `octaves` added (defaults to `2`), `perlinOctaves` (defaults to `1`)
#### Biomes #### Biomes
* Tree biome placeables now have `variantsRange` (defaults to `[1, 1]`) and `subVariantsRange` (defaults to `[2, 2]`) * Tree biome placeables now have `variantsRange` (defaults to `[1, 1]`) and `subVariantsRange` (defaults to `[2, 2]`)
@ -53,18 +40,25 @@ val color: TileColor = TileColor.DEFAULT
* By default, they mark entire _part_ of dungeon with their ID. To mark specific tile inside dungeon with its own Dungeon ID, supply `true` as third value to brush (e.g `["dungeonid", 40000, true"]`) * By default, they mark entire _part_ of dungeon with their ID. To mark specific tile inside dungeon with its own Dungeon ID, supply `true` as third value to brush (e.g `["dungeonid", 40000, true"]`)
* Tiled map behavior is unchanged, and marks their position only. * Tiled map behavior is unchanged, and marks their position only.
--------------- #### .terrain
* All composing terrain selectors (such as `min`, `displacement`, `rotate`, etc) now can reference other terrain selectors by name (the `.terrain` files) instead of embedding entire config inside them
* They can be referenced by either specifying corresponding field as string, or as object like so: `{"name": "namedselector"}`
* `min`, `max` and `minmax` terrain selectors now also accept next format: `{"name": "namedselector", "seedBias": 4}`
* `mix` terrain selector got `mixSeedBias`, `aSeedBias` and `bSeedBias` fields, whose deviate respective selectors seeds (default to `0`)
* `displacement` terrain selector has `seedBias` added, which deviate seed of `source` selector (default to `0`)
* `displacement` terrain selector has `xClamp` added, works like `yClamp`
* `rotate` terrain selector has `rotationWidth` (defaults to `0.5`) and `rotationHeight` (defaults to `0.0`) added, which are multiplied by world's size and world's height respectively to determine rotation point center
* `min` terrain selector added, opposite of existing `max` (json format is the same as `max`)
* `cache` terrain selector removed due it not being documented, and having little practical value
* `perlin` terrain selector now accepts `type`, `frequency` and `amplitude` values (naming inconsistency fix)
* `ridgeblocks` terrain selector now accepts `amplitude` and `frequency` values (naming inconsistency fix);
* `ridgeblocks` has `octaves` added (defaults to `2`), `perlinOctaves` (defaults to `1`)
### player.config ### player.config
* Inventory bags are no longer limited to 255 slots * Inventory bags are no longer limited to 255 slots
* However, when joining original servers with mod which increase bag size past 255 slots will result in undefined behavior (joining servers with inventory size bag mods will already result in nearly instant desync though, so you may not ever live to see the side effects; and if original server installs said mod, original clients and original server will experience severe desyncs/undefined behavior too) * However, when joining original servers with mod which increase bag size past 255 slots will result in undefined behavior (joining servers with inventory size bag mods will already result in nearly instant desync though, so you may not ever live to see the side effects; and if original server installs said mod, original clients and original server will experience severe desyncs/undefined behavior too)
--------------- #### .item
### Prototypes
* `damageTable` can be defined directly, without referencing other JSON file (experimental feature)
#### Items
* `inventoryIcon` additions if specified as array: * `inventoryIcon` additions if specified as array:
* `scale`, either as float or as vector (for x and y scales); both in prototype file and in `parameters`. * `scale`, either as float or as vector (for x and y scales); both in prototype file and in `parameters`.
* `color` (defaults to white `[255, 255, 255, 255]`) * `color` (defaults to white `[255, 255, 255, 255]`)
@ -91,13 +85,17 @@ val color: TileColor = TileColor.DEFAULT
* Used by world tile rendering code (render piece rule `Connects`) * Used by world tile rendering code (render piece rule `Connects`)
* And finally, used by `canPlaceMaterial` to determine whenever player can place blocks next to it (at least one such tile should be present for player to be able to place blocks next to it) * And finally, used by `canPlaceMaterial` to determine whenever player can place blocks next to it (at least one such tile should be present for player to be able to place blocks next to it)
#### .object
* `breakDropOptions` and `smashDropOptions` items created now obey world's threat level
* `smashDropPool`, `breakDropPool`, `breakDropOptions` and `smashDropOptions` are now deterministic (see [worldgen section](#Deterministic_world_generation))
#### .matmod #### .matmod
* `modId` is no longer essential and can be skipped, or specified as any number in 1 -> 2^31 range, with notes of `materialId` and `liquidId` apply. * `modId` is no longer essential and can be skipped, or specified as any number in 1 -> 2^31 range, with notes of `materialId` and `liquidId` apply.
## Scripting
--------------- ---------------
## Scripting
#### Random #### Random
* Added `random:randn(deviation: double, mean: double): double`, returns normally distributed double, where `deviation` stands for [Standard deviation](https://en.wikipedia.org/wiki/Standard_deviation), and `mean` specifies middle point * Added `random:randn(deviation: double, mean: double): double`, returns normally distributed double, where `deviation` stands for [Standard deviation](https://en.wikipedia.org/wiki/Standard_deviation), and `mean` specifies middle point
* Removed `random:addEntropy` * Removed `random:addEntropy`
@ -130,23 +128,44 @@ val color: TileColor = TileColor.DEFAULT
* Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>` * Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* `world.entityCanDamage(source: EntityID, target: EntityID): Boolean` now properly accounts for case when `source == target` * `world.entityCanDamage(source: EntityID, target: EntityID): Boolean` now properly accounts for case when `source == target`
## Behavior ---------------
## Deterministic world generation
In new engine, entirety of world generation is made deterministic. What this means that given one world seed, engine will
generate _exactly the same_ world each time it is requested to generate one (given prototype definitions which influence
world generation are the same between generations).
To put it simply, when you visit a planet on your friend's server, it is _guaranteed_ that in your singleplayer
or on other server, given same set of mods installed (and both players are using new engine server or new engine client),
you will get exactly the same planet as you saw before.
This includes, but not limited to:
* Containers (such as chests)
* Smashable objects (e.g. capsules, rocks)
* `random` dungeon brush
* Tree types / placement
* Grass / bush variants and placement
* Dungeon placement
* Initial player spawn position in world
* Microdungeon placement
However, this also means that instance worlds will generate 1:1 each time they are requested if
there is `seed` specified for such world `/instance_worlds.config`. And since vanilla dungeons have it specified
(and mod makers don't question "why" it is there), all mission dungeons will be generated 1:1 each time.
If you are mod creator, **PLEASE** update your mod(s), and remove `seed` from your dungeon worlds!
Both new and old engines will provide random seed for you if you don't specify one inside `/instance_worlds.config`.
--------------- ---------------
## Behavior
### universe_server.config ### universe_server.config
* Added `useNewWireProcessing`, which defaults to `true` * Added `useNewWireProcessing`, which defaults to `true`
* New wire updating system is insanely fast (because wiring is updated along entity ticking, and doesn't involve intense entity map lookups) * New wire updating system is insanely fast (because wiring is updated along entity ticking, and doesn't involve intense entity map lookups)
* However, it is not a complete replacement for legacy system, because some mods might rely on fact that in legacy system when wired entities update, they load all other endpoints into memory (basically, chunkload all connected entities). In new system if wired entity references unloaded entities it simply does not update its state. * However, it is not a complete replacement for legacy system, because some mods might rely on fact that in legacy system when wired entities update, they load all other endpoints into memory (basically, chunkload all connected entities). In new system if wired entity references unloaded entities it simply does not update its state.
* If specified as `false`, original behavior will be restored, but beware of performance degradation! If you are a modder, **PLEASE** consider other ways around instead of enabling the old behavior, because performance cost of using old system is almost always gonna outweight "benefits" of chunkloaded wiring systems. * If specified as `false`, original behavior will be restored, but beware of performance degradation! If you are a modder, **PLEASE** consider other ways around instead of enabling the old behavior, because performance cost of using old system is almost always gonna outweight "benefits" of chunkloaded wiring systems.
### Worldgen
* Major dungeon placement on planets is now deterministic
* Container item population in dungeons is now deterministic and is based on dungeon seed
* However, this might backfire, if you specify `seed` inside `/instance_worlds.config`; since that will set dungeon's contents in stone (don't do this, remove seed from your dungeon data, please. Both original and new engines will provide random seed for you on each world generation if you remove your own seed from data)
#### Dungeons
* All brushes are now deterministic
#### Plant drop entities (vines or steps dropping on ground) #### Plant drop entities (vines or steps dropping on ground)
* Collision is now determined using hull instead of rectangle * Collision is now determined using hull instead of rectangle

View File

@ -1,6 +1,5 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.google.common.collect.ImmutableMap
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import ru.dbotthepony.kstarbound.io.StarboundPak import ru.dbotthepony.kstarbound.io.StarboundPak
import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.util.sbIntern
@ -96,7 +95,7 @@ interface IStarboundFile : ISBFileLocator {
return path return path
} }
fun computeDirectory(): String { fun computeDirectory(includeLastSlash: Boolean = false): String {
var path = "" var path = ""
var parent = parent var parent = parent
@ -109,6 +108,9 @@ interface IStarboundFile : ISBFileLocator {
parent = parent.parent parent = parent.parent
} }
if (includeLastSlash && path.last() != '/')
return "$path/"
return path return path
} }
@ -278,13 +280,16 @@ class PhysicalFile(val real: File, override val parent: PhysicalFile? = null) :
get() = real.name get() = real.name
private val fullPatch by lazy { super.computeFullPath().sbIntern() } private val fullPatch by lazy { super.computeFullPath().sbIntern() }
private val directory by lazy { super.computeDirectory().sbIntern() } private val directory by lazy { super.computeDirectory(false).sbIntern() }
override fun computeFullPath(): String { override fun computeFullPath(): String {
return fullPatch return fullPatch
} }
override fun computeDirectory(): String { override fun computeDirectory(includeLastSlash: Boolean): String {
if (includeLastSlash)
return "$directory/"
return directory return directory
} }

View File

@ -409,8 +409,14 @@ fun provideRootBindings(lua: LuaEnvironment) {
table["techType"] = luaFunctionN("techType", ::techType) table["techType"] = luaFunctionN("techType", ::techType)
table["techConfig"] = luaFunctionN("techConfig", ::techConfig) table["techConfig"] = luaFunctionN("techConfig", ::techConfig)
table["treeStemDirectory"] = luaStub("treeStemDirectory") table["treeStemDirectory"] = luaFunction { name: ByteString ->
table["treeFoliageDirectory"] = luaStub("treeFoliageDirectory") returnBuffer.setTo(Registries.treeStemVariants[name.decode()]?.file?.computeDirectory(true) ?: "/")
}
table["treeFoliageDirectory"] = luaFunction { name: ByteString ->
returnBuffer.setTo(Registries.treeFoliageVariants[name.decode()]?.file?.computeDirectory(true))
}
table["collection"] = luaStub("collection") table["collection"] = luaStub("collection")
table["collectables"] = luaStub("collectables") table["collectables"] = luaStub("collectables")
table["elementalResistance"] = luaStub("elementalResistance") table["elementalResistance"] = luaStub("elementalResistance")

View File

@ -27,6 +27,7 @@ import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) { fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
@ -60,7 +61,15 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
table["setProcessingDirectives"] = luaFunction { directives: ByteString -> self.animator.processingDirectives = directives.decode() } table["setProcessingDirectives"] = luaFunction { directives: ByteString -> self.animator.processingDirectives = directives.decode() }
table["setSoundEffectEnabled"] = luaFunction { state: Boolean -> self.soundEffectEnabled = state } table["setSoundEffectEnabled"] = luaFunction { state: Boolean -> self.soundEffectEnabled = state }
table["smash"] = luaFunction { smash: Boolean? -> self.callBreak(smash ?: false) }
table["smash"] = luaFunction { smash: Boolean? ->
if (smash == true) {
self.health = 0.0
}
self.remove(AbstractEntity.RemovalReason.DYING)
}
table["level"] = luaFunction { returnBuffer.setTo(self.lookupProperty(JsonPath("level")) { JsonPrimitive(self.world.template.threatLevel) }.asDouble) } table["level"] = luaFunction { returnBuffer.setTo(self.lookupProperty(JsonPath("level")) { JsonPrimitive(self.world.template.threatLevel) }.asDouble) }
table["toAbsolutePosition"] = luaFunction { pos: Table -> returnBuffer.setTo(from(toVector2d(pos) + self.position)) } table["toAbsolutePosition"] = luaFunction { pos: Table -> returnBuffer.setTo(from(toVector2d(pos) + self.position)) }

View File

@ -23,6 +23,8 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.item.IContainer import ru.dbotthepony.kstarbound.item.IContainer
import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
@ -64,7 +66,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
override fun tick(delta: Double) { override fun tick(delta: Double) {
super.tick(delta) super.tick(delta)
if (world.isServer) { if (isInWorld && world.isServer) {
for (item in lostItems) { for (item in lostItems) {
val entity = ItemDropEntity(item) val entity = ItemDropEntity(item)
entity.position = position entity.position = position
@ -81,6 +83,20 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
} }
} }
override fun onRemove(world: World<*, *>, reason: RemovalReason) {
super.onRemove(world, reason)
if (!isRemote && reason.dying) {
for (i in 0 until items.size) {
val item = items[i]
if (item.isNotEmpty) {
spewItem(item)
}
}
}
}
override fun interact(request: InteractRequest): InteractAction { override fun interact(request: InteractRequest): InteractAction {
return InteractAction(InteractAction.Type.OPEN_CONTAINER, entityID) return InteractAction(InteractAction.Type.OPEN_CONTAINER, entityID)
} }

View File

@ -15,13 +15,16 @@ import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.orEmptyTile import ru.dbotthepony.kstarbound.defs.tile.orEmptyTile
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.server.world.ServerChunk import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
/** /**
* (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid * (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid
@ -318,8 +321,6 @@ abstract class TileEntity : AbstractEntity() {
fun updateMaterialSpacesNow() { fun updateMaterialSpacesNow() {
needToUpdateSpaces = false needToUpdateSpaces = false
// only server can update entity tiles
// even if this tile entity is owned by client
if (world.isServer) { if (world.isServer) {
needToUpdateSpaces = !updateMaterialSpaces(materialSpaces) needToUpdateSpaces = !updateMaterialSpaces(materialSpaces)
} }
@ -328,13 +329,20 @@ abstract class TileEntity : AbstractEntity() {
fun updateRootsNow() { fun updateRootsNow() {
needToUpdateRoots = false needToUpdateRoots = false
// only server can update entity tiles
// even if this tile entity is owned by client
if (world.isServer) { if (world.isServer) {
needToUpdateRoots = !updateRoots(roots) needToUpdateRoots = !updateRoots(roots)
} }
} }
fun spewItem(item: ItemStack) {
val entity = ItemDropEntity(item)
entity.position = occupySpaces.random(world.random) { tilePosition }.toDoubleVector()
entity.movement.velocity += Vector2d(world.random.nextDouble(-1.0, 1.0), world.random.nextDouble(-1.0, 1.0))
entity.joinWorld(world)
}
override fun tick(delta: Double) { override fun tick(delta: Double) {
super.tick(delta) super.tick(delta)

View File

@ -39,10 +39,12 @@ import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.InteractAction import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectType import ru.dbotthepony.kstarbound.defs.`object`.ObjectType
import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.io.Vector2iCodec import ru.dbotthepony.kstarbound.io.Vector2iCodec
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.JsonPath import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.json.stream
@ -77,10 +79,12 @@ import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.server.world.LegacyWireProcessor import ru.dbotthepony.kstarbound.server.world.LegacyWireProcessor
import ru.dbotthepony.kstarbound.util.ManualLazy import ru.dbotthepony.kstarbound.util.ManualLazy
import ru.dbotthepony.kstarbound.util.asStringOrNull import ru.dbotthepony.kstarbound.util.asStringOrNull
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.Animator import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.Collections import java.util.Collections
@ -139,10 +143,16 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
/** /**
* called by DungeonWorld to deterministically randomize parameters * called by DungeonWorld or biome placement to deterministically randomize parameters
*/ */
open fun randomize(random: RandomGenerator, threatLevel: Double) { open fun randomize(random: RandomGenerator, threatLevel: Double) {
if ("smashDropSeed" !in parameters) {
parameters["smashDropSeed"] = JsonPrimitive(random.nextLong())
}
if ("breakDropSeed" !in parameters) {
parameters["breakDropSeed"] = JsonPrimitive(random.nextLong())
}
} }
protected val orientationLazies = ArrayList<ManualLazy<*>>() protected val orientationLazies = ArrayList<ManualLazy<*>>()
@ -505,10 +515,6 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
return super.interact(request) return super.interact(request)
} }
fun callBreak(smash: Boolean = false) {
}
fun addChatMessage(message: String, config: JsonElement, portrait: String? = null) { fun addChatMessage(message: String, config: JsonElement, portrait: String? = null) {
} }
@ -520,6 +526,11 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
if (!isRemote) { if (!isRemote) {
tileHealth.tick(config.value.damageConfig, delta) tileHealth.tick(config.value.damageConfig, delta)
if (tileHealth.isHealthy) {
lastClosestSpaceToDamageSource = null
}
animator.tick(delta) animator.tick(delta)
val orientation = orientation val orientation = orientation
@ -607,7 +618,83 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
if (shouldBreak) { if (shouldBreak) {
callBreak(false) remove(RemovalReason.DYING)
}
}
}
private var lastClosestSpaceToDamageSource: Vector2i? = null
override fun onRemove(world: World<*, *>, reason: RemovalReason) {
super.onRemove(world, reason)
val doSmash = health <= 0.0
fun spawnRandomItems(poolName: String, optionsName: String, seedName: String): Boolean {
val dropPool = lookupProperty(poolName) { JsonPrimitive("") }.asString
val dropOptions = lookupProperty(optionsName) { JsonArray() }.asJsonArray
val smashDropSeed = parameters[seedName]?.asLong
val random by lazy {
if (smashDropSeed == null) {
world.random
} else {
random(smashDropSeed)
}
}
if (dropPool.isNotBlank()) {
val pool = Registries.treasurePools[dropPool]
if (pool != null) {
for (item in pool.value.evaluate(random, world.template.threatLevel)) {
spewItem(item)
}
}
return true
} else if (dropOptions.size() > 0) {
val option = dropOptions.random(random)
for (item in option.asJsonArray) {
spewItem(ItemDescriptor(item).build(world.template.threatLevel, random.nextLong(), random))
}
return true
} else {
return false
}
}
if (!isRemote && reason.dying) {
lua.invokeGlobal("die", health <= 0.0)
try {
if (doSmash) {
spawnRandomItems("smashDropPool", "smashDropOptions", "smashDropSeed")
} else {
val spawned = spawnRandomItems("breakDropPool", "breakDropOptions", "breakDropSeed")
if (!spawned && config.value.hasObjectItem) {
val parameters = JsonObject()
if (config.value.retainObjectParametersInItem) {
for ((k, v) in this.parameters) {
parameters[k] = v.deepCopy()
}
parameters.remove("owner")
parameters["scriptStorage"] = toJsonFromLua(lua.globals["storage"])
}
val entity = ItemDropEntity(ItemDescriptor(config.key, 1L, parameters))
entity.position = lastClosestSpaceToDamageSource?.toDoubleVector() ?: (if (occupySpaces.isNotEmpty()) AABB.ofPoints(occupySpaces).centre else position)
entity.joinWorld(world)
}
}
} catch (err: Throwable) {
LOGGER.error("Exception while destroying WorldObject at $tilePosition", err)
} }
} }
} }
@ -617,6 +704,15 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
return false return false
tileHealth.damage(config.value.damageConfig, source, damage) tileHealth.damage(config.value.damageConfig, source, damage)
if (damageSpaces.isNotEmpty()) {
lastClosestSpaceToDamageSource = damageSpaces.minBy { it.toDoubleVector().distanceSquared(source) }
}
if (tileHealth.isDead) {
remove(RemovalReason.DYING)
}
return tileHealth.isDead return tileHealth.isDead
} }
@ -629,6 +725,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
companion object { companion object {
private val LOGGER = LogManager.getLogger()
private val lightColorPath = JsonPath("lightColor") private val lightColorPath = JsonPath("lightColor")
private val lightColorsPath = JsonPath("lightColors") private val lightColorsPath = JsonPath("lightColors")
private val materialSpacesCodec = StreamCodec.Pair(Vector2iCodec, InternedStringCodec.map({ Registries.tiles.ref(this) }, { entry?.key ?: key.left.orElse("") })) private val materialSpacesCodec = StreamCodec.Pair(Vector2iCodec, InternedStringCodec.map({ Registries.tiles.ref(this) }, { entry?.key ?: key.left.orElse("") }))