prototype vibe

This commit is contained in:
Marek Lenczewski
2026-04-16 17:20:57 +02:00
parent cf5979803e
commit f21e30eb55
72 changed files with 1330 additions and 70 deletions

View File

@@ -45,7 +45,7 @@ func _apply_passive(base: float, stat: String) -> float:
match stat:
"damage": mult = PlayerData.buff_damage
"heal": mult = PlayerData.buff_heal
return base * mult
return base * mult * PlayerData.level_scale
func _in_range(ability: Ability) -> bool:
if ability.ability_range <= 0 or ability.is_heal:
@@ -77,12 +77,13 @@ func _execute_aoe(player: Node, ability: Ability, dmg: float) -> bool:
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
return true
var hit := false
for enemy in get_tree().get_nodes_in_group("enemies"):
var dist: float = player.global_position.distance_to(enemy.global_position)
var targets: Array = get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals")
for target in targets:
var dist: float = player.global_position.distance_to(target.global_position)
if dist <= ability.ability_range:
EventBus.damage_requested.emit(player, enemy, dmg)
EventBus.damage_requested.emit(player, target, dmg)
if ability.element != 0:
EventBus.element_damage_dealt.emit(player, enemy, dmg, ability.element)
EventBus.element_damage_dealt.emit(player, target, dmg, ability.element)
hit = true
if hit:
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
@@ -116,12 +117,13 @@ func _execute_ult(player: Node, ability: Ability, dmg: float) -> bool:
if ability.element != 0:
EventBus.element_damage_dealt.emit(player, target, dmg * 5.0, ability.element)
var splash_range: float = ability.aoe_radius if ability.aoe_radius > 0 else ability.ability_range
for enemy in get_tree().get_nodes_in_group("enemies"):
if enemy != target and is_instance_valid(enemy):
var enemy_dist: float = target.global_position.distance_to(enemy.global_position)
if enemy_dist <= splash_range:
EventBus.damage_requested.emit(player, enemy, dmg * 2.0)
var splash_targets: Array = get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals")
for other in splash_targets:
if other != target and is_instance_valid(other):
var other_dist: float = target.global_position.distance_to(other.global_position)
if other_dist <= splash_range:
EventBus.damage_requested.emit(player, other, dmg * 2.0)
if ability.element != 0:
EventBus.element_damage_dealt.emit(player, enemy, dmg * 2.0, ability.element)
EventBus.element_damage_dealt.emit(player, other, dmg * 2.0, ability.element)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg * 5.0)
return true

View File

@@ -11,6 +11,8 @@ func _process_group(delta: float, data_source: Node) -> void:
if not is_instance_valid(entity) or not data_source.is_alive(entity):
continue
var data: Dictionary = data_source.entities[entity]
if entity.is_in_group("invasion"):
_force_invasion_target(entity, data)
var state: int = data["state"]
match state:
State.IDLE:
@@ -23,12 +25,22 @@ func _process_group(delta: float, data_source: Node) -> void:
State.RETURN:
_return_to_spawn(entity, data, data_source, delta)
func _force_invasion_target(entity: Node, data: Dictionary) -> void:
var tavern: Node = get_tree().get_first_node_in_group("tavern")
if not tavern:
return
data["target"] = tavern
if data["state"] == State.IDLE or data["state"] == State.RETURN:
data["state"] = State.CHASE
func _chase(entity: Node, data: Dictionary, data_source: Node) -> void:
if not is_instance_valid(data["target"]):
data["state"] = State.RETURN
return
var base: EnemyStats = data_source.get_base(entity)
var attack_range: float = base.attack_range
if data["target"].is_in_group("tavern"):
attack_range += 3.0
var dist: float = entity.global_position.distance_to(data["target"].global_position)
if dist <= attack_range:
data["state"] = State.ATTACK
@@ -49,13 +61,17 @@ func _attack(entity: Node, data: Dictionary, data_source: Node, delta: float) ->
data["state"] = State.RETURN
return
var base: EnemyStats = data_source.get_base(entity)
var attack_range: float = base.attack_range
if data["target"].is_in_group("tavern"):
attack_range += 3.0
var dist: float = entity.global_position.distance_to(data["target"].global_position)
if dist > base.attack_range:
if dist > attack_range:
data["state"] = State.CHASE
return
if data["attack_timer"] <= 0:
data["attack_timer"] = base.attack_cooldown
EventBus.damage_requested.emit(entity, data["target"], base.attack_damage)
var scale: float = data.get("scale", 1.0)
EventBus.damage_requested.emit(entity, data["target"], base.attack_damage * scale)
entity.velocity.x = 0
entity.velocity.z = 0

View File

@@ -16,7 +16,7 @@ func _process(_delta: float) -> void:
var aa_damage: float = ability_set.aa_damage
var aa_range: float = ability_set.aa_range
var aa_is_heal: bool = ability_set.aa_is_heal
var dmg: float = aa_damage * (PlayerData.buff_heal if aa_is_heal else PlayerData.buff_damage)
var dmg: float = aa_damage * (PlayerData.buff_heal if aa_is_heal else PlayerData.buff_damage) * PlayerData.level_scale
if aa_is_heal:
EventBus.heal_requested.emit(player, player, dmg)
else:

113
systems/audio_system.gd Normal file
View File

@@ -0,0 +1,113 @@
extends Node
const SFX_PATHS := {
"hit": "res://assets/audio/sfx/hit.wav",
"death": "res://assets/audio/sfx/death.wav",
"level_up": "res://assets/audio/sfx/level_up.wav",
"ability_cast": "res://assets/audio/sfx/ability_cast.wav",
"portal_spawn": "res://assets/audio/sfx/portal_spawn.wav",
"invasion_alarm": "res://assets/audio/sfx/invasion_alarm.wav",
"tavern_damage": "res://assets/audio/sfx/tavern_damage.wav",
}
const MUSIC_PATHS := {
"tavern": "res://assets/audio/music/tavern.wav",
"battle": "res://assets/audio/music/battle.wav",
"invasion": "res://assets/audio/music/invasion.wav",
}
const SFX_POOL_SIZE := 8
var sfx_cache: Dictionary = {}
var music_cache: Dictionary = {}
var sfx_players: Array[AudioStreamPlayer] = []
var music_player: AudioStreamPlayer = null
var current_music: String = ""
func _ready() -> void:
for i in range(SFX_POOL_SIZE):
var p := AudioStreamPlayer.new()
p.volume_db = -6.0
add_child(p)
sfx_players.append(p)
music_player = AudioStreamPlayer.new()
music_player.volume_db = -12.0
music_player.finished.connect(_on_music_finished)
add_child(music_player)
_preload_audio()
EventBus.attack_executed.connect(_on_attack_executed)
EventBus.entity_died.connect(_on_entity_died)
EventBus.level_up.connect(_on_level_up)
EventBus.portal_spawn.connect(_on_portal_spawn)
EventBus.tavern_damaged.connect(_on_tavern_damaged)
EventBus.invasion_started.connect(_on_invasion_started)
EventBus.invasion_ended.connect(_on_invasion_ended)
EventBus.wave_started.connect(_on_wave_started)
_play_music("tavern")
func _preload_audio() -> void:
for key in SFX_PATHS:
var path: String = SFX_PATHS[key]
if ResourceLoader.exists(path):
var stream: AudioStream = load(path)
if stream:
sfx_cache[key] = stream
for key in MUSIC_PATHS:
var path: String = MUSIC_PATHS[key]
if ResourceLoader.exists(path):
var stream: AudioStream = load(path)
if stream is AudioStreamWAV:
(stream as AudioStreamWAV).loop_mode = AudioStreamWAV.LOOP_FORWARD
(stream as AudioStreamWAV).loop_end = (stream as AudioStreamWAV).data.size() / 2
if stream:
music_cache[key] = stream
func play_sfx(key: String) -> void:
if not sfx_cache.has(key):
return
for p in sfx_players:
if not p.playing:
p.stream = sfx_cache[key]
p.play()
return
func _play_music(key: String) -> void:
if current_music == key and music_player.playing:
return
if not music_cache.has(key):
return
music_player.stream = music_cache[key]
music_player.play()
current_music = key
func _on_music_finished() -> void:
if current_music != "" and music_cache.has(current_music):
music_player.play()
func _on_attack_executed(_attacker, _pos, _dir, _dmg) -> void:
play_sfx("hit")
func _on_entity_died(entity: Node) -> void:
if entity == PlayerData:
return
play_sfx("death")
func _on_level_up(_player, _level) -> void:
play_sfx("level_up")
func _on_portal_spawn(_portal, _enemies) -> void:
play_sfx("portal_spawn")
func _on_tavern_damaged(_current, _max_val) -> void:
play_sfx("tavern_damage")
func _on_invasion_started(_enemies) -> void:
play_sfx("invasion_alarm")
_play_music("invasion")
func _on_invasion_ended(_success) -> void:
_play_music("battle")
func _on_wave_started(_wave) -> void:
if current_music != "invasion":
_play_music("battle")

View File

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

View File

@@ -5,9 +5,10 @@ func _ready() -> void:
func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void:
var remaining: float = amount
var shield_system: Node = get_node_or_null("../ShieldSystem")
if shield_system:
remaining = shield_system.absorb(target, remaining)
if not target.is_in_group("tavern"):
var shield_system: Node = get_node_or_null("../ShieldSystem")
if shield_system:
remaining = shield_system.absorb(target, remaining)
EventBus.damage_dealt.emit(attacker, target, amount)
if remaining > 0:
_apply_damage(target, remaining)
@@ -33,6 +34,11 @@ func _apply_damage(entity: Node, amount: float) -> void:
if health < 0:
health = 0
PortalData.set_health(entity, health)
elif entity.is_in_group("tavern"):
var health: float = TavernData.get_stat(entity, "health") - amount
if health < 0:
health = 0
TavernData.set_health(entity, health)
func _get_player() -> Node:
return get_tree().get_first_node_in_group("player")

View File

@@ -8,6 +8,12 @@ const MARGIN := 2
var ability_labels: Array[String] = ["1", "2", "3", "4", "P"]
var effect_container: HBoxContainer = null
var tavern_bar: ProgressBar = null
var tavern_label: Label = null
var wave_label: Label = null
var level_label: Label = null
var xp_bar: ProgressBar = null
var xp_label: Label = null
func _ready() -> void:
EventBus.health_changed.connect(_on_health_changed)
@@ -19,6 +25,11 @@ func _ready() -> void:
EventBus.cooldown_tick.connect(_on_cooldown_tick)
EventBus.effect_applied.connect(_on_effect_applied)
EventBus.effect_expired.connect(_on_effect_expired)
EventBus.tavern_damaged.connect(_on_tavern_damaged)
EventBus.wave_started.connect(_on_wave_started)
EventBus.wave_timer_tick.connect(_on_wave_timer_tick)
EventBus.xp_gained.connect(_on_xp_gained)
EventBus.level_up.connect(_on_level_up)
_init_hud.call_deferred()
func _init_hud() -> void:
@@ -31,6 +42,121 @@ func _init_hud() -> void:
effect_container.position = Vector2(10, 60)
effect_container.add_theme_constant_override("separation", 3)
hud.add_child(effect_container)
_init_tavern_bar(hud)
_init_xp_bar(hud)
func _init_tavern_bar(hud: CanvasLayer) -> void:
var container := VBoxContainer.new()
container.name = "TavernContainer"
container.anchor_left = 0.5
container.anchor_right = 0.5
container.offset_left = -150
container.offset_right = 150
container.offset_top = 10
container.add_theme_constant_override("separation", 2)
wave_label = Label.new()
wave_label.text = "Welle 1 — 60:00"
wave_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
wave_label.add_theme_font_size_override("font_size", 18)
container.add_child(wave_label)
var title := Label.new()
title.text = "Taverne"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 14)
container.add_child(title)
tavern_bar = ProgressBar.new()
tavern_bar.custom_minimum_size = Vector2(300, 22)
tavern_bar.show_percentage = false
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.3, 0.1, 0.1, 1)
var fill := StyleBoxFlat.new()
fill.bg_color = Color(0.9, 0.7, 0.2, 1)
tavern_bar.add_theme_stylebox_override("background", bg)
tavern_bar.add_theme_stylebox_override("fill", fill)
tavern_bar.max_value = 5000.0
tavern_bar.value = 5000.0
container.add_child(tavern_bar)
tavern_label = Label.new()
tavern_label.text = "5000/5000"
tavern_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
tavern_label.anchor_left = 0.0
tavern_label.anchor_right = 1.0
tavern_bar.add_child(tavern_label)
hud.add_child(container)
func _on_tavern_damaged(current: float, max_val: float) -> void:
if tavern_bar:
tavern_bar.max_value = max_val
tavern_bar.value = current
if tavern_label:
tavern_label.text = "%d/%d" % [current, max_val]
func _init_xp_bar(hud: CanvasLayer) -> void:
var container := VBoxContainer.new()
container.name = "LevelContainer"
container.anchor_left = 1.0
container.anchor_top = 1.0
container.anchor_right = 1.0
container.anchor_bottom = 1.0
container.offset_left = -260
container.offset_top = -80
container.offset_right = -10
container.offset_bottom = -10
container.add_theme_constant_override("separation", 2)
level_label = Label.new()
level_label.text = "Level 1"
level_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
level_label.add_theme_font_size_override("font_size", 20)
container.add_child(level_label)
xp_bar = ProgressBar.new()
xp_bar.custom_minimum_size = Vector2(250, 22)
xp_bar.max_value = 1
xp_bar.value = 0
xp_bar.show_percentage = false
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.1, 0.2, 0.1, 1)
var fill := StyleBoxFlat.new()
fill.bg_color = Color(0.3, 0.9, 0.3, 1)
xp_bar.add_theme_stylebox_override("background", bg)
xp_bar.add_theme_stylebox_override("fill", fill)
xp_label = Label.new()
xp_label.text = "0/1"
xp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
xp_label.anchor_left = 0.0
xp_label.anchor_right = 1.0
xp_bar.add_child(xp_label)
container.add_child(xp_bar)
hud.add_child(container)
_update_xp_ui()
func _on_xp_gained(_player: Node, _amount: int) -> void:
_update_xp_ui()
func _on_level_up(_player: Node, _new_level: int) -> void:
_update_xp_ui()
func _update_xp_ui() -> void:
if level_label:
level_label.text = "Level %d" % PlayerData.level
if xp_bar:
xp_bar.max_value = PlayerData.xp_to_next
xp_bar.value = PlayerData.xp
if xp_label:
xp_label.text = "%d/%d" % [PlayerData.xp, PlayerData.xp_to_next]
func _on_wave_started(_wave_number: int) -> void:
_update_wave_label()
func _on_wave_timer_tick(_seconds_remaining: float) -> void:
_update_wave_label()
func _update_wave_label() -> void:
if not wave_label:
return
var secs: int = int(max(0.0, GameState.wave_timer_remaining))
var mm: int = secs / 60
var ss: int = secs % 60
wave_label.text = "Welle %d%02d:%02d" % [GameState.current_wave, mm, ss]
func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
if entity != PlayerData:

View File

@@ -0,0 +1,88 @@
extends Node
const ENEMY_SCENE: PackedScene = preload("res://scenes/enemy/enemy.tscn")
const BOSS_STATS: Resource = preload("res://scenes/enemy/boss_stats.tres")
const INVASION_COUNT := 16
const SPAWN_RADIUS := 45.0
var active: bool = false
var invasion_enemies: Array[Node] = []
func _ready() -> void:
EventBus.wave_timer_tick.connect(_on_wave_timer_tick)
EventBus.entity_died.connect(_on_entity_died)
EventBus.tavern_destroyed.connect(_on_tavern_destroyed)
func _on_wave_timer_tick(seconds_remaining: float) -> void:
if active:
return
if seconds_remaining > 0:
return
var has_alive_red := false
for p in get_tree().get_nodes_in_group("red_portal"):
if is_instance_valid(p) and PortalData.is_alive(p):
has_alive_red = true
break
if not has_alive_red:
return
trigger()
func trigger() -> void:
active = true
invasion_enemies.clear()
var tavern: Node = get_tree().get_first_node_in_group("tavern")
if not tavern:
active = false
return
var world: Node = get_tree().current_scene
var scale: float = PlayerData.level_scale * 10.0
for enemy in get_tree().get_nodes_in_group("red_enemies"):
if is_instance_valid(enemy):
_convert_to_invasion(enemy, tavern)
for i in range(INVASION_COUNT):
var angle: float = randf() * TAU
var pos := Vector3(cos(angle) * SPAWN_RADIUS, 0, sin(angle) * SPAWN_RADIUS)
var enemy: Node = ENEMY_SCENE.instantiate()
enemy.spawn_scale = scale
world.add_child(enemy)
enemy.global_position = pos
_convert_to_invasion(enemy, tavern)
var boss_angle: float = randf() * TAU
var boss_pos := Vector3(cos(boss_angle) * SPAWN_RADIUS, 0, sin(boss_angle) * SPAWN_RADIUS)
var boss: Node = ENEMY_SCENE.instantiate()
boss.add_to_group("boss")
boss.stats = BOSS_STATS
boss.spawn_scale = scale
world.add_child(boss)
boss.global_position = boss_pos
_convert_to_invasion(boss, tavern)
EventBus.invasion_started.emit(invasion_enemies)
func _convert_to_invasion(enemy: Node, tavern: Node) -> void:
enemy.add_to_group("invasion")
var data_source: Node = BossData if enemy.is_in_group("boss") else EnemyData
data_source.set_stat(enemy, "target", tavern)
data_source.set_stat(enemy, "state", 1)
invasion_enemies.append(enemy)
func _on_entity_died(entity: Node) -> void:
if not active:
return
if entity not in invasion_enemies:
return
invasion_enemies.erase(entity)
var alive_count := 0
for e in invasion_enemies:
if is_instance_valid(e):
alive_count += 1
if alive_count == 0:
_end_invasion(true)
func _end_invasion(success: bool) -> void:
active = false
invasion_enemies.clear()
EventBus.invasion_ended.emit(success)
func _on_tavern_destroyed() -> void:
active = false
EventBus.game_over.emit()

View File

@@ -0,0 +1 @@
uid://841gb4nrydai

View File

@@ -16,8 +16,14 @@ func _ready() -> void:
EventBus.effect_applied.connect(_on_effect_applied)
EventBus.effect_expired.connect(_on_effect_expired)
EventBus.portal_spawn.connect(_on_portal_spawn)
EventBus.invasion_started.connect(_on_invasion_started)
_init_nameplates.call_deferred()
func _on_invasion_started(enemies: Array) -> void:
for enemy in enemies:
if is_instance_valid(enemy):
_setup_nameplate.call_deferred(enemy)
func _init_nameplates() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
_setup_nameplate(enemy)
@@ -80,6 +86,8 @@ func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
var nameplate: Sprite3D = entity.get_node_or_null("Healthbar")
if not nameplate:
return
if not nameplate.texture:
_setup_nameplate(entity)
var bar: ProgressBar = nameplate.get_node_or_null("SubViewport/ShieldBar")
if not bar:
return
@@ -96,12 +104,16 @@ func _on_target_changed(_player: Node, target: Node) -> void:
continue
var nameplate: Sprite3D = enemy.get_node_or_null("Healthbar")
if nameplate:
if not nameplate.texture:
_setup_nameplate(enemy)
nameplate.get_node("SubViewport/Border").visible = (target == enemy)
for portal in get_tree().get_nodes_in_group("portals"):
if not is_instance_valid(portal):
continue
var nameplate: Sprite3D = portal.get_node_or_null("Healthbar")
if nameplate:
if not nameplate.texture:
_setup_nameplate(portal)
nameplate.get_node("SubViewport/Border").visible = (target == portal)
func _on_entity_died(entity: Node) -> void:

View File

@@ -14,8 +14,6 @@ func _on_entity_died(entity: Node) -> void:
var gate: Node3D = GATE_SCENE.instantiate()
entity.get_parent().add_child(gate)
gate.global_position = pos
for enemy in get_tree().get_nodes_in_group("enemies"):
if is_instance_valid(enemy):
enemy.queue_free()
gate.dungeon_variant = entity.stats.variant
EventBus.portal_defeated.emit(entity)
entity.queue_free()

View File

@@ -15,18 +15,21 @@ func _process(delta: float) -> void:
_respawn()
func _on_entity_died(entity: Node) -> void:
if not entity.is_in_group("player"):
if entity != PlayerData:
return
if is_dead:
return
is_dead = true
respawn_timer = PlayerData.respawn_time
entity.velocity = Vector3.ZERO
entity.get_node("Mesh").visible = false
entity.get_node("CollisionShape3D").disabled = true
entity.get_node("Movement").set_physics_process(false)
entity.get_node("Ability").set_process_unhandled_input(false)
entity.get_node("Targeting").set_process_unhandled_input(false)
var player: Node = get_tree().get_first_node_in_group("player")
if not player:
return
player.velocity = Vector3.ZERO
player.get_node("Mesh").visible = false
player.get_node("CollisionShape3D").disabled = true
player.get_node("Movement").set_physics_process(false)
player.get_node("Ability").set_process_unhandled_input(false)
player.get_node("Targeting").set_process_unhandled_input(false)
func _respawn() -> void:
is_dead = false

View File

@@ -25,11 +25,18 @@ func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
func _spawn_enemies(portal: Node, count: int) -> void:
var spawned: Array = []
var is_red: bool = portal.is_in_group("red_portal")
var portal_bonus: float = 10.0 if is_red else 1.0
var total_scale: float = PlayerData.level_scale * portal_bonus
for j in range(count):
var entity: Node = ENEMY_SCENE.instantiate()
entity.spawn_scale = total_scale
var offset := Vector3(randf_range(-2, 2), 0, randf_range(-2, 2))
portal.get_parent().add_child(entity)
if is_red:
entity.add_to_group("red_enemies")
entity.global_position = portal.global_position + offset
EnemyData.set_stat(entity, "portal", portal)
spawned.append(entity)
var player: Node = get_tree().get_first_node_in_group("player")
if player:

View File

@@ -14,6 +14,9 @@ func _process(delta: float) -> void:
func _on_target_requested(_player: Node, target: Node3D) -> void:
PlayerData.set_target(target)
if target and (target.is_in_group("enemies") or target.is_in_group("portals")):
PlayerData.in_combat = true
PlayerData.combat_timer = PlayerData.combat_timeout
func _on_entity_died(entity: Node) -> void:
if entity == PlayerData.target:

47
systems/wave_system.gd Normal file
View File

@@ -0,0 +1,47 @@
extends Node
const WAVE_DURATION := 60.0
var tick_accumulator := 0.0
func _ready() -> void:
EventBus.portal_defeated.connect(_on_portal_defeated)
EventBus.invasion_ended.connect(_on_invasion_ended)
call_deferred("_start_run")
func _start_run() -> void:
if not GameState.run_initialized:
GameState.current_wave = 1
GameState.wave_timer_remaining = WAVE_DURATION
GameState.run_initialized = true
EventBus.run_started.emit(GameState.current_wave)
EventBus.wave_started.emit(GameState.current_wave)
func _process(delta: float) -> void:
if GameState.wave_timer_remaining <= 0:
return
GameState.wave_timer_remaining -= delta
tick_accumulator += delta
if tick_accumulator >= 1.0:
tick_accumulator -= 1.0
EventBus.wave_timer_tick.emit(max(0.0, GameState.wave_timer_remaining))
if GameState.wave_timer_remaining <= 0:
GameState.wave_timer_remaining = 0.0
EventBus.wave_timer_tick.emit(0.0)
func _on_portal_defeated(portal: Node) -> void:
if not portal.is_in_group("red_portal"):
return
_advance_wave()
func _on_invasion_ended(success: bool) -> void:
if not success:
return
_advance_wave()
func _advance_wave() -> void:
EventBus.wave_ended.emit(GameState.current_wave, true)
GameState.current_wave += 1
GameState.wave_timer_remaining = WAVE_DURATION
tick_accumulator = 0.0
EventBus.wave_started.emit(GameState.current_wave)

View File

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

19
systems/xp_system.gd Normal file
View File

@@ -0,0 +1,19 @@
extends Node
const XP_PER_ENEMY: int = 3
const XP_PER_BOSS: int = 30
func _ready() -> void:
EventBus.entity_died.connect(_on_entity_died)
func _on_entity_died(entity: Node) -> void:
if not entity or not is_instance_valid(entity):
return
if not entity.is_in_group("enemies"):
return
var is_boss: bool = entity.is_in_group("boss")
var data_source: Node = BossData if is_boss else EnemyData
var scale_val: Variant = data_source.get_stat(entity, "scale")
var scale: float = scale_val if scale_val != null else 1.0
var base_xp: int = XP_PER_BOSS if is_boss else XP_PER_ENEMY
PlayerData.add_xp(int(base_xp * scale))

1
systems/xp_system.gd.uid Normal file
View File

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