406 lines
15 KiB
Kotlin
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>)
|