KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt

406 lines
15 KiB
Kotlin

package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readDouble
import ru.dbotthepony.kstarbound.io.readEnumStupid
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readMVariant2
import ru.dbotthepony.kstarbound.io.readNullableDouble
import ru.dbotthepony.kstarbound.io.readNullableString
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.io.writeEnumStupid
import ru.dbotthepony.kstarbound.io.writeMVariant2
import ru.dbotthepony.kstarbound.io.writeNullable
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.EnumSet
// uint8_t
enum class TeamType(override val jsonName: String) : IStringSerializable {
NULL("null"),
// non-PvP-enabled players and player allied NPCs
FRIENDLY("friendly"),
// hostile and neutral NPCs and monsters
ENEMY("enemy"),
// PvP-enabled players
PVP("pvp"),
// cannot damage anything, can be damaged by Friendly/PVP/Assistant
PASSIVE("passive"),
// cannot damage or be damaged
GHOSTLY("ghostly"),
// cannot damage enemies, can be damaged by anything except enemy
ENVIRONMENT("environment"),
// damages anything except ghostly, damaged by anything except ghostly/passive
// used for self damage
INDISCRIMINATE("indiscriminate"),
// cannot damage friendlies and cannot be damaged by anything
ASSISTANT("assistant");
}
// int32_t
enum class HitType(override val jsonName: String) : IStringSerializable {
HIT("Hit"),
STRONG_HIT("StrongHit"),
WEAK_HIT("WeakHit"),
SHIELD_HIT("ShieldHit"),
KILL("Kill");
}
// uint8_t
enum class DamageType(override val jsonName: String) : IStringSerializable {
NO_DAMAGE("NoDamage"),
DAMAGE("Damage"),
IGNORE_DEFENCE("IgnoresDef"),
KNOCKBACK("IgnoresDef"),
ENVIRONMENT("Environment"),
STATUS("Environment");
}
@JsonFactory
data class DamageKind(
val kind: String,
val elementalType: String = "default",
val effects: JsonObject = JsonObject()
)
@JsonFactory
data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int = 0) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(TeamType.entries[stream.readUnsignedByte()], stream.readUnsignedShort())
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
stream.writeShort(team)
}
fun canDamage(victim: EntityDamageTeam, victimIsSelf: Boolean): Boolean {
if (victimIsSelf) {
return type == TeamType.INDISCRIMINATE
}
return when (type) {
TeamType.NULL -> false
TeamType.FRIENDLY -> victim.type in damageableByFriendly
TeamType.ENEMY -> victim.type in damageableByEnemy || victim.type == TeamType.ENEMY && team != victim.team
TeamType.PVP -> victim.type in damageableByFriendly || victim.type == TeamType.PVP && (team == 0 || team != victim.team)
TeamType.PASSIVE -> false // never deal damage
TeamType.GHOSTLY -> false // never deal damage
TeamType.ENVIRONMENT -> victim.type in damageableByEnvironment
TeamType.INDISCRIMINATE -> victim.type != TeamType.GHOSTLY
TeamType.ASSISTANT -> victim.type in damageableByFriendly
}
}
companion object {
private val damageableByFriendly = EnumSet.noneOf(TeamType::class.java)
private val damageableByEnemy = EnumSet.noneOf(TeamType::class.java)
private val damageableByEnvironment = EnumSet.noneOf(TeamType::class.java)
init {
damageableByFriendly.add(TeamType.ENEMY)
damageableByFriendly.add(TeamType.PASSIVE)
damageableByFriendly.add(TeamType.ENVIRONMENT)
damageableByFriendly.add(TeamType.INDISCRIMINATE)
damageableByEnemy.add(TeamType.FRIENDLY)
damageableByEnemy.add(TeamType.PVP)
damageableByEnemy.add(TeamType.INDISCRIMINATE)
damageableByEnvironment.add(TeamType.FRIENDLY)
damageableByEnvironment.add(TeamType.PVP)
damageableByEnvironment.add(TeamType.INDISCRIMINATE)
}
val NULL = EntityDamageTeam()
val FRIENDLY = EntityDamageTeam(TeamType.FRIENDLY)
val PASSIVE = EntityDamageTeam(TeamType.PASSIVE)
val CODEC = nativeCodec(::EntityDamageTeam, EntityDamageTeam::write)
val LEGACY_CODEC = legacyCodec(::EntityDamageTeam, EntityDamageTeam::write)
}
}
@JsonFactory
data class TouchDamage(
val poly: ImmutableList<Vector2d> = ImmutableList.of(),
val teamType: TeamType = TeamType.ENVIRONMENT,
val damage: Double = 0.0,
val damageSourceKind: String = "",
val knockback: Double = 0.0,
val statusEffects: ImmutableSet<String> = ImmutableSet.of(),
) {
/**
* new protocol only
*/
constructor(stream: DataInputStream) : this(
ImmutableList.copyOf(stream.readCollection { readVector2d() }),
TeamType.entries[stream.readUnsignedByte()],
stream.readDouble(),
stream.readInternedString(),
stream.readDouble(),
ImmutableSet.copyOf(stream.readCollection { readInternedString() })
)
/**
* new protocol only
*/
fun write(stream: DataOutputStream) {
stream.writeCollection(poly) { writeStruct2d(it) }
stream.writeByte(teamType.ordinal)
stream.writeDouble(damage)
stream.writeBinaryString(damageSourceKind)
stream.writeDouble(knockback)
stream.writeCollection(statusEffects) { writeBinaryString(it) }
}
companion object {
val EMPTY = TouchDamage()
}
}
@JsonFactory
data class DamageNotification(
val sourceEntityId: Int,
val targetEntityId: Int,
val position: Vector2d,
val damageDealt: Double,
val healthLost: Double,
val hitType: HitType,
val damageSourceKind: String,
val targetMaterialKind: String
) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
if (isLegacy) stream.readSignedVarInt() else stream.readInt(),
if (isLegacy) stream.readSignedVarInt() else stream.readInt(),
Vector2d(stream.readDouble(0.01, isLegacy), stream.readDouble(0.01, isLegacy)),
stream.readDouble(isLegacy),
stream.readDouble(isLegacy),
HitType.entries[stream.readEnumStupid(isLegacy)],
stream.readInternedString(),
stream.readInternedString(),
)
fun isAttacker(entity: AbstractEntity): Boolean = entity.entityID == sourceEntityId
fun isVictim(entity: AbstractEntity): Boolean = entity.entityID == targetEntityId
fun write(stream: DataOutputStream, isLegacy: Boolean) {
if (isLegacy) stream.writeSignedVarInt(sourceEntityId) else stream.writeInt(sourceEntityId)
if (isLegacy) stream.writeSignedVarInt(targetEntityId) else stream.writeInt(targetEntityId)
stream.writeDouble(position.x, 0.01, isLegacy)
stream.writeDouble(position.y, 0.01, isLegacy)
stream.writeDouble(damageDealt, isLegacy)
stream.writeDouble(healthLost, isLegacy)
stream.writeEnumStupid(hitType.ordinal, isLegacy)
stream.writeBinaryString(damageSourceKind)
stream.writeBinaryString(targetMaterialKind)
}
}
@JsonFactory
data class DamageData(
val hitType: HitType,
val damageType: DamageType,
val damage: Double,
val knockbackMomentum: Vector2d,
val sourceEntityId: Int,
val inflictorEntityId: Int = 0,
val damageSourceKind: String,
val statusEffects: Collection<EphemeralStatusEffect>,
) {
constructor(stream: DataInputStream, isLegacy: Boolean, inflictorEntityId: Int) : this(
HitType.entries[stream.readEnumStupid(isLegacy)],
DamageType.entries[stream.readUnsignedByte()],
stream.readDouble(isLegacy),
stream.readVector2d(isLegacy),
stream.readInt(),
inflictorEntityId, // DamageData is written inside packets which specify inflictor by themselves
stream.readInternedString(),
stream.readCollection { EphemeralStatusEffect(stream, isLegacy) }
)
fun isAttacker(entity: AbstractEntity): Boolean = entity.entityID == sourceEntityId
fun isInflictor(entity: AbstractEntity): Boolean = entity.entityID == inflictorEntityId
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeEnumStupid(hitType.ordinal, isLegacy)
stream.writeByte(damageType.ordinal)
stream.writeDouble(damage, isLegacy)
stream.writeStruct2d(knockbackMomentum, isLegacy)
stream.writeInt(sourceEntityId)
stream.writeBinaryString(damageSourceKind)
stream.writeCollection(statusEffects) { it.write(this, isLegacy) }
}
}
// this shit is a complete mess, because in original code DamageSource::toJson() method
// will create json structure which will not be readable by DamageSource's constructor
// (will always throw an exception)
@JsonAdapter(DamageSource.Adapter::class)
data class DamageSource(
val damageType: DamageType,
val damageArea: Either<Poly, Line2d>,
val damage: Double,
val trackSourceEntity: Boolean,
val sourceEntityId: Int = 0,
val team: EntityDamageTeam = EntityDamageTeam.PASSIVE,
val damageRepeatGroup: String? = null,
val damageRepeatTimeout: Double? = null,
val damageSourceKind: String = "",
val statusEffects: ImmutableList<EphemeralStatusEffect> = ImmutableList.of(),
val knockback: Either<Double, Vector2d>,
val rayCheck: Boolean = false,
) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
DamageType.entries[stream.readUnsignedByte()],
stream.readMVariant2({ Poly.read(this, isLegacy) }, { Line2d(stream, isLegacy) }) ?: throw IllegalArgumentException("Empty MVariant damageArea"),
stream.readDouble(isLegacy),
stream.readBoolean(),
stream.readInt(),
EntityDamageTeam(stream, isLegacy),
stream.readNullableString(),
stream.readNullableDouble(isLegacy),
stream.readInternedString(),
ImmutableList.copyOf(stream.readCollection { EphemeralStatusEffect(stream, isLegacy) }),
stream.readMVariant2({ readDouble(isLegacy) }, { readVector2d(isLegacy) }) ?: throw IllegalArgumentException("Empty MVariant knockback"),
stream.readBoolean()
)
fun intersect(other: Poly): Boolean {
return damageArea.map({ it.intersect(other) != null }, { other.intersect(it) != null })
}
fun intersect(geometry: WorldGeometry, other: Poly): Boolean {
return damageArea.map({ geometry.polyIntersectsPoly(other, it) }, { geometry.lineIntersectsPoly(it, other) })
}
fun intersect(geometry: WorldGeometry, list: List<Poly>): Boolean {
return list.any { other ->
damageArea.map({ geometry.polyIntersectsPoly(other, it) }, { geometry.lineIntersectsPoly(it, other) })
}
}
operator fun plus(offset: Vector2d): DamageSource {
return copy(damageArea = damageArea.flatMap({ it + offset }, { it + offset }))
}
fun knockbackMomentum(geometry: WorldGeometry, targetCenter: Vector2d): Vector2d {
if (knockback.isRight) {
return knockback.right()
} else {
val knockback = knockback.left()
if (knockback == 0.0)
return Vector2d.ZERO
return damageArea.map({ geometry.diff(targetCenter, it.centre).unitVector * knockback }, { it.difference.unitVector * knockback })
}
}
data class JsonData(
val poly: Poly? = null,
val line: Line2d? = null,
val damage: Double,
val damageType: DamageType = DamageType.DAMAGE,
val trackSourceEntity: Boolean = true,
val sourceEntityId: Int = 0,
val teamType: TeamType = TeamType.PASSIVE,
val teamNumber: Int = 0,
val team: EntityDamageTeam? = null,
val damageRepeatGroup: String? = null,
val damageRepeatTimeout: Double? = null,
val damageSourceKind: String = "",
val statusEffects: ImmutableList<EphemeralStatusEffect> = ImmutableList.of(),
val knockback: Either<Double, Vector2d> = Either.left(0.0),
val rayCheck: Boolean = false,
)
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(damageType.ordinal)
stream.writeMVariant2(damageArea, { it.write(stream, isLegacy) }, { it.write(stream, isLegacy) })
stream.writeDouble(damage, isLegacy)
stream.writeBoolean(trackSourceEntity)
stream.writeInt(sourceEntityId)
team.write(stream, isLegacy)
stream.writeNullable(damageRepeatGroup, DataOutputStream::writeBinaryString)
stream.writeNullable(damageRepeatTimeout) { writeDouble(it, isLegacy) }
stream.writeBinaryString(damageSourceKind)
stream.writeCollection(statusEffects) { it.write(stream, isLegacy) }
stream.writeMVariant2(knockback, { writeDouble(it, isLegacy) }, { writeStruct2d(it, isLegacy) })
stream.writeBoolean(rayCheck)
}
class Adapter(gson: Gson) : TypeAdapter<DamageSource>() {
private val data = FactoryAdapter.createFor(JsonData::class, gson = gson)
override fun write(out: JsonWriter, value: DamageSource) {
data.write(out, JsonData(
poly = value.damageArea.left.orNull(),
line = value.damageArea.right.orNull(),
damage = value.damage,
damageType = value.damageType,
trackSourceEntity = value.trackSourceEntity,
sourceEntityId = value.sourceEntityId,
team = value.team,
damageRepeatGroup = value.damageRepeatGroup,
damageRepeatTimeout = value.damageRepeatTimeout,
damageSourceKind = value.damageSourceKind,
statusEffects = value.statusEffects,
knockback = value.knockback,
rayCheck = value.rayCheck,
))
}
override fun read(`in`: JsonReader): DamageSource {
val read = data.read(`in`)
return DamageSource(
damageType = read.damageType,
damageArea = if (read.line == null) Either.left(read.poly ?: throw JsonSyntaxException("Missing both 'line' and 'poly' from DamageSource json")) else Either.right(read.line),
damage = read.damage,
trackSourceEntity = read.trackSourceEntity,
sourceEntityId = read.sourceEntityId,
statusEffects = read.statusEffects,
team = read.team ?: EntityDamageTeam(read.teamType, read.teamNumber),
damageRepeatGroup = read.damageRepeatGroup,
damageRepeatTimeout = read.damageRepeatTimeout,
damageSourceKind = read.damageSourceKind,
knockback = read.knockback,
rayCheck = read.rayCheck,
)
}
}
companion object {
val CODEC = nativeCodec(::DamageSource, DamageSource::write)
val LEGACY_CODEC = legacyCodec(::DamageSource, DamageSource::write)
}
}
data class ElementalDamageType(val resistanceStat: String, val damageNumberParticles: ImmutableMap<HitType, String>)