extends Node const SYNCED_STATS: Array = [ "health", "shield", "max_health", "max_shield", "role", "level", "xp", "xp_to_next", "buff_damage", "buff_heal", "buff_shield", ] const SYNC_INTERVAL: float = 0.10 var _entities: Dictionary = {} var _player_cache: Dictionary = {} var _dirty: Dictionary = {} var _accum: float = 0.0 func _ready() -> void: set_process(true) func _process(delta: float) -> void: _accum += delta if _accum < SYNC_INTERVAL: return _accum = 0.0 if multiplayer.multiplayer_peer == null or multiplayer.multiplayer_peer is OfflineMultiplayerPeer or not multiplayer.is_server(): _dirty.clear() return if _dirty.is_empty(): return for entity in _dirty.keys(): if not is_instance_valid(entity) or not entity.is_inside_tree(): continue _sync_stats_batch.rpc(String(entity.get_path()), _dirty[entity]) _dirty.clear() @rpc("authority", "unreliable_ordered") func _sync_stats_batch(path_str: String, changes: Dictionary) -> void: var entity := get_node_or_null(NodePath(path_str)) if entity == null or not entity in _entities: return for stat in changes.keys(): _entities[entity][stat] = changes[stat] match stat: "health": EventBus.health_changed.emit(entity, changes[stat], _entities[entity].get("max_health", 100.0)) "shield": EventBus.shield_changed.emit(entity, changes[stat], _entities[entity].get("max_shield", 0.0)) "role": EventBus.role_changed.emit(entity, changes[stat]) "level": EventBus.level_up.emit(entity, changes[stat]) "buff_damage", "buff_heal", "buff_shield": EventBus.buff_changed.emit(entity, StringName(stat), changes[stat]) func register(entity: Node, base_resource: Resource) -> void: if not is_instance_valid(entity): return var data: Dictionary = {} for prop in base_resource.get_property_list(): var name: String = prop.name if not (prop.usage & PROPERTY_USAGE_STORAGE): continue if name in ["resource_local_to_scene", "resource_path", "resource_name", "resource_scene_unique_id", "script"]: continue data[name] = base_resource.get(name) data["health"] = data.get("max_health", 100.0) data["shield"] = data.get("max_shield", 0.0) data["shield_regen_timer"] = 0.0 data["base_resource"] = base_resource _entities[entity] = data EventBus.entity_registered.emit(entity) func deregister(entity: Node) -> void: if entity in _entities: _entities.erase(entity) EventBus.entity_deregistered.emit(entity) func has(entity: Node) -> bool: return entity in _entities func get_stat(entity: Node, stat: String, default: Variant = 0.0) -> Variant: if entity in _entities: return _entities[entity].get(stat, default) return default func set_stat(entity: Node, stat: String, value: Variant) -> void: if not entity in _entities: return var prev: Variant = _entities[entity].get(stat) _entities[entity][stat] = value if stat in SYNCED_STATS and prev != value and multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.is_server() and is_instance_valid(entity) and entity.is_inside_tree(): if not entity in _dirty: _dirty[entity] = {} _dirty[entity][stat] = value func get_all(entity: Node) -> Dictionary: return _entities.get(entity, {}) func entities() -> Array: return _entities.keys() func entities_in_group(group: StringName) -> Array: var out: Array = [] for e in _entities.keys(): if is_instance_valid(e) and e.is_in_group(group): out.append(e) return out func cache_player(peer_id: int, entity: Node) -> void: if entity in _entities: _player_cache[peer_id] = _entities[entity].duplicate(true) func restore_player(peer_id: int, entity: Node) -> void: if peer_id in _player_cache: _entities[entity] = _player_cache[peer_id].duplicate(true) func clear_player_cache(peer_id: int = -1) -> void: if peer_id == -1: _player_cache.clear() else: _player_cache.erase(peer_id) func clear_all() -> void: _entities.clear() _player_cache.clear()