diff --git a/assets/audio/music/battle.wav b/assets/audio/music/battle.wav new file mode 100644 index 0000000..136171b Binary files /dev/null and b/assets/audio/music/battle.wav differ diff --git a/assets/audio/music/battle.wav.import b/assets/audio/music/battle.wav.import new file mode 100644 index 0000000..4294ad8 --- /dev/null +++ b/assets/audio/music/battle.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://d4a76sj0xk578" +path="res://.godot/imported/battle.wav-74e134d2675fd72e2f3b1b22f3e2be00.sample" + +[deps] + +source_file="res://assets/audio/music/battle.wav" +dest_files=["res://.godot/imported/battle.wav-74e134d2675fd72e2f3b1b22f3e2be00.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/assets/audio/music/invasion.wav b/assets/audio/music/invasion.wav new file mode 100644 index 0000000..a6cf56d Binary files /dev/null and b/assets/audio/music/invasion.wav differ diff --git a/assets/audio/music/invasion.wav.import b/assets/audio/music/invasion.wav.import new file mode 100644 index 0000000..e2fe73b --- /dev/null +++ b/assets/audio/music/invasion.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://clvph60pdl81v" +path="res://.godot/imported/invasion.wav-6117678cf33bae18ebd1655477798610.sample" + +[deps] + +source_file="res://assets/audio/music/invasion.wav" +dest_files=["res://.godot/imported/invasion.wav-6117678cf33bae18ebd1655477798610.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/assets/audio/music/tavern.wav b/assets/audio/music/tavern.wav new file mode 100644 index 0000000..78eb6ad Binary files /dev/null and b/assets/audio/music/tavern.wav differ diff --git a/assets/audio/music/tavern.wav.import b/assets/audio/music/tavern.wav.import new file mode 100644 index 0000000..b90d3fa --- /dev/null +++ b/assets/audio/music/tavern.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dnowuon22316o" +path="res://.godot/imported/tavern.wav-8d46496f3ea930a74a1080b53ceb6a0e.sample" + +[deps] + +source_file="res://assets/audio/music/tavern.wav" +dest_files=["res://.godot/imported/tavern.wav-8d46496f3ea930a74a1080b53ceb6a0e.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/assets/audio/sfx/ability_cast.wav b/assets/audio/sfx/ability_cast.wav new file mode 100644 index 0000000..def4882 Binary files /dev/null and b/assets/audio/sfx/ability_cast.wav differ diff --git a/assets/audio/sfx/ability_cast.wav.import b/assets/audio/sfx/ability_cast.wav.import new file mode 100644 index 0000000..bf34ba2 --- /dev/null +++ b/assets/audio/sfx/ability_cast.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://crpv7tej1lwnn" +path="res://.godot/imported/ability_cast.wav-05d75ac0fb52a99d863b460bd374fd73.sample" + +[deps] + +source_file="res://assets/audio/sfx/ability_cast.wav" +dest_files=["res://.godot/imported/ability_cast.wav-05d75ac0fb52a99d863b460bd374fd73.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/assets/audio/sfx/death.wav b/assets/audio/sfx/death.wav new file mode 100644 index 0000000..d299a3b Binary files /dev/null and b/assets/audio/sfx/death.wav differ diff --git a/assets/audio/sfx/death.wav.import b/assets/audio/sfx/death.wav.import new file mode 100644 index 0000000..04b365d --- /dev/null +++ b/assets/audio/sfx/death.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dmmeubtnbibaj" +path="res://.godot/imported/death.wav-eb8bf206799fefce7cf26366434348b8.sample" + +[deps] + +source_file="res://assets/audio/sfx/death.wav" +dest_files=["res://.godot/imported/death.wav-eb8bf206799fefce7cf26366434348b8.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/assets/audio/sfx/hit.wav b/assets/audio/sfx/hit.wav new file mode 100644 index 0000000..7c01ab0 Binary files /dev/null and b/assets/audio/sfx/hit.wav differ diff --git a/assets/audio/sfx/hit.wav.import b/assets/audio/sfx/hit.wav.import new file mode 100644 index 0000000..2332bf3 --- /dev/null +++ b/assets/audio/sfx/hit.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://blv6fecsx5wax" +path="res://.godot/imported/hit.wav-27e178036f6cee6545e9f025a3865a36.sample" + +[deps] + +source_file="res://assets/audio/sfx/hit.wav" +dest_files=["res://.godot/imported/hit.wav-27e178036f6cee6545e9f025a3865a36.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/assets/audio/sfx/invasion_alarm.wav b/assets/audio/sfx/invasion_alarm.wav new file mode 100644 index 0000000..7a82c01 Binary files /dev/null and b/assets/audio/sfx/invasion_alarm.wav differ diff --git a/assets/audio/sfx/invasion_alarm.wav.import b/assets/audio/sfx/invasion_alarm.wav.import new file mode 100644 index 0000000..5b61a32 --- /dev/null +++ b/assets/audio/sfx/invasion_alarm.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://cys5bjcvl3d1q" +path="res://.godot/imported/invasion_alarm.wav-f4375961fdd02f77d27cc495cdccde5c.sample" + +[deps] + +source_file="res://assets/audio/sfx/invasion_alarm.wav" +dest_files=["res://.godot/imported/invasion_alarm.wav-f4375961fdd02f77d27cc495cdccde5c.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/assets/audio/sfx/level_up.wav b/assets/audio/sfx/level_up.wav new file mode 100644 index 0000000..9204277 Binary files /dev/null and b/assets/audio/sfx/level_up.wav differ diff --git a/assets/audio/sfx/level_up.wav.import b/assets/audio/sfx/level_up.wav.import new file mode 100644 index 0000000..d672141 --- /dev/null +++ b/assets/audio/sfx/level_up.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://2ogpkwinaq32" +path="res://.godot/imported/level_up.wav-60e30bfe8ab247d27e7615e99e00b8f1.sample" + +[deps] + +source_file="res://assets/audio/sfx/level_up.wav" +dest_files=["res://.godot/imported/level_up.wav-60e30bfe8ab247d27e7615e99e00b8f1.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/assets/audio/sfx/portal_spawn.wav b/assets/audio/sfx/portal_spawn.wav new file mode 100644 index 0000000..3263079 Binary files /dev/null and b/assets/audio/sfx/portal_spawn.wav differ diff --git a/assets/audio/sfx/portal_spawn.wav.import b/assets/audio/sfx/portal_spawn.wav.import new file mode 100644 index 0000000..596f68e --- /dev/null +++ b/assets/audio/sfx/portal_spawn.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://c6us2m0yer33r" +path="res://.godot/imported/portal_spawn.wav-7a2c81bb1bec2cdea2b6aa74f4884f93.sample" + +[deps] + +source_file="res://assets/audio/sfx/portal_spawn.wav" +dest_files=["res://.godot/imported/portal_spawn.wav-7a2c81bb1bec2cdea2b6aa74f4884f93.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/assets/audio/sfx/tavern_damage.wav b/assets/audio/sfx/tavern_damage.wav new file mode 100644 index 0000000..508e347 Binary files /dev/null and b/assets/audio/sfx/tavern_damage.wav differ diff --git a/assets/audio/sfx/tavern_damage.wav.import b/assets/audio/sfx/tavern_damage.wav.import new file mode 100644 index 0000000..83d5a39 --- /dev/null +++ b/assets/audio/sfx/tavern_damage.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dcfon26db14hk" +path="res://.godot/imported/tavern_damage.wav-1e664835ae36452ac3c6f2da885f2ce2.sample" + +[deps] + +source_file="res://assets/audio/sfx/tavern_damage.wav" +dest_files=["res://.godot/imported/tavern_damage.wav-1e664835ae36452ac3c6f2da885f2ce2.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/autoloads/boss_stats.gd b/autoloads/boss_stats.gd index 9533018..4e1a774 100644 --- a/autoloads/boss_stats.gd +++ b/autoloads/boss_stats.gd @@ -2,14 +2,17 @@ extends Node var entities: Dictionary = {} -func register(entity: Node, base: EnemyStats) -> void: +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, - "health": base.max_health, - "max_health": base.max_health, - "health_regen": base.health_regen, - "shield": base.max_shield, - "max_shield": base.max_shield, + "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, @@ -24,6 +27,21 @@ func register(entity: Node, base: EnemyStats) -> void: "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) diff --git a/autoloads/enemy_stats.gd b/autoloads/enemy_stats.gd index 9533018..4e1a774 100644 --- a/autoloads/enemy_stats.gd +++ b/autoloads/enemy_stats.gd @@ -2,14 +2,17 @@ extends Node var entities: Dictionary = {} -func register(entity: Node, base: EnemyStats) -> void: +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, - "health": base.max_health, - "max_health": base.max_health, - "health_regen": base.health_regen, - "shield": base.max_shield, - "max_shield": base.max_shield, + "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, @@ -24,6 +27,21 @@ func register(entity: Node, base: EnemyStats) -> void: "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) diff --git a/autoloads/event_bus.gd b/autoloads/event_bus.gd index a9a6d8d..b95a1d7 100644 --- a/autoloads/event_bus.gd +++ b/autoloads/event_bus.gd @@ -50,3 +50,24 @@ signal effect_expired(target, effect) 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 game_over() diff --git a/autoloads/game_state.gd b/autoloads/game_state.gd new file mode 100644 index 0000000..1e65473 --- /dev/null +++ b/autoloads/game_state.gd @@ -0,0 +1,19 @@ +extends Node + +# Run-Zustand +var current_wave: int = 1 +var wave_timer_remaining: float = 0.0 +var run_initialized: bool = false + +# 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: + current_wave = 1 + wave_timer_remaining = 0.0 + run_initialized = false + last_dungeon_variant = 0 + force_return_to_world = false diff --git a/autoloads/game_state.gd.uid b/autoloads/game_state.gd.uid new file mode 100644 index 0000000..4cf2998 --- /dev/null +++ b/autoloads/game_state.gd.uid @@ -0,0 +1 @@ +uid://c3jq4raqs0onf diff --git a/autoloads/player_stats.gd b/autoloads/player_stats.gd index 2310063..60d31bd 100644 --- a/autoloads/player_stats.gd +++ b/autoloads/player_stats.gd @@ -28,6 +28,13 @@ 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 @@ -61,11 +68,11 @@ func init_from_resource(res: PlayerStats) -> void: gcd_time = res.gcd_time aa_cooldown = res.aa_cooldown if _cache.is_empty(): - health = res.max_health - max_health = res.max_health - health_regen = res.health_regen - shield = res.max_shield - max_shield = res.max_shield + 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 @@ -130,6 +137,49 @@ func clear_cache() -> void: 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) diff --git a/autoloads/tavern_stats.gd b/autoloads/tavern_stats.gd new file mode 100644 index 0000000..bc28eed --- /dev/null +++ b/autoloads/tavern_stats.gd @@ -0,0 +1,38 @@ +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() diff --git a/autoloads/tavern_stats.gd.uid b/autoloads/tavern_stats.gd.uid new file mode 100644 index 0000000..975fe5e --- /dev/null +++ b/autoloads/tavern_stats.gd.uid @@ -0,0 +1 @@ +uid://822h8c1pur1a diff --git a/project.godot b/project.godot index 88490ae..382b8fd 100644 --- a/project.godot +++ b/project.godot @@ -11,17 +11,19 @@ config_version=5 [application] config/name="mmo" -run/main_scene="res://scenes/world/world.tscn" +run/main_scene="res://scenes/menu/main_menu.tscn" config/features=PackedStringArray("4.6", "Forward Plus") config/icon="res://icon.svg" [autoload] EventBus="*res://autoloads/event_bus.gd" +GameState="*res://autoloads/game_state.gd" PlayerData="*res://autoloads/player_stats.gd" EnemyData="*res://autoloads/enemy_stats.gd" BossData="*res://autoloads/boss_stats.gd" PortalData="*res://autoloads/portal_stats.gd" +TavernData="*res://autoloads/tavern_stats.gd" [dotnet] diff --git a/scenes/dungeon/dungeon.tscn b/scenes/dungeon/dungeon.tscn index 58306d6..72a7a50 100644 --- a/scenes/dungeon/dungeon.tscn +++ b/scenes/dungeon/dungeon.tscn @@ -5,6 +5,9 @@ [ext_resource type="PackedScene" path="res://scenes/enemy/enemy.tscn" id="enemy"] [ext_resource type="Resource" path="res://scenes/enemy/boss_stats.tres" id="boss_stats"] [ext_resource type="Script" path="res://systems/dungeon_system.gd" id="dungeon_system"] +[ext_resource type="Script" path="res://scenes/dungeon/dungeon_manager.gd" id="dungeon_manager"] +[ext_resource type="Script" path="res://systems/audio_system.gd" id="audio_system"] +[ext_resource type="Script" path="res://systems/xp_system.gd" id="xp_system"] [ext_resource type="PackedScene" path="res://scenes/portal/gate.tscn" id="gate"] [ext_resource type="Script" path="res://systems/damage_system.gd" id="damage_system"] [ext_resource type="Script" path="res://systems/health_system.gd" id="health_system"] @@ -252,3 +255,12 @@ is_exit = true [node name="DungeonSystem" type="Node" parent="Systems"] script = ExtResource("dungeon_system") + +[node name="AudioSystem" type="Node" parent="Systems"] +script = ExtResource("audio_system") + +[node name="XpSystem" type="Node" parent="Systems"] +script = ExtResource("xp_system") + +[node name="DungeonManager" type="Node" parent="."] +script = ExtResource("dungeon_manager") diff --git a/scenes/dungeon/dungeon_manager.gd b/scenes/dungeon/dungeon_manager.gd new file mode 100644 index 0000000..a7c01ba --- /dev/null +++ b/scenes/dungeon/dungeon_manager.gd @@ -0,0 +1,16 @@ +extends Node + +func _ready() -> void: + call_deferred("_scale_dungeon") + +func _scale_dungeon() -> void: + var variant_multiplier: float = 10.0 if GameState.last_dungeon_variant == 1 else 1.0 + var total_scale: float = PlayerData.level_scale * variant_multiplier + var parent: Node = get_parent() + for child in parent.get_children(): + if not child.is_in_group("enemies"): + continue + if child.is_in_group("boss"): + BossData.apply_scale(child, total_scale) + else: + EnemyData.apply_scale(child, total_scale) diff --git a/scenes/dungeon/dungeon_manager.gd.uid b/scenes/dungeon/dungeon_manager.gd.uid new file mode 100644 index 0000000..799400b --- /dev/null +++ b/scenes/dungeon/dungeon_manager.gd.uid @@ -0,0 +1 @@ +uid://bfkxrflfn5qx4 diff --git a/scenes/enemy/init.gd b/scenes/enemy/init.gd index 348641c..63f883b 100644 --- a/scenes/enemy/init.gd +++ b/scenes/enemy/init.gd @@ -3,16 +3,23 @@ extends CharacterBody3D @export var stats: EnemyStats var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") +var spawn_scale: float = 1.0 +var hover_t: float = 0.0 +var mesh_base_y: float = 0.0 func _ready() -> void: add_to_group("enemies") if is_in_group("boss"): - BossData.register(self, stats) + BossData.register(self, stats, spawn_scale) BossData.set_stat(self, "spawn_position", global_position) else: - EnemyData.register(self, stats) + EnemyData.register(self, stats, spawn_scale) EnemyData.set_stat(self, "spawn_position", global_position) EventBus.entity_died.connect(_on_entity_died) + var mesh: Node3D = get_node_or_null("Mesh") + if mesh: + mesh_base_y = mesh.position.y + _apply_appearance(mesh) func _exit_tree() -> void: if is_in_group("boss"): @@ -24,6 +31,27 @@ func _on_entity_died(entity: Node) -> void: if entity == self: queue_free() +func _process(delta: float) -> void: + hover_t += delta + var mesh: Node3D = get_node_or_null("Mesh") + if mesh: + mesh.position.y = mesh_base_y + sin(hover_t * 3.0) * 0.08 + +func _apply_appearance(mesh: Node3D) -> void: + if mesh is MeshInstance3D: + var mat := StandardMaterial3D.new() + if is_in_group("boss"): + mat.albedo_color = Color(0.6, 0.15, 0.7, 1) + mat.emission_enabled = true + mat.emission = Color(0.8, 0.2, 0.9, 1) + mat.emission_energy_multiplier = 0.4 + else: + mat.albedo_color = Color(0.7, 0.25, 0.25, 1) + mat.emission_enabled = true + mat.emission = Color(0.9, 0.3, 0.2, 1) + mat.emission_energy_multiplier = 0.25 + (mesh as MeshInstance3D).material_override = mat + func _physics_process(delta: float) -> void: if not is_on_floor(): velocity.y -= gravity * delta diff --git a/scenes/menu/game_over_overlay.gd b/scenes/menu/game_over_overlay.gd new file mode 100644 index 0000000..707f167 --- /dev/null +++ b/scenes/menu/game_over_overlay.gd @@ -0,0 +1,21 @@ +extends CanvasLayer + +@onready var label: Label = $Center/VBox/Label +@onready var button: Button = $Center/VBox/Button + +func _ready() -> void: + visible = false + button.pressed.connect(_on_button) + +func show_overlay(wave: int) -> void: + label.text = "GAME OVER — Welle %d erreicht" % wave + visible = true + +func _on_button() -> void: + GameState.reset() + PlayerData.reset_run() + EnemyData.entities.clear() + BossData.entities.clear() + PortalData.entities.clear() + TavernData.entities.clear() + get_tree().change_scene_to_file("res://scenes/menu/main_menu.tscn") diff --git a/scenes/menu/game_over_overlay.gd.uid b/scenes/menu/game_over_overlay.gd.uid new file mode 100644 index 0000000..a77f403 --- /dev/null +++ b/scenes/menu/game_over_overlay.gd.uid @@ -0,0 +1 @@ +uid://dm00anoh5wtyu diff --git a/scenes/menu/game_over_overlay.tscn b/scenes/menu/game_over_overlay.tscn new file mode 100644 index 0000000..20a2c11 --- /dev/null +++ b/scenes/menu/game_over_overlay.tscn @@ -0,0 +1,41 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://scenes/menu/game_over_overlay.gd" id="1"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button"] +bg_color = Color(0.2, 0.2, 0.25, 0.9) +border_width_bottom = 2 +border_width_left = 2 +border_width_right = 2 +border_width_top = 2 +border_color = Color(0.7, 0.7, 0.7, 1) + +[node name="GameOverOverlay" type="CanvasLayer"] +layer = 10 +script = ExtResource("1") + +[node name="Background" type="ColorRect" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0, 0, 0, 0.75) + +[node name="Center" type="CenterContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="VBox" type="VBoxContainer" parent="Center"] +custom_minimum_size = Vector2(400, 0) +theme_override_constants/separation = 30 + +[node name="Label" type="Label" parent="Center/VBox"] +text = "GAME OVER" +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 48 +theme_override_colors/font_color = Color(1, 0.3, 0.3, 1) + +[node name="Button" type="Button" parent="Center/VBox"] +custom_minimum_size = Vector2(0, 48) +text = "Zurück zum Menü" +theme_override_styles/normal = SubResource("StyleBoxFlat_button") diff --git a/scenes/menu/main_menu.gd b/scenes/menu/main_menu.gd new file mode 100644 index 0000000..f430346 --- /dev/null +++ b/scenes/menu/main_menu.gd @@ -0,0 +1,32 @@ +extends CanvasLayer + +@onready var singleplayer_button: Button = $Center/VBox/SingleplayerButton +@onready var host_button: Button = $Center/VBox/HostButton +@onready var join_button: Button = $Center/VBox/JoinButton +@onready var quit_button: Button = $Center/VBox/QuitButton + +func _ready() -> void: + singleplayer_button.pressed.connect(_on_singleplayer) + host_button.pressed.connect(_on_host) + join_button.pressed.connect(_on_join) + quit_button.pressed.connect(_on_quit) + host_button.disabled = true + join_button.disabled = true + +func _on_singleplayer() -> void: + GameState.reset() + PlayerData.reset_run() + EnemyData.entities.clear() + BossData.entities.clear() + PortalData.entities.clear() + TavernData.entities.clear() + get_tree().change_scene_to_file("res://scenes/world/world.tscn") + +func _on_host() -> void: + pass + +func _on_join() -> void: + pass + +func _on_quit() -> void: + get_tree().quit() diff --git a/scenes/menu/main_menu.gd.uid b/scenes/menu/main_menu.gd.uid new file mode 100644 index 0000000..ac165e6 --- /dev/null +++ b/scenes/menu/main_menu.gd.uid @@ -0,0 +1 @@ +uid://b4m6byh4k2mg7 diff --git a/scenes/menu/main_menu.tscn b/scenes/menu/main_menu.tscn new file mode 100644 index 0000000..1e7b6ff --- /dev/null +++ b/scenes/menu/main_menu.tscn @@ -0,0 +1,57 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://scenes/menu/main_menu.gd" id="1"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button"] +bg_color = Color(0.2, 0.2, 0.25, 0.9) +border_width_bottom = 2 +border_width_left = 2 +border_width_right = 2 +border_width_top = 2 +border_color = Color(0.7, 0.7, 0.7, 1) + +[node name="MainMenu" type="CanvasLayer"] +script = ExtResource("1") + +[node name="Background" type="ColorRect" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.08, 0.08, 0.12, 1) + +[node name="Center" type="CenterContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="VBox" type="VBoxContainer" parent="Center"] +custom_minimum_size = Vector2(320, 0) +theme_override_constants/separation = 20 + +[node name="Title" type="Label" parent="Center/VBox"] +text = "MMO" +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 64 + +[node name="Spacer" type="Control" parent="Center/VBox"] +custom_minimum_size = Vector2(0, 40) + +[node name="SingleplayerButton" type="Button" parent="Center/VBox"] +custom_minimum_size = Vector2(0, 48) +text = "Singleplayer" +theme_override_styles/normal = SubResource("StyleBoxFlat_button") + +[node name="HostButton" type="Button" parent="Center/VBox"] +custom_minimum_size = Vector2(0, 48) +text = "Host (bald)" +theme_override_styles/normal = SubResource("StyleBoxFlat_button") + +[node name="JoinButton" type="Button" parent="Center/VBox"] +custom_minimum_size = Vector2(0, 48) +text = "Join (bald)" +theme_override_styles/normal = SubResource("StyleBoxFlat_button") + +[node name="QuitButton" type="Button" parent="Center/VBox"] +custom_minimum_size = Vector2(0, 48) +text = "Quit" +theme_override_styles/normal = SubResource("StyleBoxFlat_button") diff --git a/scenes/player/targeting.gd b/scenes/player/targeting.gd index 0e0b704..f3da88e 100644 --- a/scenes/player/targeting.gd +++ b/scenes/player/targeting.gd @@ -1,6 +1,6 @@ extends Node -const TARGET_RANGE := 20.0 +const TARGET_RANGE := 100.0 var mouse_press_pos: Vector2 = Vector2.ZERO @@ -29,15 +29,28 @@ func _try_target_under_mouse(mouse_pos: Vector2) -> void: var result := space.intersect_ray(query) if result: var hit_target := result.collider.get_parent() as Node3D - EventBus.target_requested.emit(player, hit_target) - else: - EventBus.target_requested.emit(player, null) + if hit_target and hit_target.is_in_group("tavern"): + EventBus.target_requested.emit(player, null) + return + if hit_target and (hit_target.is_in_group("enemies") or hit_target.is_in_group("portals")): + EventBus.target_requested.emit(player, hit_target) + return + EventBus.target_requested.emit(player, null) func _cycle_target() -> void: - var targets := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("portals") + var enemies := get_tree().get_nodes_in_group("enemies") + var portals := get_tree().get_nodes_in_group("portals") + var targets: Array = [] + for e in enemies: + if is_instance_valid(e): + targets.append(e) + for p in portals: + if is_instance_valid(p): + targets.append(p) if targets.is_empty(): EventBus.target_requested.emit(player, null) return + targets.sort_custom(func(a, b): return player.global_position.distance_squared_to(a.global_position) < player.global_position.distance_squared_to(b.global_position)) var current: Node3D = PlayerData.target if current == null or current not in targets: EventBus.target_requested.emit(player, targets[0]) diff --git a/scenes/portal/gate.gd b/scenes/portal/gate.gd index 1200f48..ee35323 100644 --- a/scenes/portal/gate.gd +++ b/scenes/portal/gate.gd @@ -4,6 +4,7 @@ extends StaticBody3D @export var is_exit: bool = false var active := false +var dungeon_variant: int = 0 func _ready() -> void: if not is_exit: @@ -28,6 +29,7 @@ func _on_gate_area_body_entered(body: Node3D) -> void: PlayerData.returning_from_dungeon = true else: PlayerData.portal_position = global_position + GameState.last_dungeon_variant = dungeon_variant call_deferred("_change_scene") func _change_scene() -> void: diff --git a/scenes/portal/init.gd b/scenes/portal/init.gd index 6c98a8b..3820fb5 100644 --- a/scenes/portal/init.gd +++ b/scenes/portal/init.gd @@ -4,11 +4,34 @@ extends StaticBody3D func _ready() -> void: add_to_group("portals") + if stats.variant == PortalStats.Kind.RED: + add_to_group("red_portal") + _apply_appearance() PortalData.register(self, stats) func _exit_tree() -> void: PortalData.deregister(self) +func _process(delta: float) -> void: + var mesh: Node3D = get_node_or_null("Mesh") + if mesh: + mesh.rotate_y(delta * 1.5) + +func _apply_appearance() -> void: + var mesh: MeshInstance3D = get_node_or_null("Mesh") + if not mesh: + return + var mat := StandardMaterial3D.new() + mat.emission_enabled = true + mat.emission_energy_multiplier = 0.8 + if stats.variant == PortalStats.Kind.RED: + mat.albedo_color = Color(0.9, 0.1, 0.1, 1) + mat.emission = Color(1.0, 0.25, 0.25, 1) + else: + mat.albedo_color = Color(0.4, 0.2, 0.85, 1) + mat.emission = Color(0.6, 0.35, 1.0, 1) + mesh.material_override = mat + func _on_detection_area_body_entered(body: Node3D) -> void: if body is CharacterBody3D and body.name == "Player": EventBus.portal_entered.emit(self, body) diff --git a/scenes/portal/portal_stats.gd b/scenes/portal/portal_stats.gd index 7a41d9f..e0e18ed 100644 --- a/scenes/portal/portal_stats.gd +++ b/scenes/portal/portal_stats.gd @@ -1,5 +1,8 @@ extends BaseStats class_name PortalStats +enum Kind { NORMAL, RED } + +@export var variant: Kind = Kind.NORMAL @export var spawn_count := 3 @export var thresholds: Array[float] = [0.85, 0.70, 0.55, 0.40, 0.25, 0.10] diff --git a/scenes/portal/red_portal_stats.tres b/scenes/portal/red_portal_stats.tres new file mode 100644 index 0000000..24a5be4 --- /dev/null +++ b/scenes/portal/red_portal_stats.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="PortalStats" format=3] + +[ext_resource type="Script" path="res://scenes/portal/portal_stats.gd" id="1"] + +[resource] +script = ExtResource("1") +variant = 1 +max_health = 5000.0 +spawn_count = 5 diff --git a/scenes/tavern/init.gd b/scenes/tavern/init.gd new file mode 100644 index 0000000..3ced402 --- /dev/null +++ b/scenes/tavern/init.gd @@ -0,0 +1,10 @@ +extends StaticBody3D + +@export var stats: TavernStats + +func _ready() -> void: + add_to_group("tavern") + TavernData.register(self, stats) + +func _exit_tree() -> void: + TavernData.deregister(self) diff --git a/scenes/tavern/init.gd.uid b/scenes/tavern/init.gd.uid new file mode 100644 index 0000000..24284c9 --- /dev/null +++ b/scenes/tavern/init.gd.uid @@ -0,0 +1 @@ +uid://dd103qxf2s5i5 diff --git a/scenes/tavern/tavern.tscn b/scenes/tavern/tavern.tscn new file mode 100644 index 0000000..7779554 --- /dev/null +++ b/scenes/tavern/tavern.tscn @@ -0,0 +1,66 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://scenes/tavern/init.gd" id="1"] +[ext_resource type="Resource" path="res://scenes/tavern/tavern_stats.tres" id="2"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_tavern"] +size = Vector3(5, 3, 5) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_tavern"] +albedo_color = Color(0.45, 0.3, 0.15, 1) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_roof"] +albedo_color = Color(0.3, 0.15, 0.08, 1) + +[sub_resource type="BoxMesh" id="BoxMesh_tavern"] +size = Vector3(5, 3, 5) +material = SubResource("StandardMaterial3D_tavern") + +[sub_resource type="PrismMesh" id="PrismMesh_roof"] +size = Vector3(5.4, 2, 5.4) +material = SubResource("StandardMaterial3D_roof") + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"] +bg_color = Color(0.3, 0.1, 0.1, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"] +bg_color = Color(0.9, 0.7, 0.2, 1) + +[node name="Tavern" type="StaticBody3D"] +script = ExtResource("1") +stats = ExtResource("2") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +shape = SubResource("BoxShape3D_tavern") + +[node name="Mesh" type="MeshInstance3D" parent="."] +mesh = SubResource("BoxMesh_tavern") + +[node name="Roof" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0) +mesh = SubResource("PrismMesh_roof") + +[node name="Healthbar" type="Sprite3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, 0) +billboard = 1 +pixel_size = 0.015 + +[node name="SubViewport" type="SubViewport" parent="Healthbar"] +transparent_bg = true +size = Vector2i(204, 25) + +[node name="Border" type="ColorRect" parent="Healthbar/SubViewport"] +offset_right = 204.0 +offset_bottom = 25.0 +color = Color(0.1, 0.1, 0.1, 1) + +[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport"] +offset_left = 2.0 +offset_top = 2.0 +offset_right = 202.0 +offset_bottom = 23.0 +theme_override_styles/background = SubResource("StyleBoxFlat_health_bg") +theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill") +max_value = 5000.0 +value = 5000.0 +show_percentage = false diff --git a/scenes/tavern/tavern_stats.gd b/scenes/tavern/tavern_stats.gd new file mode 100644 index 0000000..fa2326c --- /dev/null +++ b/scenes/tavern/tavern_stats.gd @@ -0,0 +1,2 @@ +extends BaseStats +class_name TavernStats diff --git a/scenes/tavern/tavern_stats.gd.uid b/scenes/tavern/tavern_stats.gd.uid new file mode 100644 index 0000000..72c740b --- /dev/null +++ b/scenes/tavern/tavern_stats.gd.uid @@ -0,0 +1 @@ +uid://duw4m3mhgmixk diff --git a/scenes/tavern/tavern_stats.tres b/scenes/tavern/tavern_stats.tres new file mode 100644 index 0000000..b7973e5 --- /dev/null +++ b/scenes/tavern/tavern_stats.tres @@ -0,0 +1,7 @@ +[gd_resource type="Resource" script_class="TavernStats" format=3] + +[ext_resource type="Script" path="res://scenes/tavern/tavern_stats.gd" id="1"] + +[resource] +script = ExtResource("1") +max_health = 5000.0 diff --git a/scenes/world/portal_spawner.gd b/scenes/world/portal_spawner.gd index e0bb415..18dc73c 100644 --- a/scenes/world/portal_spawner.gd +++ b/scenes/world/portal_spawner.gd @@ -2,34 +2,55 @@ extends Node const PORTAL_SCENE: PackedScene = preload("res://scenes/portal/portal.tscn") const GATE_SCENE: PackedScene = preload("res://scenes/portal/gate.tscn") -const SPAWN_INTERVAL := 30.0 -const MAX_PORTALS := 3 +const RED_PORTAL_STATS: Resource = preload("res://scenes/portal/red_portal_stats.tres") +const MAX_NORMAL_PORTALS := 3 const MIN_DISTANCE := 20.0 const MAX_DISTANCE := 40.0 +const RESPAWN_DELAY := 1.0 var portals: Array[Node] = [] -var timer := 0.0 func _ready() -> void: + EventBus.portal_defeated.connect(_on_portal_defeated) + EventBus.wave_started.connect(_on_wave_started) if PlayerData.portal_position != Vector3.ZERO and not PlayerData.dungeon_cleared: call_deferred("_restore_gate") else: if PlayerData.dungeon_cleared: PlayerData.clear_cache() - call_deferred("_spawn_portal") + call_deferred("_ensure_portals") func _restore_gate() -> void: var gate: Node3D = GATE_SCENE.instantiate() get_parent().add_child(gate) gate.global_position = PlayerData.portal_position -func _process(delta: float) -> void: - timer += delta - if timer >= SPAWN_INTERVAL: - timer = 0.0 - _cleanup_dead() - if portals.size() < MAX_PORTALS: - _spawn_portal() +func _ensure_portals() -> void: + _cleanup_dead() + while portals.size() < MAX_NORMAL_PORTALS: + _spawn_portal() + +func _on_portal_defeated(portal: Node) -> void: + if portal.is_in_group("red_portal"): + return + portals.erase(portal) + await get_tree().create_timer(RESPAWN_DELAY).timeout + _ensure_portals() + +func _on_wave_started(_wave_number: int) -> void: + _spawn_red_portal() + +func _spawn_red_portal() -> void: + for p in get_tree().get_nodes_in_group("red_portal"): + if is_instance_valid(p): + return + var angle: float = randf() * TAU + var distance: float = randf_range(MIN_DISTANCE, MAX_DISTANCE) + var pos := Vector3(cos(angle) * distance, 0, sin(angle) * distance) + var portal: Node3D = PORTAL_SCENE.instantiate() + portal.stats = RED_PORTAL_STATS + get_parent().add_child(portal) + portal.global_position = pos func _spawn_portal() -> void: var angle: float = randf() * TAU diff --git a/scenes/world/world.tscn b/scenes/world/world.tscn index 3c9e00b..ca8d061 100644 --- a/scenes/world/world.tscn +++ b/scenes/world/world.tscn @@ -23,7 +23,14 @@ [ext_resource type="Script" path="res://systems/respawn_system.gd" id="respawn_system"] [ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"] [ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"] +[ext_resource type="Script" path="res://systems/wave_system.gd" id="wave_system"] +[ext_resource type="Script" path="res://systems/xp_system.gd" id="xp_system"] +[ext_resource type="Script" path="res://systems/invasion_system.gd" id="invasion_system"] +[ext_resource type="Script" path="res://systems/audio_system.gd" id="audio_system"] +[ext_resource type="Script" path="res://scenes/world/world_manager.gd" id="world_manager"] +[ext_resource type="PackedScene" path="res://scenes/menu/game_over_overlay.tscn" id="game_over_overlay"] [ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"] +[ext_resource type="PackedScene" path="res://scenes/tavern/tavern.tscn" id="tavern"] [ext_resource type="PackedScene" uid="uid://cdnkbt1f0db7e" path="res://scenes/player/player.tscn" id="player"] [ext_resource type="Script" path="res://scenes/world/portal_spawner.gd" id="portal_spawner"] [ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="tank_set"] @@ -141,6 +148,18 @@ script = ExtResource("hud_system") [node name="NameplateSystem" type="Node" parent="Systems"] script = ExtResource("nameplate_system") +[node name="WaveSystem" type="Node" parent="Systems"] +script = ExtResource("wave_system") + +[node name="XpSystem" type="Node" parent="Systems"] +script = ExtResource("xp_system") + +[node name="InvasionSystem" type="Node" parent="Systems"] +script = ExtResource("invasion_system") + +[node name="AudioSystem" type="Node" parent="Systems"] +script = ExtResource("audio_system") + [node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0027503967, 0.014227867, 0.023231506) navigation_mesh = SubResource("NavigationMesh_1") @@ -157,15 +176,9 @@ shape = SubResource("WorldBoundaryShape3D_1") transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 0) shadow_enabled = true -[node name="Taverne" type="StaticBody3D" parent="."] +[node name="Tavern" parent="." instance=ExtResource("tavern")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) -[node name="Mesh" type="MeshInstance3D" parent="Taverne"] -mesh = SubResource("BoxMesh_tavern") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="Taverne"] -shape = SubResource("BoxShape3D_tavern") - [node name="Player" parent="." instance=ExtResource("player")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -5) @@ -173,3 +186,8 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -5) [node name="PortalSpawner" type="Node" parent="."] script = ExtResource("portal_spawner") + +[node name="WorldManager" type="Node" parent="."] +script = ExtResource("world_manager") + +[node name="GameOverOverlay" parent="." instance=ExtResource("game_over_overlay")] diff --git a/scenes/world/world_manager.gd b/scenes/world/world_manager.gd new file mode 100644 index 0000000..2789ba4 --- /dev/null +++ b/scenes/world/world_manager.gd @@ -0,0 +1,21 @@ +extends Node + +func _ready() -> void: + EventBus.game_over.connect(_on_game_over) + if GameState.force_return_to_world: + call_deferred("_handle_force_return") + +func _handle_force_return() -> void: + GameState.force_return_to_world = false + var player: Node3D = get_tree().get_first_node_in_group("player") + var tavern: Node3D = get_tree().get_first_node_in_group("tavern") + if player and tavern: + player.global_position = tavern.global_position + Vector3(0, 1, -6) + var invasion: Node = get_node_or_null("../Systems/InvasionSystem") + if invasion: + invasion.trigger() + +func _on_game_over() -> void: + var overlay: CanvasLayer = get_node_or_null("../GameOverOverlay") + if overlay and overlay.has_method("show_overlay"): + overlay.show_overlay(GameState.current_wave) diff --git a/scenes/world/world_manager.gd.uid b/scenes/world/world_manager.gd.uid new file mode 100644 index 0000000..583038f --- /dev/null +++ b/scenes/world/world_manager.gd.uid @@ -0,0 +1 @@ +uid://cejlqodm01ob3 diff --git a/systems/ability_system.gd b/systems/ability_system.gd index c76f820..ea34af2 100644 --- a/systems/ability_system.gd +++ b/systems/ability_system.gd @@ -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 diff --git a/systems/ai_system.gd b/systems/ai_system.gd index 4c52887..fc86752 100644 --- a/systems/ai_system.gd +++ b/systems/ai_system.gd @@ -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 diff --git a/systems/attack_system.gd b/systems/attack_system.gd index a683f89..bae1d91 100644 --- a/systems/attack_system.gd +++ b/systems/attack_system.gd @@ -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: diff --git a/systems/audio_system.gd b/systems/audio_system.gd new file mode 100644 index 0000000..b01b97a --- /dev/null +++ b/systems/audio_system.gd @@ -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") diff --git a/systems/audio_system.gd.uid b/systems/audio_system.gd.uid new file mode 100644 index 0000000..c4ab9eb --- /dev/null +++ b/systems/audio_system.gd.uid @@ -0,0 +1 @@ +uid://cbfc1ys0i4svm diff --git a/systems/damage_system.gd b/systems/damage_system.gd index 58eb844..84a07af 100644 --- a/systems/damage_system.gd +++ b/systems/damage_system.gd @@ -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") diff --git a/systems/hud_system.gd b/systems/hud_system.gd index 82addd6..50a94ff 100644 --- a/systems/hud_system.gd +++ b/systems/hud_system.gd @@ -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: diff --git a/systems/invasion_system.gd b/systems/invasion_system.gd new file mode 100644 index 0000000..94b147a --- /dev/null +++ b/systems/invasion_system.gd @@ -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() diff --git a/systems/invasion_system.gd.uid b/systems/invasion_system.gd.uid new file mode 100644 index 0000000..80ffe88 --- /dev/null +++ b/systems/invasion_system.gd.uid @@ -0,0 +1 @@ +uid://841gb4nrydai diff --git a/systems/nameplate_system.gd b/systems/nameplate_system.gd index e802394..c417c5b 100644 --- a/systems/nameplate_system.gd +++ b/systems/nameplate_system.gd @@ -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: diff --git a/systems/portal_system.gd b/systems/portal_system.gd index 20a2223..9a3e65d 100644 --- a/systems/portal_system.gd +++ b/systems/portal_system.gd @@ -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() diff --git a/systems/respawn_system.gd b/systems/respawn_system.gd index 21cae83..69b1c1b 100644 --- a/systems/respawn_system.gd +++ b/systems/respawn_system.gd @@ -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 diff --git a/systems/spawn_system.gd b/systems/spawn_system.gd index cc4c6fe..0193a4b 100644 --- a/systems/spawn_system.gd +++ b/systems/spawn_system.gd @@ -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: diff --git a/systems/targeting_system.gd b/systems/targeting_system.gd index f2541cd..177317c 100644 --- a/systems/targeting_system.gd +++ b/systems/targeting_system.gd @@ -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: diff --git a/systems/wave_system.gd b/systems/wave_system.gd new file mode 100644 index 0000000..1372f4c --- /dev/null +++ b/systems/wave_system.gd @@ -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) diff --git a/systems/wave_system.gd.uid b/systems/wave_system.gd.uid new file mode 100644 index 0000000..51a390c --- /dev/null +++ b/systems/wave_system.gd.uid @@ -0,0 +1 @@ +uid://chfcocmkb0wnp diff --git a/systems/xp_system.gd b/systems/xp_system.gd new file mode 100644 index 0000000..53f0e8e --- /dev/null +++ b/systems/xp_system.gd @@ -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)) diff --git a/systems/xp_system.gd.uid b/systems/xp_system.gd.uid new file mode 100644 index 0000000..4e97898 --- /dev/null +++ b/systems/xp_system.gd.uid @@ -0,0 +1 @@ +uid://clivdryqcvfmh