prototype vibe
This commit is contained in:
BIN
assets/audio/music/battle.wav
Normal file
BIN
assets/audio/music/battle.wav
Normal file
Binary file not shown.
24
assets/audio/music/battle.wav.import
Normal file
24
assets/audio/music/battle.wav.import
Normal file
@@ -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
|
||||
BIN
assets/audio/music/invasion.wav
Normal file
BIN
assets/audio/music/invasion.wav
Normal file
Binary file not shown.
24
assets/audio/music/invasion.wav.import
Normal file
24
assets/audio/music/invasion.wav.import
Normal file
@@ -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
|
||||
BIN
assets/audio/music/tavern.wav
Normal file
BIN
assets/audio/music/tavern.wav
Normal file
Binary file not shown.
24
assets/audio/music/tavern.wav.import
Normal file
24
assets/audio/music/tavern.wav.import
Normal file
@@ -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
|
||||
BIN
assets/audio/sfx/ability_cast.wav
Normal file
BIN
assets/audio/sfx/ability_cast.wav
Normal file
Binary file not shown.
24
assets/audio/sfx/ability_cast.wav.import
Normal file
24
assets/audio/sfx/ability_cast.wav.import
Normal file
@@ -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
|
||||
BIN
assets/audio/sfx/death.wav
Normal file
BIN
assets/audio/sfx/death.wav
Normal file
Binary file not shown.
24
assets/audio/sfx/death.wav.import
Normal file
24
assets/audio/sfx/death.wav.import
Normal file
@@ -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
|
||||
BIN
assets/audio/sfx/hit.wav
Normal file
BIN
assets/audio/sfx/hit.wav
Normal file
Binary file not shown.
24
assets/audio/sfx/hit.wav.import
Normal file
24
assets/audio/sfx/hit.wav.import
Normal file
@@ -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
|
||||
BIN
assets/audio/sfx/invasion_alarm.wav
Normal file
BIN
assets/audio/sfx/invasion_alarm.wav
Normal file
Binary file not shown.
24
assets/audio/sfx/invasion_alarm.wav.import
Normal file
24
assets/audio/sfx/invasion_alarm.wav.import
Normal file
@@ -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
|
||||
BIN
assets/audio/sfx/level_up.wav
Normal file
BIN
assets/audio/sfx/level_up.wav
Normal file
Binary file not shown.
24
assets/audio/sfx/level_up.wav.import
Normal file
24
assets/audio/sfx/level_up.wav.import
Normal file
@@ -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
|
||||
BIN
assets/audio/sfx/portal_spawn.wav
Normal file
BIN
assets/audio/sfx/portal_spawn.wav
Normal file
Binary file not shown.
24
assets/audio/sfx/portal_spawn.wav.import
Normal file
24
assets/audio/sfx/portal_spawn.wav.import
Normal file
@@ -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
|
||||
BIN
assets/audio/sfx/tavern_damage.wav
Normal file
BIN
assets/audio/sfx/tavern_damage.wav
Normal file
Binary file not shown.
24
assets/audio/sfx/tavern_damage.wav.import
Normal file
24
assets/audio/sfx/tavern_damage.wav.import
Normal file
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
19
autoloads/game_state.gd
Normal file
19
autoloads/game_state.gd
Normal file
@@ -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
|
||||
1
autoloads/game_state.gd.uid
Normal file
1
autoloads/game_state.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c3jq4raqs0onf
|
||||
@@ -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)
|
||||
|
||||
38
autoloads/tavern_stats.gd
Normal file
38
autoloads/tavern_stats.gd
Normal file
@@ -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()
|
||||
1
autoloads/tavern_stats.gd.uid
Normal file
1
autoloads/tavern_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://822h8c1pur1a
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
16
scenes/dungeon/dungeon_manager.gd
Normal file
16
scenes/dungeon/dungeon_manager.gd
Normal file
@@ -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)
|
||||
1
scenes/dungeon/dungeon_manager.gd.uid
Normal file
1
scenes/dungeon/dungeon_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bfkxrflfn5qx4
|
||||
@@ -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
|
||||
|
||||
21
scenes/menu/game_over_overlay.gd
Normal file
21
scenes/menu/game_over_overlay.gd
Normal file
@@ -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")
|
||||
1
scenes/menu/game_over_overlay.gd.uid
Normal file
1
scenes/menu/game_over_overlay.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dm00anoh5wtyu
|
||||
41
scenes/menu/game_over_overlay.tscn
Normal file
41
scenes/menu/game_over_overlay.tscn
Normal file
@@ -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")
|
||||
32
scenes/menu/main_menu.gd
Normal file
32
scenes/menu/main_menu.gd
Normal file
@@ -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()
|
||||
1
scenes/menu/main_menu.gd.uid
Normal file
1
scenes/menu/main_menu.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b4m6byh4k2mg7
|
||||
57
scenes/menu/main_menu.tscn
Normal file
57
scenes/menu/main_menu.tscn
Normal file
@@ -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")
|
||||
@@ -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])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
9
scenes/portal/red_portal_stats.tres
Normal file
9
scenes/portal/red_portal_stats.tres
Normal file
@@ -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
|
||||
10
scenes/tavern/init.gd
Normal file
10
scenes/tavern/init.gd
Normal file
@@ -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)
|
||||
1
scenes/tavern/init.gd.uid
Normal file
1
scenes/tavern/init.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dd103qxf2s5i5
|
||||
66
scenes/tavern/tavern.tscn
Normal file
66
scenes/tavern/tavern.tscn
Normal file
@@ -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
|
||||
2
scenes/tavern/tavern_stats.gd
Normal file
2
scenes/tavern/tavern_stats.gd
Normal file
@@ -0,0 +1,2 @@
|
||||
extends BaseStats
|
||||
class_name TavernStats
|
||||
1
scenes/tavern/tavern_stats.gd.uid
Normal file
1
scenes/tavern/tavern_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://duw4m3mhgmixk
|
||||
7
scenes/tavern/tavern_stats.tres
Normal file
7
scenes/tavern/tavern_stats.tres
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
21
scenes/world/world_manager.gd
Normal file
21
scenes/world/world_manager.gd
Normal file
@@ -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)
|
||||
1
scenes/world/world_manager.gd.uid
Normal file
1
scenes/world/world_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cejlqodm01ob3
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
113
systems/audio_system.gd
Normal 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")
|
||||
1
systems/audio_system.gd.uid
Normal file
1
systems/audio_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cbfc1ys0i4svm
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
88
systems/invasion_system.gd
Normal file
88
systems/invasion_system.gd
Normal 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()
|
||||
1
systems/invasion_system.gd.uid
Normal file
1
systems/invasion_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://841gb4nrydai
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
47
systems/wave_system.gd
Normal 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)
|
||||
1
systems/wave_system.gd.uid
Normal file
1
systems/wave_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://chfcocmkb0wnp
|
||||
19
systems/xp_system.gd
Normal file
19
systems/xp_system.gd
Normal 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
1
systems/xp_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://clivdryqcvfmh
|
||||
Reference in New Issue
Block a user