This commit is contained in:
Marek Le
2026-05-09 23:37:26 +02:00
parent 6d28b04c12
commit 2d4002bd3f
263 changed files with 5250 additions and 4597 deletions

View File

@@ -1,82 +0,0 @@
extends Node
var entities: Dictionary = {}
func register(entity: Node, base: EnemyStats, scale: float = 1.0) -> void:
var max_hp: float = base.max_health * scale
var max_sh: float = base.max_shield * scale
entities[entity] = {
"base": base,
"scale": scale,
"health": max_hp,
"max_health": max_hp,
"health_regen": base.health_regen * scale,
"shield": max_sh,
"max_shield": max_sh,
"shield_regen_delay": base.shield_regen_delay,
"shield_regen_time": base.shield_regen_time,
"shield_regen_timer": 0.0,
"alive": true,
"buff_damage": 1.0,
"buff_heal": 1.0,
"buff_shield": 1.0,
"state": 0,
"target": null,
"spawn_position": Vector3.ZERO,
"portal": null,
"attack_timer": 0.0,
}
func apply_scale(entity: Node, scale: float) -> void:
if entity not in entities:
return
var data: Dictionary = entities[entity]
var base: EnemyStats = data["base"]
data["scale"] = scale
data["max_health"] = base.max_health * scale
data["health"] = data["max_health"]
data["health_regen"] = base.health_regen * scale
data["max_shield"] = base.max_shield * scale
data["shield"] = data["max_shield"]
EventBus.health_changed.emit(entity, data["health"], data["max_health"])
if base.max_shield > 0:
EventBus.shield_changed.emit(entity, data["shield"], data["max_shield"])
func deregister(entity: Node) -> void:
entities.erase(entity)
func get_stat(entity: Node, key: String) -> Variant:
if entity in entities:
return entities[entity].get(key)
return null
func set_stat(entity: Node, key: String, value: Variant) -> void:
if entity in entities:
entities[entity][key] = value
func get_base(entity: Node) -> EnemyStats:
if entity in entities:
return entities[entity]["base"]
return null
func is_alive(entity: Node) -> bool:
if entity in entities:
return entities[entity]["alive"]
return false
func set_health(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["health"] = value
var max_health: float = entities[entity]["max_health"]
EventBus.health_changed.emit(entity, value, max_health)
if value <= 0 and entities[entity]["alive"]:
entities[entity]["alive"] = false
EventBus.entity_died.emit(entity)
func set_shield(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["shield"] = value
var max_shield: float = entities[entity]["max_shield"]
EventBus.shield_changed.emit(entity, value, max_shield)

View File

@@ -1 +0,0 @@
uid://dbr02t7pt4vcn

View File

@@ -1,82 +0,0 @@
extends Node
var entities: Dictionary = {}
func register(entity: Node, base: EnemyStats, scale: float = 1.0) -> void:
var max_hp: float = base.max_health * scale
var max_sh: float = base.max_shield * scale
entities[entity] = {
"base": base,
"scale": scale,
"health": max_hp,
"max_health": max_hp,
"health_regen": base.health_regen * scale,
"shield": max_sh,
"max_shield": max_sh,
"shield_regen_delay": base.shield_regen_delay,
"shield_regen_time": base.shield_regen_time,
"shield_regen_timer": 0.0,
"alive": true,
"buff_damage": 1.0,
"buff_heal": 1.0,
"buff_shield": 1.0,
"state": 0,
"target": null,
"spawn_position": Vector3.ZERO,
"portal": null,
"attack_timer": 0.0,
}
func apply_scale(entity: Node, scale: float) -> void:
if entity not in entities:
return
var data: Dictionary = entities[entity]
var base: EnemyStats = data["base"]
data["scale"] = scale
data["max_health"] = base.max_health * scale
data["health"] = data["max_health"]
data["health_regen"] = base.health_regen * scale
data["max_shield"] = base.max_shield * scale
data["shield"] = data["max_shield"]
EventBus.health_changed.emit(entity, data["health"], data["max_health"])
if base.max_shield > 0:
EventBus.shield_changed.emit(entity, data["shield"], data["max_shield"])
func deregister(entity: Node) -> void:
entities.erase(entity)
func get_stat(entity: Node, key: String) -> Variant:
if entity in entities:
return entities[entity].get(key)
return null
func set_stat(entity: Node, key: String, value: Variant) -> void:
if entity in entities:
entities[entity][key] = value
func get_base(entity: Node) -> EnemyStats:
if entity in entities:
return entities[entity]["base"]
return null
func is_alive(entity: Node) -> bool:
if entity in entities:
return entities[entity]["alive"]
return false
func set_health(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["health"] = value
var max_health: float = entities[entity]["max_health"]
EventBus.health_changed.emit(entity, value, max_health)
if value <= 0 and entities[entity]["alive"]:
entities[entity]["alive"] = false
EventBus.entity_died.emit(entity)
func set_shield(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["shield"] = value
var max_shield: float = entities[entity]["max_shield"]
EventBus.shield_changed.emit(entity, value, max_shield)

View File

@@ -1 +0,0 @@
uid://bvxn6y15tvidu

View File

@@ -1,73 +1,60 @@
extends Node
# Intentionen (Input → System)
signal ability_use(player, ability_index)
signal role_change_requested(player, role)
signal target_requested(player, target)
signal enemy_detected(enemy, player)
signal enemy_lost(enemy, player)
signal portal_entered(portal, player)
signal entity_registered(entity: Node)
signal entity_deregistered(entity: Node)
# Kampf
signal attack_executed(attacker, position, direction, damage)
signal damage_dealt(attacker, target, damage)
signal damage_requested(attacker, target, amount)
signal heal_requested(healer, target, amount)
signal damage_requested(attacker: Node, target: Node, amount: float, element: int)
signal heal_requested(healer: Node, target: Node, amount: float)
signal damage_dealt(attacker: Node, target: Node, amount: float)
signal entity_died(entity: Node)
signal entity_respawned(entity: Node)
signal health_changed(entity: Node, current: float, max: float)
signal shield_changed(entity: Node, current: float, max: float)
signal shield_broken(entity: Node)
# Entity
signal entity_died(entity)
signal health_changed(entity, current, max_val)
signal shield_changed(entity, current, max_val)
signal shield_broken(entity)
signal shield_regenerated(entity)
signal ability_use_requested(player: Node, ability_index: int)
signal ability_used(player: Node, ability_index: int, ability: Resource)
signal cooldown_tick(entity: Node, cds: PackedFloat32Array, max_cds: PackedFloat32Array, gcd: float)
signal role_changed(player: Node, role: int)
signal target_changed(player: Node, target: Node)
# Spieler
signal target_changed(player, target)
signal player_respawned(player)
signal role_changed(player, role_type)
signal respawn_tick(timer)
signal cooldown_tick(cooldowns, max_cooldowns, gcd_timer)
signal effect_applied(target: Node, effect: Resource, source: Node)
signal effect_expired(target: Node, effect: Resource)
signal element_applied(target: Node, element: int)
signal buff_changed(entity: Node, stat: StringName, value: float)
# Buff
signal buff_changed(entity, stat, value)
signal enemy_detected(enemy: Node, player: Node)
signal enemy_lost(enemy: Node, player: Node)
signal enemy_engaged(enemy: Node, target: Node)
# Gegner
signal enemy_engaged(enemy, target)
signal gate_destroyed(gate: Node)
signal portal_spawned(portal: Node)
signal portal_entered(portal: Node, player: Node)
signal dungeon_cleared(seed: int)
signal boss_defeated(boss: Node)
# Portal
signal portal_spawn(portal, enemies)
signal portal_defeated(portal)
signal loot_dropped(items: Array, position: Vector3)
signal item_picked_up(player: Node, item: Resource)
signal inventory_changed(player: Node)
signal item_crafted(player: Node, item: Resource)
signal building_placed(building: Node)
signal building_removed(building: Node)
# Dungeon
signal dungeon_cleared()
# Effects
signal effect_requested(target, effect, source)
signal effect_applied(target, effect)
signal effect_expired(target, effect)
# Elements
signal element_damage_dealt(attacker, target, amount, element)
signal element_applied(target, element)
signal element_reaction(target, element_a, element_b, reaction_name)
# Wave
signal run_started(wave_number)
signal wave_started(wave_number)
signal wave_timer_tick(seconds_remaining)
signal wave_ended(wave_number, success)
# XP / Level
signal xp_gained(player, amount)
signal level_up(player, new_level)
# Taverne
signal tavern_damaged(current, max_val)
signal tavern_destroyed()
# Invasion
signal invasion_started(enemies)
signal invasion_ended(success)
# Game-Over
signal wave_started(wave_number: int)
signal wave_timer_tick(seconds_remaining: float)
signal wave_ended(wave_number: int, success: bool)
signal invasion_started()
signal invasion_ended(success: bool)
signal village_damaged(current: float, max: float)
signal village_destroyed()
signal game_over()
signal run_started(wave_number: int)
signal xp_gained(player: Node, amount: float)
signal level_up(player: Node, new_level: int)
signal dialog_opened(player: Node, npc: Node)
signal dialog_closed(player: Node)
signal chat_message(peer_id: int, sender_name: String, text: String)
signal scene_change_requested(scene_path: String)

View File

@@ -1 +1 @@
uid://g7a7xkg1pgb4
uid://361x7bdk2j6v

View File

@@ -1,19 +1,36 @@
extends Node
# Run-Zustand
const ROLE_TANK: int = 0
const ROLE_DAMAGE: int = 1
const ROLE_HEALER: int = 2
const SCENE_MAIN_MENU: String = "res://scenes/menu/main_menu.tscn"
const SCENE_LOBBY: String = "res://scenes/menu/lobby.tscn"
const SCENE_WORLD: String = "res://scenes/world/world.tscn"
const SCENE_DUNGEON: String = "res://scenes/dungeon/dungeon.tscn"
const SCENE_OPTIONS: String = "res://scenes/menu/options_menu.tscn"
var current_scene: String = SCENE_MAIN_MENU
var paused: bool = false
var run_seed: int = 0
var dungeon_seed: int = 0
var dungeon_red: bool = false
var current_wave: int = 1
var wave_timer_remaining: float = 0.0
var run_initialized: bool = false
var portal_return_position: Vector3 = Vector3.ZERO
# Dungeon-Kontext (für XP-Zuordnung nach Clear)
var last_dungeon_variant: int = 0
# Flag für Forced Return (Timer läuft ab während Spieler im Dungeon)
var force_return_to_world: bool = false
func reset() -> void:
func reset_run() -> void:
run_seed = randi()
current_wave = 1
wave_timer_remaining = 0.0
run_initialized = false
last_dungeon_variant = 0
force_return_to_world = false
dungeon_seed = 0
dungeon_red = false
paused = false
Stats.clear_all()
func change_scene(path: String) -> void:
current_scene = path
EventBus.scene_change_requested.emit(path)
get_tree().change_scene_to_file(path)
func set_paused(value: bool) -> void:
paused = value
get_tree().paused = value

View File

@@ -1 +1 @@
uid://c3jq4raqs0onf
uid://dettmu50fjtvc

148
autoloads/net.gd Normal file
View File

@@ -0,0 +1,148 @@
extends Node
const DEFAULT_PORT: int = 7777
const MAX_PLAYERS: int = 4
signal peer_connected(id: int)
signal peer_disconnected(id: int)
signal connected_to_server()
signal connection_failed()
signal server_disconnected()
signal world_ready()
signal peer_world_loaded(peer_id: int)
var player_names: Dictionary = {}
var local_name: String = "Player"
var _expected_peers: Array = []
var _ready_peers: Array = []
func _ready() -> void:
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
multiplayer.connected_to_server.connect(_on_connected_to_server)
multiplayer.connection_failed.connect(_on_connection_failed)
multiplayer.server_disconnected.connect(_on_server_disconnected)
func host(port: int = DEFAULT_PORT, max_clients: int = MAX_PLAYERS - 1) -> bool:
var peer := ENetMultiplayerPeer.new()
var err := peer.create_server(port, max_clients)
if err != OK:
push_error("Net: failed to host on port %d (err=%d)" % [port, err])
return false
multiplayer.multiplayer_peer = peer
player_names[1] = local_name
return true
func join(address: String, port: int = DEFAULT_PORT) -> bool:
var peer := ENetMultiplayerPeer.new()
var err := peer.create_client(address, port)
if err != OK:
push_error("Net: failed to join %s:%d (err=%d)" % [address, port, err])
return false
multiplayer.multiplayer_peer = peer
return true
func host_singleplayer() -> bool:
if multiplayer.multiplayer_peer != null:
multiplayer.multiplayer_peer.close()
var peer := OfflineMultiplayerPeer.new()
multiplayer.multiplayer_peer = peer
player_names.clear()
player_names[1] = local_name
return true
func disconnect_net() -> void:
if multiplayer.multiplayer_peer != null:
multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = null
player_names.clear()
func is_host() -> bool:
return multiplayer.multiplayer_peer == null or multiplayer.is_server()
func is_authority_for(node: Node) -> bool:
return node.is_multiplayer_authority()
func local_id() -> int:
if multiplayer.multiplayer_peer == null:
return 1
return multiplayer.get_unique_id()
func _on_peer_connected(id: int) -> void:
peer_connected.emit(id)
if is_host():
_register_name.rpc_id(id, 1, local_name)
func _on_peer_disconnected(id: int) -> void:
player_names.erase(id)
peer_disconnected.emit(id)
func _on_connected_to_server() -> void:
player_names[1] = "Host"
player_names[multiplayer.get_unique_id()] = local_name
_register_name.rpc(multiplayer.get_unique_id(), local_name)
connected_to_server.emit()
func _on_connection_failed() -> void:
multiplayer.multiplayer_peer = null
connection_failed.emit()
func _on_server_disconnected() -> void:
multiplayer.multiplayer_peer = null
player_names.clear()
server_disconnected.emit()
@rpc("any_peer", "reliable", "call_local")
func _register_name(peer_id: int, name: String) -> void:
player_names[peer_id] = name
func mark_world_loaded() -> void:
if multiplayer.multiplayer_peer == null:
_expected_peers = [1]
_ready_peers = [1]
world_ready.emit()
return
if is_host():
_expected_peers = [1]
for p in multiplayer.get_peers():
_expected_peers.append(p)
_ready_peers = [1]
if _all_ready():
_broadcast_ready.rpc()
else:
_client_world_loaded.rpc_id(1, multiplayer.get_unique_id())
@rpc("any_peer", "reliable")
func _client_world_loaded(peer_id: int) -> void:
if not is_host():
return
if not peer_id in _expected_peers:
_expected_peers.append(peer_id)
if not peer_id in _ready_peers:
_ready_peers.append(peer_id)
peer_world_loaded.emit(peer_id)
if _all_ready():
_broadcast_ready.rpc()
@rpc("authority", "reliable")
func _load_scene_for_peer(path: String) -> void:
GameState.change_scene(path)
func tell_peer_to_load_scene(peer_id: int, path: String) -> void:
if not is_host():
return
_load_scene_for_peer.rpc_id(peer_id, path)
@rpc("authority", "reliable", "call_local")
func _broadcast_ready() -> void:
world_ready.emit()
func _all_ready() -> bool:
for p in _expected_peers:
if not p in _ready_peers:
return false
return true
func reset_world_ready() -> void:
_expected_peers.clear()
_ready_peers.clear()

1
autoloads/net.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://1k1cufc2skfr

View File

@@ -1,195 +0,0 @@
extends Node
enum Role { TANK, DAMAGE, HEALER }
# Basis (aus Resource geladen)
var base: PlayerStats
var speed := 5.0
var jump_velocity := 4.5
var target_range := 20.0
var combat_timeout := 3.0
var respawn_time := 3.0
var gcd_time := 0.5
var aa_cooldown := 0.5
# Laufzeit
var health := 100.0
var max_health := 100.0
var health_regen := 0.0
var shield := 0.0
var max_shield := 0.0
var shield_regen_delay := 3.0
var shield_regen_time := 5.0
var shield_regen_timer := 0.0
var alive := true
# Buffs
var buff_damage := 1.0
var buff_heal := 1.0
var buff_shield := 1.0
# Level / XP
const XP_PER_LEVEL: int = 50
var level: int = 1
var xp: int = 0
var xp_to_next: int = XP_PER_LEVEL
var level_scale: float = 1.0
# Rolle
var current_role: int = Role.DAMAGE
var ability_set: AbilitySet = null
# Kampf
var target: Node3D = null
var in_combat := false
var combat_timer := 0.0
# Cooldowns
var cooldowns: Array[float] = []
var max_cooldowns: Array[float] = []
var gcd := 0.0
var aa_timer := 0.0
# Szenenwechsel
var portal_position := Vector3.ZERO
var returning_from_dungeon := false
var dungeon_cleared := false
# Cache für Szenenwechsel
var _cache: Dictionary = {}
func init_from_resource(res: PlayerStats) -> void:
base = res
speed = res.speed
jump_velocity = res.jump_velocity
target_range = res.target_range
combat_timeout = res.combat_timeout
respawn_time = res.respawn_time
gcd_time = res.gcd_time
aa_cooldown = res.aa_cooldown
if _cache.is_empty():
max_health = res.max_health * level_scale
health = max_health
health_regen = res.health_regen * level_scale
max_shield = res.max_shield * level_scale
shield = max_shield
shield_regen_delay = res.shield_regen_delay
shield_regen_time = res.shield_regen_time
shield_regen_timer = 0.0
alive = true
buff_damage = 1.0
buff_heal = 1.0
buff_shield = 1.0
else:
_restore_cache()
cooldowns.resize(5)
cooldowns.fill(0.0)
max_cooldowns.resize(5)
max_cooldowns.fill(0.0)
gcd = 0.0
aa_timer = 0.0
func set_health(value: float) -> void:
health = value
EventBus.health_changed.emit(self, health, max_health)
if health <= 0 and alive:
alive = false
EventBus.entity_died.emit(self)
func set_shield(value: float) -> void:
shield = value
EventBus.shield_changed.emit(self, shield, max_shield)
func set_role(role: int) -> void:
current_role = role
EventBus.role_changed.emit(self, current_role)
func set_target(new_target: Node3D) -> void:
target = new_target
EventBus.target_changed.emit(self, target)
func respawn() -> void:
health = max_health
shield = max_shield
alive = true
EventBus.health_changed.emit(self, health, max_health)
EventBus.shield_changed.emit(self, shield, max_shield)
EventBus.player_respawned.emit(self)
func save_cache() -> void:
_cache = {
"health": health,
"max_health": max_health,
"health_regen": health_regen,
"shield": shield,
"max_shield": max_shield,
"shield_regen_delay": shield_regen_delay,
"shield_regen_time": shield_regen_time,
"alive": alive,
"buff_damage": buff_damage,
"buff_heal": buff_heal,
"buff_shield": buff_shield,
}
func clear_cache() -> void:
_cache.clear()
portal_position = Vector3.ZERO
returning_from_dungeon = false
dungeon_cleared = false
func reset_run() -> void:
clear_cache()
level = 1
xp = 0
xp_to_next = XP_PER_LEVEL
level_scale = 1.0
func add_xp(amount: int) -> void:
xp += amount
EventBus.xp_gained.emit(self, amount)
while xp >= xp_to_next:
xp -= xp_to_next
level_up()
func level_up() -> void:
level += 1
level_scale = float(_fibonacci(level))
xp_to_next = XP_PER_LEVEL * _fibonacci(level)
if base:
max_health = base.max_health * level_scale
max_shield = base.max_shield * level_scale
else:
max_health = 100.0 * level_scale
max_shield = 50.0 * level_scale
health = max_health
shield = max_shield
EventBus.health_changed.emit(self, health, max_health)
EventBus.shield_changed.emit(self, shield, max_shield)
EventBus.level_up.emit(self, level)
func _fibonacci(n: int) -> int:
if n <= 1:
return 1
if n == 2:
return 2
var a := 1
var b := 2
for i in range(3, n + 1):
var c := a + b
a = b
b = c
return b
func _restore_cache() -> void:
health = _cache.get("health", max_health)
max_health = _cache.get("max_health", max_health)
health_regen = _cache.get("health_regen", 0.0)
shield = _cache.get("shield", 0.0)
max_shield = _cache.get("max_shield", 0.0)
shield_regen_delay = _cache.get("shield_regen_delay", 3.0)
shield_regen_time = _cache.get("shield_regen_time", 5.0)
alive = _cache.get("alive", true)
buff_damage = _cache.get("buff_damage", 1.0)
buff_heal = _cache.get("buff_heal", 1.0)
buff_shield = _cache.get("buff_shield", 1.0)
_cache.clear()

View File

@@ -1 +0,0 @@
uid://blmuqkl3aro5w

View File

@@ -1,45 +0,0 @@
extends Node
var entities: Dictionary = {}
func register(entity: Node, base: PortalStats) -> void:
var thresholds: Array[float] = base.thresholds.duplicate()
var triggered: Array[bool] = []
triggered.resize(thresholds.size())
triggered.fill(false)
entities[entity] = {
"base": base,
"health": base.max_health,
"max_health": base.max_health,
"alive": true,
"spawn_count": base.spawn_count,
"thresholds": thresholds,
"triggered": triggered,
}
func deregister(entity: Node) -> void:
entities.erase(entity)
func get_stat(entity: Node, key: String) -> Variant:
if entity in entities:
return entities[entity].get(key)
return null
func set_stat(entity: Node, key: String, value: Variant) -> void:
if entity in entities:
entities[entity][key] = value
func is_alive(entity: Node) -> bool:
if entity in entities:
return entities[entity]["alive"]
return false
func set_health(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["health"] = value
var max_health: float = entities[entity]["max_health"]
EventBus.health_changed.emit(entity, value, max_health)
if value <= 0 and entities[entity]["alive"]:
entities[entity]["alive"] = false
EventBus.entity_died.emit(entity)

View File

@@ -1 +0,0 @@
uid://doullpjapcsk1

46
autoloads/save_load.gd Normal file
View File

@@ -0,0 +1,46 @@
extends Node
const SAVE_DIR: String = "user://saves/"
func ensure_dir() -> void:
DirAccess.make_dir_recursive_absolute(SAVE_DIR)
func save_path(slot: String) -> String:
return SAVE_DIR + slot + ".save"
func save_run(slot: String, payload: Dictionary) -> bool:
ensure_dir()
var f := FileAccess.open(save_path(slot), FileAccess.WRITE)
if f == null:
push_error("SaveLoad: cannot open %s for write" % slot)
return false
f.store_string(JSON.stringify(payload, " "))
f.close()
return true
func load_run(slot: String) -> Dictionary:
var path := save_path(slot)
if not FileAccess.file_exists(path):
return {}
var f := FileAccess.open(path, FileAccess.READ)
var text := f.get_as_text()
f.close()
var data: Variant = JSON.parse_string(text)
if typeof(data) != TYPE_DICTIONARY:
return {}
return data
func list_slots() -> Array[String]:
ensure_dir()
var out: Array[String] = []
var d := DirAccess.open(SAVE_DIR)
if d == null:
return out
d.list_dir_begin()
var fname := d.get_next()
while fname != "":
if not d.current_is_dir() and fname.ends_with(".save"):
out.append(fname.trim_suffix(".save"))
fname = d.get_next()
d.list_dir_end()
return out

View File

@@ -0,0 +1 @@
uid://bstx6urqlutmq

124
autoloads/stats.gd Normal file
View File

@@ -0,0 +1,124 @@
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()

1
autoloads/stats.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://cdrii2l4sefow

View File

@@ -1,8 +0,0 @@
extends Resource
class_name BaseStats
@export var max_health := 100.0
@export var health_regen := 0.0
@export var max_shield := 0.0
@export var shield_regen_delay := 3.0
@export var shield_regen_time := 5.0

View File

@@ -1 +0,0 @@
uid://cet184f878lb8

View File

@@ -1,38 +0,0 @@
extends Node
var entities: Dictionary = {}
func register(entity: Node, base: Resource) -> void:
entities[entity] = {
"base": base,
"health": base.max_health,
"max_health": base.max_health,
"alive": true,
}
func deregister(entity: Node) -> void:
entities.erase(entity)
func get_stat(entity: Node, key: String) -> Variant:
if entity in entities:
return entities[entity].get(key)
return null
func set_stat(entity: Node, key: String, value: Variant) -> void:
if entity in entities:
entities[entity][key] = value
func is_alive(entity: Node) -> bool:
if entity in entities:
return entities[entity]["alive"]
return false
func set_health(entity: Node, value: float) -> void:
if entity not in entities:
return
entities[entity]["health"] = value
var max_health: float = entities[entity]["max_health"]
EventBus.tavern_damaged.emit(value, max_health)
if value <= 0 and entities[entity]["alive"]:
entities[entity]["alive"] = false
EventBus.tavern_destroyed.emit()

View File

@@ -1 +0,0 @@
uid://822h8c1pur1a