prototype vibe

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

View File

@@ -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")

View 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)

View File

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

View File

@@ -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

View 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")

View File

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

View 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
View 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()

View File

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

View 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")

View File

@@ -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])

View File

@@ -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:

View File

@@ -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)

View File

@@ -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]

View 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
View 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)

View File

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

66
scenes/tavern/tavern.tscn Normal file
View 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

View File

@@ -0,0 +1,2 @@
extends BaseStats
class_name TavernStats

View File

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

View 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

View File

@@ -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

View File

@@ -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")]

View 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)

View File

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