272 lines
7.2 KiB
GDScript
272 lines
7.2 KiB
GDScript
class_name CombatantStateRecord
|
|
extends RefCounted
|
|
|
|
signal changed(state: CombatantStateRecord)
|
|
|
|
const SCHEMA_VERSION := 2
|
|
const LEGACY_SCHEMA_VERSION := 1
|
|
const NO_NPC_ID := -1
|
|
const NO_ENEMY_DEFINITION_ID := &""
|
|
const LEGACY_RAIDER_DEFINITION_ID := &"enemy_raider"
|
|
const LEGACY_WOLF_DEFINITION_ID := &"enemy_wolf"
|
|
|
|
var data: Dictionary
|
|
|
|
|
|
func _init(record_data: Dictionary = {}) -> void:
|
|
data = record_data.duplicate(true)
|
|
|
|
|
|
static func _snap(value: float) -> float:
|
|
return snappedf(value, 1.0 / 1024.0)
|
|
|
|
|
|
static func create(
|
|
combatant_id: StringName,
|
|
kind: StringName,
|
|
faction_id: StringName,
|
|
display_name: String,
|
|
position: Vector3,
|
|
max_health: float,
|
|
weapon_id: StringName,
|
|
npc_id: int = NO_NPC_ID,
|
|
hostile: bool = false,
|
|
enemy_definition_id: StringName = NO_ENEMY_DEFINITION_ID
|
|
) -> CombatantStateRecord:
|
|
var snapped_position := Vector3(_snap(position.x), _snap(position.y), _snap(position.z))
|
|
var canonical_enemy_definition_id := enemy_definition_id
|
|
if canonical_enemy_definition_id.is_empty():
|
|
canonical_enemy_definition_id = _legacy_definition_id_for_kind(kind)
|
|
return (
|
|
CombatantStateRecord
|
|
. new(
|
|
{
|
|
"schema_version": SCHEMA_VERSION,
|
|
"combatant_id": String(combatant_id),
|
|
"kind": String(kind),
|
|
"faction_id": String(faction_id),
|
|
"display_name": display_name,
|
|
"npc_id": npc_id,
|
|
"enemy_definition_id": String(canonical_enemy_definition_id),
|
|
"position": [snapped_position.x, snapped_position.y, snapped_position.z],
|
|
"health": max_health,
|
|
"max_health": max_health,
|
|
"weapon_id": String(weapon_id),
|
|
"alive": true,
|
|
"hostile": hostile,
|
|
"attack_cooldown": 0,
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
static func from_dictionary(record_data: Dictionary) -> CombatantStateRecord:
|
|
var version := int(record_data.get("schema_version", -1))
|
|
var normalized := record_data.duplicate(true)
|
|
if version == LEGACY_SCHEMA_VERSION:
|
|
normalized["schema_version"] = SCHEMA_VERSION
|
|
normalized["enemy_definition_id"] = String(
|
|
_legacy_definition_id_for_kind(StringName(normalized.get("kind", &"")))
|
|
)
|
|
elif version != SCHEMA_VERSION:
|
|
return null
|
|
if not (
|
|
normalized
|
|
. has_all(
|
|
[
|
|
"combatant_id",
|
|
"kind",
|
|
"faction_id",
|
|
"display_name",
|
|
"npc_id",
|
|
"enemy_definition_id",
|
|
"position",
|
|
"health",
|
|
"max_health",
|
|
"weapon_id",
|
|
"alive",
|
|
"hostile",
|
|
"attack_cooldown",
|
|
]
|
|
)
|
|
):
|
|
return null
|
|
var combatant_id := String(normalized["combatant_id"])
|
|
var display_name := String(normalized["display_name"])
|
|
if combatant_id.is_empty() or display_name.is_empty():
|
|
return null
|
|
if (
|
|
StringName(normalized["kind"])
|
|
not in [
|
|
SimulationIds.COMBATANT_KIND_NPC,
|
|
SimulationIds.COMBATANT_KIND_RAIDER,
|
|
SimulationIds.COMBATANT_KIND_WOLF,
|
|
SimulationIds.COMBATANT_KIND_PLAYER,
|
|
]
|
|
):
|
|
return null
|
|
var kind := StringName(normalized["kind"])
|
|
var enemy_definition_id := StringName(normalized["enemy_definition_id"])
|
|
var is_enemy := kind in [SimulationIds.COMBATANT_KIND_RAIDER, SimulationIds.COMBATANT_KIND_WOLF]
|
|
if is_enemy == enemy_definition_id.is_empty():
|
|
return null
|
|
var saved_position = normalized["position"]
|
|
if not saved_position is Array or saved_position.size() != 3:
|
|
return null
|
|
for component in saved_position:
|
|
if not is_finite(float(component)):
|
|
return null
|
|
var health := float(normalized["health"])
|
|
var max_health := float(normalized["max_health"])
|
|
var npc_id := int(normalized["npc_id"])
|
|
if (
|
|
not is_finite(health)
|
|
or not is_finite(max_health)
|
|
or health < 0.0
|
|
or max_health <= 0.0
|
|
or health > max_health
|
|
or npc_id < NO_NPC_ID
|
|
or int(normalized["attack_cooldown"]) < 0
|
|
):
|
|
return null
|
|
normalized["schema_version"] = SCHEMA_VERSION
|
|
normalized["combatant_id"] = combatant_id
|
|
normalized["kind"] = String(kind)
|
|
normalized["faction_id"] = String(normalized["faction_id"])
|
|
normalized["display_name"] = display_name
|
|
normalized["npc_id"] = npc_id
|
|
normalized["enemy_definition_id"] = String(enemy_definition_id)
|
|
normalized["position"] = [
|
|
_snap(float(saved_position[0])),
|
|
_snap(float(saved_position[1])),
|
|
_snap(float(saved_position[2]))
|
|
]
|
|
normalized["health"] = health
|
|
normalized["max_health"] = max_health
|
|
normalized["weapon_id"] = String(normalized["weapon_id"])
|
|
normalized["alive"] = bool(normalized["alive"])
|
|
normalized["hostile"] = bool(normalized["hostile"])
|
|
normalized["attack_cooldown"] = int(normalized["attack_cooldown"])
|
|
return CombatantStateRecord.new(normalized)
|
|
|
|
|
|
func get_combatant_id() -> StringName:
|
|
return StringName(data["combatant_id"])
|
|
|
|
|
|
func get_kind() -> StringName:
|
|
return StringName(data["kind"])
|
|
|
|
|
|
func get_faction_id() -> StringName:
|
|
return StringName(data["faction_id"])
|
|
|
|
|
|
func get_display_name() -> String:
|
|
return String(data["display_name"])
|
|
|
|
|
|
func get_npc_id() -> int:
|
|
return int(data["npc_id"])
|
|
|
|
|
|
func get_enemy_definition_id() -> StringName:
|
|
return StringName(data["enemy_definition_id"])
|
|
|
|
|
|
func get_position() -> Vector3:
|
|
var saved_position: Array = data["position"]
|
|
return Vector3(float(saved_position[0]), float(saved_position[1]), float(saved_position[2]))
|
|
|
|
|
|
func get_health() -> float:
|
|
return float(data["health"])
|
|
|
|
|
|
func get_max_health() -> float:
|
|
return float(data["max_health"])
|
|
|
|
|
|
func get_weapon_id() -> StringName:
|
|
return StringName(data["weapon_id"])
|
|
|
|
|
|
func is_alive() -> bool:
|
|
return bool(data["alive"]) and get_health() > 0.0
|
|
|
|
|
|
func is_hostile() -> bool:
|
|
return bool(data["hostile"])
|
|
|
|
|
|
func get_attack_cooldown() -> int:
|
|
return int(data["attack_cooldown"])
|
|
|
|
|
|
func is_attack_ready() -> bool:
|
|
return is_alive() and get_attack_cooldown() <= 0
|
|
|
|
|
|
func set_position(value: Vector3) -> void:
|
|
if not value.is_finite() or value.is_equal_approx(get_position()):
|
|
return
|
|
data["position"] = [_snap(value.x), _snap(value.y), _snap(value.z)]
|
|
changed.emit(self)
|
|
|
|
|
|
func set_hostile(value: bool) -> void:
|
|
if value == is_hostile():
|
|
return
|
|
data["hostile"] = value
|
|
changed.emit(self)
|
|
|
|
|
|
func set_health_and_alive(value: float, alive: bool) -> void:
|
|
if not is_finite(value):
|
|
return
|
|
var next_health := clampf(value, 0.0, get_max_health()) if alive else 0.0
|
|
var next_alive := alive and next_health > 0.0
|
|
if is_equal_approx(next_health, get_health()) and bool(data["alive"]) == next_alive:
|
|
return
|
|
data["health"] = next_health
|
|
data["alive"] = next_alive
|
|
changed.emit(self)
|
|
|
|
|
|
func tick_cooldown() -> void:
|
|
if get_attack_cooldown() > 0:
|
|
data["attack_cooldown"] = get_attack_cooldown() - 1
|
|
|
|
|
|
func take_damage(amount: float) -> float:
|
|
if not is_alive() or not is_finite(amount) or amount <= 0.0:
|
|
return 0.0
|
|
var previous := get_health()
|
|
data["health"] = maxf(previous - amount, 0.0)
|
|
if get_health() <= 0.0:
|
|
data["alive"] = false
|
|
changed.emit(self)
|
|
return previous - get_health()
|
|
|
|
|
|
func mark_attacked(tick_interval: float = 1.2) -> void:
|
|
data["attack_cooldown"] = _weapon_cooldown_ticks(tick_interval)
|
|
|
|
|
|
func _weapon_cooldown_ticks(tick_interval: float) -> int:
|
|
var weapon := SimulationItems.get_weapon(get_weapon_id())
|
|
var seconds := weapon.attack_cooldown if weapon != null else 0.6
|
|
return maxi(int(ceil(seconds / maxf(tick_interval, 0.0001))), 1)
|
|
|
|
|
|
func to_dictionary() -> Dictionary:
|
|
return data.duplicate(true)
|
|
|
|
|
|
static func _legacy_definition_id_for_kind(kind: StringName) -> StringName:
|
|
if kind == SimulationIds.COMBATANT_KIND_RAIDER:
|
|
return LEGACY_RAIDER_DEFINITION_ID
|
|
if kind == SimulationIds.COMBATANT_KIND_WOLF:
|
|
return LEGACY_WOLF_DEFINITION_ID
|
|
return NO_ENEMY_DEFINITION_ID
|