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

View File

@@ -1,266 +1,133 @@
[gd_scene format=3]
[gd_scene load_steps=23 format=3 uid="uid://b0dungeon0001"]
[ext_resource type="PackedScene" path="res://scenes/player/player.tscn" id="player"]
[ext_resource type="PackedScene" path="res://scenes/hud/hud.tscn" id="hud"]
[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"]
[ext_resource type="Script" path="res://systems/heal_system.gd" id="heal_system"]
[ext_resource type="Script" path="res://systems/shield_system.gd" id="shield_system"]
[ext_resource type="Script" path="res://systems/role_system.gd" id="role_system"]
[ext_resource type="Script" path="res://systems/ability_system.gd" id="ability_system"]
[ext_resource type="Script" path="res://systems/attack_system.gd" id="attack_system"]
[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="cooldown_system"]
[ext_resource type="Script" path="res://systems/targeting_system.gd" id="targeting_system"]
[ext_resource type="Script" path="res://systems/aggro/aggro_system.gd" id="aggro_system"]
[ext_resource type="Script" path="res://systems/aggro/aggro_tracker.gd" id="aggro_tracker"]
[ext_resource type="Script" path="res://systems/aggro/aggro_decay.gd" id="aggro_decay"]
[ext_resource type="Script" path="res://systems/aggro/aggro_events.gd" id="aggro_events"]
[ext_resource type="Script" path="res://systems/ai_system.gd" id="ai_system"]
[ext_resource type="Script" path="res://systems/respawn_system.gd" id="respawn_system"]
[ext_resource type="Script" path="res://systems/spawn_system.gd" id="spawn_system"]
[ext_resource type="Script" path="res://systems/aura_system.gd" id="aura_system"]
[ext_resource type="Script" path="res://systems/buff_system.gd" id="buff_system"]
[ext_resource type="Script" path="res://systems/debuff_system.gd" id="debuff_system"]
[ext_resource type="Script" path="res://systems/element_system.gd" id="element_system"]
[ext_resource type="Script" path="res://systems/hud_system.gd" id="hud_system"]
[ext_resource type="Script" path="res://systems/nameplate_system.gd" id="nameplate_system"]
[ext_resource type="Resource" uid="uid://cgxtn7dfs40bh" path="res://scenes/player/role/tank/set.tres" id="tank_set"]
[ext_resource type="Resource" uid="uid://beodknb6i1pm4" path="res://scenes/player/role/damage/set.tres" id="damage_set"]
[ext_resource type="Resource" uid="uid://kcwuhnqy34mj" path="res://scenes/player/role/healer/set.tres" id="healer_set"]
[ext_resource type="Script" path="res://scenes/dungeon/dungeon_manager.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://b0player00001" path="res://scenes/entities/player/player.tscn" id="2"]
[ext_resource type="Script" path="res://systems/health_system.gd" id="4"]
[ext_resource type="Script" path="res://systems/shield_system.gd" id="5"]
[ext_resource type="Script" path="res://systems/respawn_system.gd" id="6"]
[ext_resource type="Script" path="res://systems/cooldown_system.gd" id="7"]
[ext_resource type="Script" path="res://systems/role_system.gd" id="8"]
[ext_resource type="Script" path="res://systems/effect_system.gd" id="9"]
[ext_resource type="Script" path="res://systems/element_system.gd" id="10"]
[ext_resource type="Script" path="res://systems/aggro_system.gd" id="11"]
[ext_resource type="Script" path="res://systems/combat/ability_system.gd" id="12"]
[ext_resource type="Script" path="res://systems/combat/auto_attack_system.gd" id="13"]
[ext_resource type="Script" path="res://systems/spawn_system.gd" id="14"]
[ext_resource type="Script" path="res://systems/xp_system.gd" id="17"]
[ext_resource type="Script" path="res://systems/loot_system.gd" id="18"]
[ext_resource type="Script" path="res://systems/inventory_system.gd" id="19"]
[ext_resource type="Script" path="res://systems/chat_system.gd" id="24"]
[ext_resource type="Script" path="res://systems/map_system.gd" id="25"]
[ext_resource type="PackedScene" uid="uid://b0hud00001" path="res://scenes/hud/hud.tscn" id="26"]
[ext_resource type="Script" path="res://systems/audio_system.gd" id="27"]
[sub_resource type="NavigationMesh" id="NavigationMesh_1"]
vertices = PackedVector3Array(-7.0, 0.5, -7.0, -7.0, 0.5, 87.0, 7.0, 0.5, 87.0, 7.0, 0.5, -7.0)
polygons = [PackedInt32Array(3, 2, 0), PackedInt32Array(0, 2, 1)]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_1"]
sky_top_color = Color(0.1, 0.05, 0.1, 1)
sky_horizon_color = Color(0.15, 0.05, 0.05, 1)
ground_horizon_color = Color(0.1, 0.05, 0.05, 1)
ground_bottom_color = Color(0.0, 0.0, 0.0, 1)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
albedo_color = Color(0.2, 0.18, 0.15, 1)
[sub_resource type="Sky" id="Sky_1"]
sky_material = SubResource("ProceduralSkyMaterial_1")
[sub_resource type="PlaneMesh" id="PlaneMesh_1"]
material = SubResource("StandardMaterial3D_floor")
size = Vector2(15, 90)
[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_1"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wall"]
albedo_color = Color(0.25, 0.22, 0.2, 1)
[sub_resource type="BoxMesh" id="BoxMesh_north_south"]
material = SubResource("StandardMaterial3D_wall")
size = Vector3(15, 3, 0.5)
[sub_resource type="BoxShape3D" id="BoxShape3D_north_south"]
size = Vector3(15, 3, 0.5)
[sub_resource type="BoxMesh" id="BoxMesh_east_west"]
material = SubResource("StandardMaterial3D_wall")
size = Vector3(0.5, 3, 90)
[sub_resource type="BoxShape3D" id="BoxShape3D_east_west"]
size = Vector3(0.5, 3, 90)
[sub_resource type="Environment" id="Environment_1"]
background_mode = 2
sky = SubResource("Sky_1")
ambient_light_source = 3
ambient_light_color = Color(0.2, 0.18, 0.2, 1)
ambient_light_energy = 0.4
[node name="Dungeon" type="Node3D"]
script = ExtResource("1")
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_1")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 30, 0)
light_energy = 0.7
[node name="DungeonGeometry" type="Node3D" parent="."]
[node name="Systems" type="Node" parent="."]
[node name="HealthSystem" type="Node" parent="Systems"]
script = ExtResource("health_system")
[node name="DamageSystem" type="Node" parent="Systems"]
script = ExtResource("damage_system")
[node name="HealSystem" type="Node" parent="Systems"]
script = ExtResource("heal_system")
script = ExtResource("4")
[node name="ShieldSystem" type="Node" parent="Systems"]
script = ExtResource("shield_system")
[node name="RoleSystem" type="Node" parent="Systems"]
script = ExtResource("role_system")
tank_set = ExtResource("tank_set")
damage_set = ExtResource("damage_set")
healer_set = ExtResource("healer_set")
[node name="AbilitySystem" type="Node" parent="Systems"]
script = ExtResource("ability_system")
[node name="AttackSystem" type="Node" parent="Systems"]
script = ExtResource("attack_system")
[node name="CooldownSystem" type="Node" parent="Systems"]
script = ExtResource("cooldown_system")
[node name="TargetingSystem" type="Node" parent="Systems"]
script = ExtResource("targeting_system")
[node name="AggroSystem" type="Node" parent="Systems"]
script = ExtResource("aggro_system")
[node name="AggroTracker" type="Node" parent="Systems/AggroSystem"]
script = ExtResource("aggro_tracker")
[node name="AggroDecay" type="Node" parent="Systems/AggroSystem"]
script = ExtResource("aggro_decay")
[node name="AggroEvents" type="Node" parent="Systems/AggroSystem"]
script = ExtResource("aggro_events")
[node name="AISystem" type="Node" parent="Systems"]
script = ExtResource("ai_system")
script = ExtResource("5")
[node name="RespawnSystem" type="Node" parent="Systems"]
script = ExtResource("respawn_system")
script = ExtResource("6")
[node name="SpawnSystem" type="Node" parent="Systems"]
script = ExtResource("spawn_system")
[node name="CooldownSystem" type="Node" parent="Systems"]
script = ExtResource("7")
[node name="AuraSystem" type="Node" parent="Systems"]
script = ExtResource("aura_system")
[node name="RoleSystem" type="Node" parent="Systems"]
script = ExtResource("8")
[node name="BuffSystem" type="Node" parent="Systems"]
script = ExtResource("buff_system")
[node name="DebuffSystem" type="Node" parent="Systems"]
script = ExtResource("debuff_system")
[node name="EffectSystem" type="Node" parent="Systems"]
script = ExtResource("9")
[node name="ElementSystem" type="Node" parent="Systems"]
script = ExtResource("element_system")
script = ExtResource("10")
[node name="HudSystem" type="Node" parent="Systems"]
script = ExtResource("hud_system")
[node name="AggroSystem" type="Node" parent="Systems"]
script = ExtResource("11")
[node name="NameplateSystem" type="Node" parent="Systems"]
script = ExtResource("nameplate_system")
[node name="AbilitySystem" type="Node" parent="Systems"]
script = ExtResource("12")
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
navigation_mesh = SubResource("NavigationMesh_1")
[node name="AutoAttackSystem" type="Node" parent="Systems"]
script = ExtResource("13")
[node name="Boden" type="MeshInstance3D" parent="NavigationRegion3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 40)
mesh = SubResource("PlaneMesh_1")
[node name="SpawnSystem" type="Node" parent="Systems"]
script = ExtResource("14")
[node name="BodenCollision" type="StaticBody3D" parent="."]
[node name="CollisionShape3D" type="CollisionShape3D" parent="BodenCollision"]
shape = SubResource("WorldBoundaryShape3D_1")
[node name="WallSouth" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -5.25)
[node name="Mesh" type="MeshInstance3D" parent="WallSouth"]
mesh = SubResource("BoxMesh_north_south")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallSouth"]
shape = SubResource("BoxShape3D_north_south")
[node name="WallNorth" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 85.25)
[node name="Mesh" type="MeshInstance3D" parent="WallNorth"]
mesh = SubResource("BoxMesh_north_south")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallNorth"]
shape = SubResource("BoxShape3D_north_south")
[node name="WallEast" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.75, 1.5, 40)
[node name="Mesh" type="MeshInstance3D" parent="WallEast"]
mesh = SubResource("BoxMesh_east_west")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallEast"]
shape = SubResource("BoxShape3D_east_west")
[node name="WallWest" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.75, 1.5, 40)
[node name="Mesh" type="MeshInstance3D" parent="WallWest"]
mesh = SubResource("BoxMesh_east_west")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WallWest"]
shape = SubResource("BoxShape3D_east_west")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 40)
light_energy = 0.6
shadow_enabled = true
[node name="Player" parent="." instance=ExtResource("player")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -3)
[node name="HUD" parent="." instance=ExtResource("hud")]
[node name="Enemy1a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 15)
[node name="Enemy1b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 15)
[node name="Enemy1c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 15)
[node name="Enemy1d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 15)
[node name="Enemy2a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 30)
[node name="Enemy2b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 30)
[node name="Enemy2c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 30)
[node name="Enemy2d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 30)
[node name="Enemy3a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 45)
[node name="Enemy3b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 45)
[node name="Enemy3c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 45)
[node name="Enemy3d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 45)
[node name="Enemy4a" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 60)
[node name="Enemy4b" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0, 60)
[node name="Enemy4c" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 60)
[node name="Enemy4d" parent="." instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 60)
[node name="Boss" parent="." groups=["boss"] instance=ExtResource("enemy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 75)
stats = ExtResource("boss_stats")
[node name="ExitGate" parent="." instance=ExtResource("gate")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -4)
target_scene = "res://scenes/world/world.tscn"
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="LootSystem" type="Node" parent="Systems"]
script = ExtResource("18")
[node name="XpSystem" type="Node" parent="Systems"]
script = ExtResource("xp_system")
script = ExtResource("17")
[node name="DungeonManager" type="Node" parent="."]
script = ExtResource("dungeon_manager")
[node name="InventorySystem" type="Node" parent="Systems"]
script = ExtResource("19")
[node name="ChatSystem" type="Node" parent="Systems"]
script = ExtResource("24")
[node name="MapSystem" type="Node" parent="Systems"]
script = ExtResource("25")
[node name="AudioSystem" type="Node" parent="Systems"]
script = ExtResource("27")
[node name="EntityRoot" type="Node3D" parent="."]
[node name="Players" type="Node3D" parent="EntityRoot"]
[node name="Enemies" type="Node3D" parent="EntityRoot"]
[node name="Loot" type="Node3D" parent="EntityRoot"]
[node name="Portals" type="Node3D" parent="EntityRoot"]
[node name="Buildings" type="Node3D" parent="EntityRoot"]
[node name="Gates" type="Node3D" parent="EntityRoot"]
[node name="Npcs" type="Node3D" parent="EntityRoot"]
[node name="PlayerSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/entities/player/player.tscn")
spawn_path = NodePath("../EntityRoot/Players")
[node name="EnemySpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/entities/enemy/enemy.tscn")
spawn_path = NodePath("../EntityRoot/Enemies")
[node name="LootSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/entities/loot/loot_drop.tscn")
spawn_path = NodePath("../EntityRoot/Loot")
[node name="HUD" parent="." instance=ExtResource("26")]

View File

@@ -0,0 +1,93 @@
extends Node
const ROOM_HEIGHT: float = 4.0
const WALL_THICKNESS: float = 0.4
const CORRIDOR_WIDTH: float = 4.0
var rng: RandomNumberGenerator
var rooms: Array = []
func generate(parent: Node3D, seed: int, scale_difficulty: float = 1.0) -> Dictionary:
rng = RandomNumberGenerator.new()
rng.seed = seed
rooms.clear()
var room_count: int = rng.randi_range(5, 8)
var pos := Vector3(0, 0, 0)
var dir := Vector3(0, 0, -1)
for i in range(room_count):
var w: float = rng.randf_range(8.0, 14.0)
var d: float = rng.randf_range(8.0, 14.0)
rooms.append({"pos": pos, "size": Vector3(w, ROOM_HEIGHT, d)})
if i == room_count - 1:
break
var corridor_len: float = rng.randf_range(4.0, 8.0)
var step: Vector3 = dir * (max(w, d) * 0.5 + corridor_len + 4.0)
pos += step
if rng.randf() < 0.5:
var rotate_left: bool = rng.randf() < 0.5
dir = dir.rotated(Vector3.UP, PI * 0.5 * (1 if rotate_left else -1))
_build_geometry(parent)
return {"rooms": rooms, "spawn": rooms[0].pos + Vector3(0, 1, 0), "boss": rooms[-1].pos}
func _build_geometry(parent: Node3D) -> void:
for r in rooms:
_build_room(parent, r.pos, r.size)
for i in range(rooms.size() - 1):
_build_corridor(parent, rooms[i].pos, rooms[i + 1].pos)
func _build_room(parent: Node3D, center: Vector3, size: Vector3) -> void:
_add_floor(parent, center, Vector2(size.x, size.z))
var hw: float = size.x * 0.5
var hd: float = size.z * 0.5
_add_wall(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, -hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS))
_add_wall(parent, center + Vector3(0, ROOM_HEIGHT * 0.5, hd), Vector3(size.x, ROOM_HEIGHT, WALL_THICKNESS))
_add_wall(parent, center + Vector3(-hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z))
_add_wall(parent, center + Vector3(hw, ROOM_HEIGHT * 0.5, 0), Vector3(WALL_THICKNESS, ROOM_HEIGHT, size.z))
func _build_corridor(parent: Node3D, from: Vector3, to: Vector3) -> void:
var mid: Vector3 = (from + to) * 0.5
var d: float = from.distance_to(to)
var dir: Vector3 = (to - from).normalized()
_add_floor(parent, Vector3(mid.x, 0, mid.z), Vector2(d, CORRIDOR_WIDTH) if abs(dir.x) > abs(dir.z) else Vector2(CORRIDOR_WIDTH, d))
func _add_floor(parent: Node3D, center: Vector3, size: Vector2) -> void:
var body := StaticBody3D.new()
body.collision_layer = 1
body.collision_mask = 0
var mesh := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(size.x, 0.4, size.y)
mesh.mesh = box
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.25, 0.22, 0.2)
mesh.material_override = mat
mesh.position = Vector3(0, -0.2, 0)
var col := CollisionShape3D.new()
var shape := BoxShape3D.new()
shape.size = Vector3(size.x, 0.4, size.y)
col.shape = shape
col.position = Vector3(0, -0.2, 0)
body.add_child(mesh)
body.add_child(col)
body.position = center
parent.add_child(body)
func _add_wall(parent: Node3D, center: Vector3, size: Vector3) -> void:
var body := StaticBody3D.new()
body.collision_layer = 1
body.collision_mask = 0
var mesh := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = size
mesh.mesh = box
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.35, 0.32, 0.28)
mesh.material_override = mat
var col := CollisionShape3D.new()
var shape := BoxShape3D.new()
shape.size = size
col.shape = shape
body.add_child(mesh)
body.add_child(col)
body.position = center
parent.add_child(body)

View File

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

View File

@@ -1,16 +1,82 @@
extends Node
extends Node3D
const PLAYER_SCENE: PackedScene = preload("res://scenes/entities/player/player.tscn")
const ENEMY_SCENE: PackedScene = preload("res://scenes/entities/enemy/enemy.tscn")
const GENERATOR: GDScript = preload("res://scenes/dungeon/dungeon_generator.gd")
@onready var players_root: Node3D = $EntityRoot/Players
@onready var dungeon_root: Node3D = $DungeonGeometry
@onready var spawn_system: Node = $Systems/SpawnSystem
var generator: Node
var data: Dictionary
func _ready() -> void:
call_deferred("_scale_dungeon")
add_to_group("dungeon")
generator = GENERATOR.new()
add_child(generator)
data = generator.generate(dungeon_root, GameState.dungeon_seed, GameState.current_wave * (5.0 if GameState.dungeon_red else 1.0))
EventBus.boss_defeated.connect(_on_boss_defeated)
Net.world_ready.connect(_on_world_ready)
Net.peer_world_loaded.connect(_on_peer_world_loaded)
if Net.is_host():
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
Net.reset_world_ready()
Net.mark_world_loaded()
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)
func _exit_tree() -> void:
if Net.world_ready.is_connected(_on_world_ready):
Net.world_ready.disconnect(_on_world_ready)
if Net.peer_world_loaded.is_connected(_on_peer_world_loaded):
Net.peer_world_loaded.disconnect(_on_peer_world_loaded)
func _on_world_ready() -> void:
if not Net.is_host():
return
_spawn_player(1)
for id in multiplayer.get_peers():
_spawn_player(id)
_populate_dungeon()
func _populate_dungeon() -> void:
var difficulty: float = GameState.current_wave * (3.0 if GameState.dungeon_red else 1.0)
for i in range(data.rooms.size() - 1):
var room: Dictionary = data.rooms[i]
var n: int = 2 + (1 if GameState.dungeon_red else 0)
for j in range(n):
var off := Vector3(randf_range(-room.size.x * 0.3, room.size.x * 0.3), 0.5, randf_range(-room.size.z * 0.3, room.size.z * 0.3))
spawn_system.spawn_enemy_at(room.pos + off, GameState.dungeon_red, difficulty * 0.5)
spawn_system.spawn_boss_at(data.boss + Vector3(0, 0.5, 0), difficulty)
func _spawn_player(peer_id: int) -> void:
if players_root.get_node_or_null(str(peer_id)) != null:
return
var p: CharacterBody3D = PLAYER_SCENE.instantiate()
p.name = str(peer_id)
players_root.add_child(p, true)
p.global_position = data.spawn + Vector3(randf_range(-1, 1), 0, randf_range(-1, 1))
func _on_peer_connected(id: int) -> void:
Net.tell_peer_to_load_scene(id, GameState.SCENE_DUNGEON)
func _on_peer_world_loaded(peer_id: int) -> void:
if not Net.is_host():
return
_spawn_player(peer_id)
func _on_peer_disconnected(id: int) -> void:
var node := players_root.get_node_or_null(str(id))
if node:
node.queue_free()
func _on_boss_defeated(_b: Node) -> void:
if Net.is_host():
var t := get_tree().create_timer(2.0)
t.timeout.connect(func():
GameState.dungeon_seed = 0
_return.rpc())
@rpc("authority", "reliable", "call_local")
func _return() -> void:
GameState.change_scene(GameState.SCENE_WORLD)

View File

@@ -1 +1 @@
uid://bfkxrflfn5qx4
uid://civwsilci7nlx

View File

@@ -1,2 +0,0 @@
extends EnemyStats
class_name BossStats

View File

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

View File

@@ -1,20 +0,0 @@
[gd_resource type="Resource" script_class="BossStats" load_steps=2 format=3]
[ext_resource type="Script" path="res://scenes/enemy/boss_stats.gd" id="1"]
[resource]
script = ExtResource("1")
max_health = 500.0
health_regen = 0.0
max_shield = 100.0
shield_regen_delay = 5.0
shield_regen_time = 8.0
speed = 3.0
attack_range = 2.0
attack_cooldown = 1.5
attack_damage = 5.0
regen_fast = 0.1
regen_slow = 0.01
aggro_decay = 1.0
portal_radius = 10.0
alert_radius = 10.0

View File

@@ -1,11 +0,0 @@
extends Node
@onready var entity: CharacterBody3D = get_parent()
func _on_detection_area_body_entered(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":
EventBus.enemy_detected.emit(entity, body)
func _on_detection_area_body_exited(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":
EventBus.enemy_lost.emit(entity, body)

View File

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

View File

@@ -1,96 +0,0 @@
[gd_scene format=3 uid="uid://db8pa55ev4l4a"]
[ext_resource type="Script" uid="uid://vy6hyqok0p8b" path="res://scenes/enemy/init.gd" id="1"]
[ext_resource type="Script" uid="uid://b07aajhufqvb3" path="res://scenes/enemy/detection.gd" id="2"]
[ext_resource type="Resource" uid="uid://cj1shmjwf0xeo" path="res://scenes/enemy/enemy_stats.tres" id="8"]
[ext_resource type="PackedScene" path="res://assets/models/characters/Skeleton_Minion.glb" id="9"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
radius = 0.4
height = 1.5
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2"]
radius = 0.4
height = 1.5
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 10.0
[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.2, 0.8, 0.2, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"]
bg_color = Color(0.1, 0.1, 0.3, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
bg_color = Color(0.2, 0.5, 0.9, 1)
[node name="Enemy" type="CharacterBody3D" unique_id=1724620529]
script = ExtResource("1")
stats = ExtResource("8")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1011138038]
shape = SubResource("CapsuleShape3D_1")
[node name="Mesh" type="Node3D" parent="." unique_id=1598094615]
[node name="Model" parent="Mesh" instance=ExtResource("9")]
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, -0.75, 0)
[node name="HitArea" type="Area3D" parent="." unique_id=893463784]
collision_layer = 4
collision_mask = 0
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=984781962]
shape = SubResource("CapsuleShape3D_2")
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=440641945]
[node name="Detection" type="Node" parent="." unique_id=534240144]
script = ExtResource("2")
[node name="DetectionArea" type="Area3D" parent="." unique_id=1955178598]
collision_layer = 0
[node name="CollisionShape3D" type="CollisionShape3D" parent="DetectionArea" unique_id=557461347]
shape = SubResource("SphereShape3D_1")
[node name="Healthbar" type="Sprite3D" parent="." unique_id=1008728031]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
billboard = 1
[node name="SubViewport" type="SubViewport" parent="Healthbar" unique_id=1219060718]
transparent_bg = true
size = Vector2i(104, 29)
[node name="Border" type="ColorRect" parent="Healthbar/SubViewport" unique_id=848146848]
offset_right = 104.0
offset_bottom = 29.0
color = Color(1, 0.9, 0.2, 1)
[node name="HealthBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1206434403]
offset_left = 2.0
offset_top = 2.0
offset_right = 102.0
offset_bottom = 12.0
theme_override_styles/background = SubResource("StyleBoxFlat_health_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill")
value = 100.0
show_percentage = false
[node name="ShieldBar" type="ProgressBar" parent="Healthbar/SubViewport" unique_id=1891108036]
offset_left = 2.0
offset_top = 15.0
offset_right = 102.0
offset_bottom = 27.0
theme_override_styles/background = SubResource("StyleBoxFlat_shield_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_shield_fill")
max_value = 50.0
value = 50.0
show_percentage = false
[connection signal="body_entered" from="DetectionArea" to="Detection" method="_on_detection_area_body_entered"]
[connection signal="body_exited" from="DetectionArea" to="Detection" method="_on_detection_area_body_exited"]

View File

@@ -1,12 +0,0 @@
extends BaseStats
class_name EnemyStats
@export var speed := 3.0
@export var attack_range := 2.0
@export var attack_cooldown := 1.5
@export var attack_damage := 5.0
@export var regen_fast := 0.10
@export var regen_slow := 0.01
@export var aggro_decay := 1.0
@export var portal_radius := 10.0
@export var alert_radius := 10.0

View File

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

View File

@@ -1,7 +0,0 @@
[gd_resource type="Resource" script_class="EnemyStats" format=3 uid="uid://cj1shmjwf0xeo"]
[ext_resource type="Script" uid="uid://bh2uuuvl30y0x" path="res://scenes/enemy/enemy_stats.gd" id="1"]
[resource]
script = ExtResource("1")
max_shield = 50.0

View File

@@ -1,101 +0,0 @@
extends CharacterBody3D
const SKELETON_WARRIOR: PackedScene = preload("res://assets/models/characters/Skeleton_Warrior.glb")
const SKELETON_MAGE: PackedScene = preload("res://assets/models/characters/Skeleton_Mage.glb")
@export var stats: EnemyStats
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
var spawn_scale: float = 1.0
var anim_player: AnimationPlayer = null
var current_anim: String = ""
var attack_lock_until: float = 0.0
var is_dying: bool = false
func _ready() -> void:
add_to_group("enemies")
if is_in_group("boss"):
BossData.register(self, stats, spawn_scale)
BossData.set_stat(self, "spawn_position", global_position)
_swap_model(SKELETON_MAGE, 1.3)
else:
EnemyData.register(self, stats, spawn_scale)
EnemyData.set_stat(self, "spawn_position", global_position)
EventBus.entity_died.connect(_on_entity_died)
EventBus.attack_executed.connect(_on_attack_executed)
call_deferred("_check_variant")
call_deferred("_init_anim")
func _init_anim() -> void:
anim_player = get_node_or_null("Mesh/Model/AnimationPlayer")
_play_anim("Idle")
func _check_variant() -> void:
if is_in_group("boss"):
return
if is_in_group("red_enemies") or is_in_group("invasion"):
_swap_model(SKELETON_WARRIOR, 1.0)
anim_player = get_node_or_null("Mesh/Model/AnimationPlayer")
_play_anim("Idle")
func _swap_model(new_scene: PackedScene, scale_factor: float = 1.0) -> void:
var mesh: Node3D = get_node_or_null("Mesh")
if not mesh:
return
var old: Node = mesh.get_node_or_null("Model")
if old:
old.queue_free()
var new_model: Node3D = new_scene.instantiate()
new_model.name = "Model"
new_model.scale = Vector3(scale_factor, scale_factor, scale_factor)
new_model.position = Vector3(0, -0.75, 0)
new_model.rotation.y = PI
mesh.add_child(new_model)
func _exit_tree() -> void:
if is_in_group("boss"):
BossData.deregister(self)
else:
EnemyData.deregister(self)
func _on_entity_died(entity: Node) -> void:
if entity != self:
return
is_dying = true
_play_anim("Death_A", false)
get_tree().create_timer(1.0).timeout.connect(queue_free)
func _on_attack_executed(attacker: Node, _pos: Vector3, _dir: Vector3, _damage: float) -> void:
if attacker != self:
return
_play_anim("1H_Melee_Attack_Chop", false)
attack_lock_until = Time.get_ticks_msec() / 1000.0 + 0.5
func _process(_delta: float) -> void:
if is_dying:
return
var now: float = Time.get_ticks_msec() / 1000.0
if now < attack_lock_until:
return
if velocity.length() > 0.1:
_play_anim("Running_A")
else:
_play_anim("Idle")
func _play_anim(anim_name: String, loop: bool = true) -> void:
if not anim_player:
return
if current_anim == anim_name:
return
if not anim_player.has_animation(anim_name):
return
var anim: Animation = anim_player.get_animation(anim_name)
if anim:
anim.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE
anim_player.play(anim_name)
current_anim = anim_name
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
move_and_slide()

View File

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

View File

@@ -0,0 +1,36 @@
extends StaticBody3D
@export var building_id: StringName = &""
@onready var mesh: MeshInstance3D = $Mesh
@onready var collision: CollisionShape3D = $Collision
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("buildings")
apply_building(building_id)
func apply_building(id: StringName) -> void:
building_id = id
var data: Building = _load_building(id)
if data == null:
return
var box: BoxMesh = mesh.mesh as BoxMesh
if box:
box.size = data.size
var shape: BoxShape3D = collision.shape as BoxShape3D
if shape:
shape.size = data.size
collision.position = Vector3(0.0, data.size.y * 0.5, 0.0)
mesh.position = Vector3(0.0, data.size.y * 0.5, 0.0)
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = data.color
mesh.material_override = mat
static func _load_building(id: StringName) -> Building:
var path := "res://resources/buildings/%s.tres" % str(id)
if ResourceLoader.exists(path):
return load(path) as Building
return null

View File

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

View File

@@ -0,0 +1,22 @@
[gd_scene load_steps=4 format=3 uid="uid://b0building001"]
[ext_resource type="Script" path="res://scenes/entities/building/building.gd" id="1"]
[sub_resource type="BoxShape3D" id="BoxShape3D_1"]
size = Vector3(1, 1, 1)
[sub_resource type="BoxMesh" id="BoxMesh_1"]
size = Vector3(1, 1, 1)
[node name="Building" type="StaticBody3D"]
collision_layer = 16
collision_mask = 0
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
shape = SubResource("BoxShape3D_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
mesh = SubResource("BoxMesh_1")

View File

@@ -0,0 +1,170 @@
extends CharacterBody3D
const GRAVITY: float = 18.0
@export var stats_resource: EnemyStats
@export var is_boss: bool = false
@onready var nav: NavigationAgent3D = $NavAgent
@onready var mesh_holder: Node3D = $MeshHolder
@onready var collision: CollisionShape3D = $Collision
@onready var detection: Area3D = $DetectionArea
@onready var sync: MultiplayerSynchronizer = $Synchronizer
@onready var healthbar: MeshInstance3D = $Healthbar
@onready var name_label: Label3D = $NameLabel
var origin: Vector3 = Vector3.ZERO
var attack_cd: float = 0.0
var dead: bool = false
var invasion_target: Node = null
@export var sync_position: Vector3 = Vector3.ZERO
@export var sync_yaw: float = 0.0
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
if is_boss:
add_to_group("boss")
add_to_group("enemies")
if stats_resource == null:
stats_resource = EnemyStats.new()
Stats.register(self, stats_resource)
origin = global_position
detection.body_entered.connect(_on_body_entered)
EventBus.health_changed.connect(_on_health_changed)
EventBus.entity_died.connect(_on_entity_died)
name_label.text = "Boss" if is_boss else "Enemy"
if is_boss:
name_label.text = "Boss"
var mesh: MeshInstance3D = mesh_holder.get_node("Mesh")
mesh.scale = Vector3(1.5, 1.5, 1.5)
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = Color(0.6, 0.2, 0.8)
mesh.material_override = mat
func _exit_tree() -> void:
Stats.deregister(self)
func _physics_process(delta: float) -> void:
if not is_multiplayer_authority():
global_position = global_position.lerp(sync_position, clamp(delta * 20.0, 0.0, 1.0))
rotation.y = lerp_angle(rotation.y, sync_yaw, clamp(delta * 20.0, 0.0, 1.0))
return
if dead:
return
if not is_on_floor():
velocity.y -= GRAVITY * delta
attack_cd = max(0.0, attack_cd - delta)
var target: Node = _get_target()
if target == null:
_return_to_origin(delta)
else:
_chase_or_attack(target, delta)
move_and_slide()
sync_position = global_position
sync_yaw = rotation.y
func _get_target() -> Node:
var aggro: Node = get_node_or_null("/root/World/Systems/AggroSystem")
if aggro == null:
aggro = get_node_or_null("/root/Dungeon/Systems/AggroSystem")
if aggro and aggro.has_method("target_for"):
var t: Node = aggro.target_for(self)
if t:
return t
if invasion_target and is_instance_valid(invasion_target):
return invasion_target
return null
func _chase_or_attack(target: Node, delta: float) -> void:
var t_pos: Vector3 = (target as Node3D).global_position
var d: float = global_position.distance_to(t_pos)
var attack_range: float = float(Stats.get_stat(self, "attack_range", 2.0))
if d <= attack_range:
velocity.x = move_toward(velocity.x, 0.0, 20.0 * delta)
velocity.z = move_toward(velocity.z, 0.0, 20.0 * delta)
var look := Vector3(t_pos.x - global_position.x, 0.0, t_pos.z - global_position.z)
if look.length() > 0.01:
rotation.y = atan2(look.x, look.z)
if attack_cd <= 0.0:
attack_cd = float(Stats.get_stat(self, "attack_cooldown", 1.5))
var dmg: float = float(Stats.get_stat(self, "attack_damage", 5.0))
EventBus.damage_requested.emit(self, target, dmg, Element.NONE)
else:
var dir := Vector3(t_pos.x - global_position.x, 0.0, t_pos.z - global_position.z).normalized()
var speed: float = float(Stats.get_stat(self, "speed", 3.0))
velocity.x = dir.x * speed
velocity.z = dir.z * speed
if dir.length() > 0.01:
rotation.y = atan2(dir.x, dir.z)
func _return_to_origin(delta: float) -> void:
var d: float = global_position.distance_to(origin)
if d < 0.5:
velocity.x = move_toward(velocity.x, 0.0, 20.0 * delta)
velocity.z = move_toward(velocity.z, 0.0, 20.0 * delta)
var max_hp: float = float(Stats.get_stat(self, "max_health", 100.0))
var hp: float = float(Stats.get_stat(self, "health", 0.0))
if hp > 0.0 and hp < max_hp:
Stats.set_stat(self, "health", min(max_hp, hp + max_hp * 0.20 * delta))
EventBus.health_changed.emit(self, Stats.get_stat(self, "health"), max_hp)
else:
var dir: Vector3 = Vector3(origin.x - global_position.x, 0.0, origin.z - global_position.z).normalized()
var speed: float = float(Stats.get_stat(self, "speed", 3.0)) * 1.5
velocity.x = dir.x * speed
velocity.z = dir.z * speed
if dir.length() > 0.01:
rotation.y = atan2(dir.x, dir.z)
func _on_body_entered(body: Node) -> void:
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
return
if body.is_in_group("player") and not dead:
EventBus.enemy_detected.emit(self, body)
func _on_health_changed(entity: Node, current: float, max: float) -> void:
if entity != self:
return
var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0)
if healthbar:
healthbar.scale.x = max(0.01, ratio)
func _on_entity_died(entity: Node) -> void:
if entity != self or dead:
return
dead = true
if multiplayer.is_server() or multiplayer.multiplayer_peer == null:
_on_death.rpc()
var loot: Node = get_node_or_null("/root/World/Systems/LootSystem")
if loot == null:
loot = get_node_or_null("/root/Dungeon/Systems/LootSystem")
if loot and loot.has_method("drop_loot_for"):
loot.drop_loot_for(self, global_position)
var xp_sys: Node = get_node_or_null("/root/World/Systems/XpSystem")
if xp_sys == null:
xp_sys = get_node_or_null("/root/Dungeon/Systems/XpSystem")
if xp_sys and xp_sys.has_method("award_for_enemy"):
xp_sys.award_for_enemy(self)
if is_boss:
EventBus.boss_defeated.emit(self)
var t := get_tree().create_timer(2.0)
t.timeout.connect(func():
if is_instance_valid(self):
queue_free())
@rpc("any_peer", "reliable", "call_local")
func _on_death() -> void:
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
return
dead = true
collision.disabled = true
modulate_alpha(0.4)
func modulate_alpha(a: float) -> void:
for child in mesh_holder.get_children():
if child is MeshInstance3D and child.material_override:
var c: Color = child.material_override.albedo_color
c.a = a
child.material_override.albedo_color = c

View File

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

View File

@@ -0,0 +1,76 @@
[gd_scene load_steps=8 format=3 uid="uid://b0enemy00001"]
[ext_resource type="Script" path="res://scenes/entities/enemy/enemy.gd" id="1"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
height = 1.6
radius = 0.4
[sub_resource type="CapsuleMesh" id="CapsuleMesh_1"]
height = 1.6
radius = 0.4
[sub_resource type="StandardMaterial3D" id="Mat_1"]
albedo_color = Color(0.6, 0.6, 0.6, 1)
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 12.0
[sub_resource type="QuadMesh" id="QuadMesh_1"]
size = Vector2(1.0, 0.12)
[sub_resource type="StandardMaterial3D" id="HBMat"]
shading_mode = 0
no_depth_test = true
albedo_color = Color(0.9, 0.2, 0.2, 1)
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_1"]
properties/0/path = NodePath(".:sync_position")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:sync_yaw")
properties/1/spawn = true
properties/1/replication_mode = 1
[node name="Enemy" type="CharacterBody3D"]
collision_layer = 4
collision_mask = 1
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource("CapsuleShape3D_1")
[node name="MeshHolder" type="Node3D" parent="."]
[node name="Mesh" type="MeshInstance3D" parent="MeshHolder"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
mesh = SubResource("CapsuleMesh_1")
material_override = SubResource("Mat_1")
[node name="NameLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.2, 0)
billboard = 1
text = "Enemy"
font_size = 20
outline_size = 3
[node name="Healthbar" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.95, 0)
mesh = SubResource("QuadMesh_1")
material_override = SubResource("HBMat")
gi_mode = 0
[node name="DetectionArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 2
[node name="DetectionShape" type="CollisionShape3D" parent="DetectionArea"]
shape = SubResource("SphereShape3D_1")
[node name="NavAgent" type="NavigationAgent3D" parent="."]
path_desired_distance = 0.5
target_desired_distance = 0.5
[node name="Synchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_1")

View File

@@ -0,0 +1,80 @@
extends StaticBody3D
@export var stats_resource: GateStats
@export var is_red: bool = false
@onready var mesh: MeshInstance3D = $Mesh
@onready var healthbar: MeshInstance3D = $Healthbar
@onready var name_label: Label3D = $NameLabel
@onready var spawn_point: Node3D = $SpawnPoint
var spawn_timer: float = 0.0
var spawned_count: int = 0
var dead: bool = false
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("gates")
if is_red:
add_to_group("red_gate")
if stats_resource == null:
stats_resource = GateStats.new()
if is_red:
stats_resource.max_health = 600.0
stats_resource.spawn_count = 8
else:
stats_resource.max_health = 200.0
stats_resource.spawn_count = 5
stats_resource.is_red = is_red
Stats.register(self, stats_resource)
EventBus.health_changed.connect(_on_health_changed)
EventBus.entity_died.connect(_on_entity_died)
if is_red:
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = Color(0.95, 0.2, 0.15)
mat.emission_enabled = true
mat.emission = Color(1.0, 0.4, 0.2)
mat.emission_energy_multiplier = 0.6
mesh.material_override = mat
name_label.text = "Red Gate"
else:
name_label.text = "Gate"
set_physics_process(true)
func _exit_tree() -> void:
Stats.deregister(self)
func _physics_process(delta: float) -> void:
if dead:
return
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
return
spawn_timer = max(0.0, spawn_timer - delta)
if spawn_timer <= 0.0 and spawned_count < int(Stats.get_stat(self, "spawn_count", 5)):
var spawn_sys: Node = get_node_or_null("/root/World/Systems/SpawnSystem")
if spawn_sys and spawn_sys.has_method("spawn_enemy_at"):
spawn_sys.spawn_enemy_at(spawn_point.global_position + Vector3(randf_range(-1.5, 1.5), 0.0, randf_range(-1.5, 1.5)), is_red)
spawned_count += 1
spawn_timer = float(Stats.get_stat(self, "spawn_interval", 4.0))
func _on_health_changed(entity: Node, current: float, max: float) -> void:
if entity != self:
return
var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0)
healthbar.scale.x = max(0.01, ratio * 2.0)
func _on_entity_died(entity: Node) -> void:
if entity != self or dead:
return
dead = true
if multiplayer.is_server() or multiplayer.multiplayer_peer == null:
var spawn_sys: Node = get_node_or_null("/root/World/Systems/SpawnSystem")
if spawn_sys and spawn_sys.has_method("spawn_portal_at"):
spawn_sys.spawn_portal_at(global_position, is_red)
EventBus.gate_destroyed.emit(self)
var t := get_tree().create_timer(0.5)
t.timeout.connect(func():
if is_instance_valid(self):
queue_free())

View File

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

View File

@@ -0,0 +1,53 @@
[gd_scene load_steps=7 format=3 uid="uid://b0gate0001"]
[ext_resource type="Script" path="res://scenes/entities/gate/gate.gd" id="1"]
[sub_resource type="BoxShape3D" id="BoxShape3D_1"]
size = Vector3(2, 3, 2)
[sub_resource type="BoxMesh" id="BoxMesh_1"]
size = Vector3(2, 3, 2)
[sub_resource type="StandardMaterial3D" id="GateMat"]
albedo_color = Color(0.4, 0.4, 0.55, 1)
emission_enabled = true
emission = Color(0.2, 0.2, 0.4, 1)
emission_energy_multiplier = 0.4
[sub_resource type="QuadMesh" id="QuadMesh_HB"]
size = Vector2(1.0, 0.18)
[sub_resource type="StandardMaterial3D" id="HBMat"]
shading_mode = 0
no_depth_test = true
albedo_color = Color(0.95, 0.4, 0.2, 1)
[node name="Gate" type="StaticBody3D"]
collision_layer = 4
collision_mask = 0
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
shape = SubResource("BoxShape3D_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
mesh = SubResource("BoxMesh_1")
material_override = SubResource("GateMat")
[node name="NameLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.0, 0)
billboard = 1
text = "Gate"
font_size = 22
outline_size = 3
[node name="Healthbar" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.5, 0)
mesh = SubResource("QuadMesh_HB")
material_override = SubResource("HBMat")
gi_mode = 0
[node name="SpawnPoint" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3)

View File

@@ -0,0 +1,39 @@
extends Area3D
@onready var mesh: MeshInstance3D = $Mesh
@onready var label: Label3D = $Label
@export var item_id: StringName = &""
@export var amount: int = 1
var rotation_speed: float = 1.5
var bob: float = 0.0
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("loot")
body_entered.connect(_on_body_entered)
if item_id != &"":
label.text = "%s x%d" % [str(item_id), amount]
else:
label.text = "Loot"
func _process(delta: float) -> void:
bob += delta
mesh.rotation.y += rotation_speed * delta
mesh.position.y = 0.5 + sin(bob * 2.0) * 0.1
func _on_body_entered(body: Node) -> void:
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
return
if not body.is_in_group("player"):
return
var inv: Node = get_node_or_null("/root/World/Systems/InventorySystem")
if inv == null:
inv = get_node_or_null("/root/Dungeon/Systems/InventorySystem")
if inv and inv.has_method("add_item"):
inv.add_item(body, item_id, amount)
EventBus.item_picked_up.emit(body, item_id)
queue_free()

View File

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

View File

@@ -0,0 +1,35 @@
[gd_scene load_steps=4 format=3 uid="uid://b0loot0001"]
[ext_resource type="Script" path="res://scenes/entities/loot/loot_drop.gd" id="1"]
[sub_resource type="BoxMesh" id="BoxMesh_1"]
size = Vector3(0.4, 0.4, 0.4)
[sub_resource type="StandardMaterial3D" id="LootMat"]
albedo_color = Color(0.95, 0.85, 0.2, 1)
emission_enabled = true
emission = Color(1.0, 0.9, 0.4, 1)
emission_energy_multiplier = 0.6
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 1.2
[node name="LootDrop" type="Area3D"]
collision_layer = 0
collision_mask = 2
script = ExtResource("1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
mesh = SubResource("BoxMesh_1")
material_override = SubResource("LootMat")
[node name="Label" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
billboard = 1
text = "Loot"
font_size = 18
outline_size = 3
[node name="PickupShape" type="CollisionShape3D" parent="."]
shape = SubResource("SphereShape3D_1")

View File

@@ -0,0 +1,52 @@
extends StaticBody3D
@export var profile: NpcProfile
@export var profile_id: StringName = &""
@onready var mesh: MeshInstance3D = $Mesh
@onready var label: Label3D = $Label
@onready var prompt: Label3D = $Prompt
@onready var interact_area: Area3D = $InteractArea
var nearby_player: Node = null
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("npc")
if profile == null and profile_id != &"":
var path := "res://resources/npcs/%s.tres" % str(profile_id)
if ResourceLoader.exists(path):
profile = load(path) as NpcProfile
if profile == null:
profile = NpcProfile.new()
profile.display_name = "Villager"
profile.lore = "A simple villager."
profile.personality = "Friendly, curious."
profile.fallback_text = "Hello there!"
label.text = profile.display_name
prompt.visible = false
if profile.color != Color.WHITE:
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = profile.color
mesh.material_override = mat
interact_area.body_entered.connect(_on_player_near)
interact_area.body_exited.connect(_on_player_left)
func _process(_delta: float) -> void:
if nearby_player and nearby_player.is_multiplayer_authority():
prompt.visible = true
if Input.is_action_just_pressed("interact"):
EventBus.dialog_opened.emit(nearby_player, self)
else:
prompt.visible = false
func _on_player_near(body: Node) -> void:
if body.is_in_group("player"):
if body.is_multiplayer_authority():
nearby_player = body
func _on_player_left(body: Node) -> void:
if body == nearby_player:
nearby_player = null

View File

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

View File

@@ -0,0 +1,53 @@
[gd_scene load_steps=6 format=3 uid="uid://b0npc0001"]
[ext_resource type="Script" path="res://scenes/entities/npc/npc.gd" id="1"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
height = 1.7
radius = 0.35
[sub_resource type="CapsuleMesh" id="CapsuleMesh_1"]
height = 1.7
radius = 0.35
[sub_resource type="StandardMaterial3D" id="NpcMat"]
albedo_color = Color(0.85, 0.7, 0.5, 1)
[sub_resource type="SphereShape3D" id="InteractShape"]
radius = 2.5
[node name="Npc" type="StaticBody3D"]
collision_layer = 0
collision_mask = 0
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
shape = SubResource("CapsuleShape3D_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
mesh = SubResource("CapsuleMesh_1")
material_override = SubResource("NpcMat")
[node name="Label" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.2, 0)
billboard = 1
text = "Villager"
font_size = 22
outline_size = 3
modulate = Color(0.95, 0.85, 0.5, 1)
[node name="Prompt" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.55, 0)
billboard = 1
text = "[E] Talk"
font_size = 16
outline_size = 2
[node name="InteractArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 2
[node name="InteractShape" type="CollisionShape3D" parent="InteractArea"]
shape = SubResource("InteractShape")

View File

@@ -0,0 +1,242 @@
extends CharacterBody3D
const GRAVITY: float = 18.0
const MOUSE_SENS: float = 0.0035
@export var stats_resource: PlayerStats
@onready var pivot: Node3D = $Pivot
@onready var pitch_pivot: Node3D = $Pivot/PitchPivot
@onready var camera: Camera3D = $Pivot/PitchPivot/Camera
@onready var mesh_holder: Node3D = $MeshHolder
@onready var collision: CollisionShape3D = $Collision
@onready var sync: MultiplayerSynchronizer = $Synchronizer
@onready var name_label: Label3D = $NameLabel
var peer_id: int = 1
var role: int = GameState.ROLE_DAMAGE
var look_dragging: bool = false
var current_target: Node = null
var dead: bool = false
var ui_capturing: bool = false
var build_mode: bool = false
@export var sync_position: Vector3 = Vector3.ZERO
@export var sync_velocity: Vector3 = Vector3.ZERO
@export var sync_yaw: float = 0.0
@export var sync_role: int = GameState.ROLE_DAMAGE
func _enter_tree() -> void:
peer_id = name.to_int()
set_multiplayer_authority(peer_id)
func _ready() -> void:
add_to_group("player")
if stats_resource == null:
stats_resource = PlayerStats.new()
Stats.register(self, stats_resource)
Stats.set_stat(self, "role", role)
name_label.text = Net.player_names.get(peer_id, "P%d" % peer_id)
EventBus.entity_died.connect(_on_entity_died_clear_target)
if is_multiplayer_authority():
camera.current = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
camera.current = false
_apply_role_visual(role)
func _on_entity_died_clear_target(entity: Node) -> void:
if entity == current_target:
current_target = null
func _set_target(t: Node) -> void:
current_target = t
EventBus.target_changed.emit(self, t)
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and not multiplayer.is_server():
_request_target.rpc_id(1, String(t.get_path()) if t else "")
@rpc("any_peer", "reliable")
func _request_target(path_str: String) -> void:
if not multiplayer.is_server():
return
var t: Node = get_node_or_null(NodePath(path_str)) if path_str != "" else null
current_target = t
func _exit_tree() -> void:
Stats.deregister(self)
func _input(event: InputEvent) -> void:
if not is_multiplayer_authority():
return
if ui_capturing:
return
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
look_dragging = event.pressed
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if look_dragging else Input.MOUSE_MODE_VISIBLE
elif event is InputEventMouseMotion and look_dragging:
pivot.rotate_y(-event.relative.x * MOUSE_SENS)
pitch_pivot.rotate_x(-event.relative.y * MOUSE_SENS)
pitch_pivot.rotation.x = clamp(pitch_pivot.rotation.x, -1.2, 0.2)
func _unhandled_input(event: InputEvent) -> void:
if not is_multiplayer_authority():
return
if dead:
return
if build_mode:
return
if event.is_action_pressed("class_tank"):
_request_role(GameState.ROLE_TANK)
elif event.is_action_pressed("class_damage"):
_request_role(GameState.ROLE_DAMAGE)
elif event.is_action_pressed("class_healer"):
_request_role(GameState.ROLE_HEALER)
elif event.is_action_pressed("ability_1"):
EventBus.ability_use_requested.emit(self, 0)
elif event.is_action_pressed("ability_2"):
EventBus.ability_use_requested.emit(self, 1)
elif event.is_action_pressed("ability_3"):
EventBus.ability_use_requested.emit(self, 2)
elif event.is_action_pressed("ability_4"):
EventBus.ability_use_requested.emit(self, 3)
elif event.is_action_pressed("target_next"):
var nt := _cycle_target()
_set_target(nt)
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed and not look_dragging:
var t := _pick_target_under_mouse()
if t:
_set_target(t)
func _physics_process(delta: float) -> void:
if is_multiplayer_authority():
if not dead:
_process_local(delta)
sync_position = global_position
sync_velocity = velocity
sync_yaw = pivot.rotation.y
sync_role = role
else:
global_position = global_position.lerp(sync_position, clamp(delta * 20.0, 0.0, 1.0))
pivot.rotation.y = lerp_angle(pivot.rotation.y, sync_yaw, clamp(delta * 20.0, 0.0, 1.0))
if sync_role != role:
role = sync_role
_apply_role_visual(role)
func _process_local(delta: float) -> void:
if not is_on_floor():
velocity.y -= GRAVITY * delta
var move_dir: Vector2 = Input.get_vector("move_left", "move_right", "move_forward", "move_back") if not ui_capturing else Vector2.ZERO
var speed: float = float(Stats.get_stat(self, "speed", 5.0))
var basis_y := Basis(Vector3.UP, pivot.rotation.y)
var direction := basis_y * Vector3(move_dir.x, 0.0, move_dir.y)
if direction.length() > 0.01:
velocity.x = direction.x * speed
velocity.z = direction.z * speed
var look_dir := Vector3(velocity.x, 0.0, velocity.z).normalized()
var target_basis := Basis.looking_at(look_dir, Vector3.UP)
mesh_holder.basis = mesh_holder.basis.slerp(target_basis, clamp(delta * 12.0, 0.0, 1.0))
else:
velocity.x = move_toward(velocity.x, 0.0, speed * 6.0 * delta)
velocity.z = move_toward(velocity.z, 0.0, speed * 6.0 * delta)
if Input.is_action_just_pressed("jump") and is_on_floor() and not ui_capturing:
velocity.y = float(Stats.get_stat(self, "jump_velocity", 4.5))
move_and_slide()
func _request_role(new_role: int) -> void:
_set_role.rpc_id(1, new_role)
@rpc("any_peer", "reliable", "call_local")
func _set_role(new_role: int) -> void:
if not multiplayer.is_server():
return
if new_role == role:
return
role = new_role
Stats.set_stat(self, "role", role)
_apply_role.rpc(role)
@rpc("any_peer", "reliable", "call_local")
func _apply_role(new_role: int) -> void:
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
return
role = new_role
if Stats.has(self):
Stats.set_stat(self, "role", role)
_apply_role_visual(role)
EventBus.role_changed.emit(self, role)
func _apply_role_visual(r: int) -> void:
var mesh: MeshInstance3D = mesh_holder.get_node("Mesh")
var mat: StandardMaterial3D = mesh.get_active_material(0).duplicate() if mesh.get_active_material(0) else StandardMaterial3D.new()
match r:
GameState.ROLE_TANK:
mat.albedo_color = Color(0.3, 0.5, 0.95)
GameState.ROLE_DAMAGE:
mat.albedo_color = Color(0.95, 0.3, 0.3)
GameState.ROLE_HEALER:
mat.albedo_color = Color(0.4, 0.85, 0.4)
mesh.material_override = mat
@rpc("any_peer", "reliable", "call_local")
func set_dead(value: bool) -> void:
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
return
dead = value
visible = not value
collision.disabled = value
if value:
velocity = Vector3.ZERO
@rpc("any_peer", "reliable", "call_local")
func teleport_to(pos: Vector3) -> void:
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
return
global_position = pos
sync_position = pos
func set_ui_capturing(v: bool) -> void:
ui_capturing = v
func set_build_mode(v: bool) -> void:
build_mode = v
func _cycle_target() -> Node:
if current_target != null and not is_instance_valid(current_target):
current_target = null
var candidates: Array = []
for n in get_tree().get_nodes_in_group("enemies"):
if is_instance_valid(n):
candidates.append(n)
for n in get_tree().get_nodes_in_group("portals"):
if is_instance_valid(n):
candidates.append(n)
for n in get_tree().get_nodes_in_group("gates"):
if is_instance_valid(n):
candidates.append(n)
if candidates.is_empty():
current_target = null
return null
candidates.sort_custom(func(a, b):
return (a as Node3D).global_position.distance_to(global_position) < (b as Node3D).global_position.distance_to(global_position))
if current_target == null or not current_target in candidates:
current_target = candidates[0]
else:
var idx: int = candidates.find(current_target)
current_target = candidates[(idx + 1) % candidates.size()]
return current_target
func _pick_target_under_mouse() -> Node:
var mouse := get_viewport().get_mouse_position()
var from := camera.project_ray_origin(mouse)
var to := from + camera.project_ray_normal(mouse) * 100.0
var space := get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(from, to)
query.collision_mask = 0xFFFFFFFF
query.exclude = [self]
var hit := space.intersect_ray(query)
if hit.is_empty():
return null
var n: Node = hit.collider
while n != null and not (n.is_in_group("enemies") or n.is_in_group("portals") or n.is_in_group("gates") or n.is_in_group("npc")):
n = n.get_parent()
return n

View File

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

View File

@@ -0,0 +1,58 @@
[gd_scene load_steps=8 format=3 uid="uid://b0player00001"]
[ext_resource type="Script" path="res://scenes/entities/player/player.gd" id="1"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
height = 1.8
radius = 0.35
[sub_resource type="CapsuleMesh" id="CapsuleMesh_1"]
height = 1.8
radius = 0.35
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
albedo_color = Color(0.3, 0.55, 0.95, 1)
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_1"]
properties/0/path = NodePath(".:sync_position")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:sync_yaw")
properties/1/spawn = true
properties/1/replication_mode = 1
[node name="Player" type="CharacterBody3D"]
collision_layer = 2
collision_mask = 1
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
shape = SubResource("CapsuleShape3D_1")
[node name="MeshHolder" type="Node3D" parent="."]
[node name="Mesh" type="MeshInstance3D" parent="MeshHolder"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
mesh = SubResource("CapsuleMesh_1")
surface_material_override/0 = SubResource("StandardMaterial3D_1")
[node name="NameLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.0, 0)
billboard = 1
text = "Player"
font_size = 24
outline_size = 4
[node name="Pivot" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
[node name="PitchPivot" type="Node3D" parent="Pivot"]
[node name="Camera" type="Camera3D" parent="Pivot/PitchPivot"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 5)
current = false
fov = 70.0
[node name="Synchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_1")

View File

@@ -0,0 +1,62 @@
extends StaticBody3D
@export var stats_resource: PortalStats
@export var is_red: bool = false
@onready var mesh: MeshInstance3D = $Mesh
@onready var name_label: Label3D = $NameLabel
@onready var enter_area: Area3D = $EnterArea
var triggered: bool = false
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("portals")
if is_red:
add_to_group("red_portal")
if stats_resource == null:
stats_resource = PortalStats.new()
stats_resource.max_health = 1.0
stats_resource.is_red = is_red
Stats.register(self, stats_resource)
enter_area.body_entered.connect(_on_body_entered)
if is_red:
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = Color(0.95, 0.2, 0.2)
mat.emission_enabled = true
mat.emission = Color(1.0, 0.4, 0.3)
mat.emission_energy_multiplier = 1.5
mesh.material_override = mat
name_label.text = "Red Portal"
else:
name_label.text = "Portal"
func _exit_tree() -> void:
Stats.deregister(self)
func _on_body_entered(body: Node) -> void:
if not body.is_in_group("player"):
return
if not body.is_multiplayer_authority():
return
if triggered:
return
triggered = true
EventBus.portal_entered.emit(self, body)
_request_enter.rpc_id(1, is_red, global_position)
@rpc("any_peer", "reliable", "call_local")
func _request_enter(red: bool, return_pos: Vector3) -> void:
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
return
var seed: int = randi()
_do_enter.rpc(seed, red, return_pos)
@rpc("authority", "reliable", "call_local")
func _do_enter(seed: int, red: bool, return_pos: Vector3) -> void:
GameState.dungeon_seed = seed
GameState.dungeon_red = red
GameState.portal_return_position = return_pos
GameState.change_scene(GameState.SCENE_DUNGEON)

View File

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

View File

@@ -0,0 +1,49 @@
[gd_scene load_steps=6 format=3 uid="uid://b0portal0001"]
[ext_resource type="Script" path="res://scenes/entities/portal/portal.gd" id="1"]
[sub_resource type="CylinderShape3D" id="CylShape_1"]
height = 0.4
radius = 1.5
[sub_resource type="CylinderMesh" id="CylMesh_1"]
top_radius = 1.5
bottom_radius = 1.5
height = 3.0
[sub_resource type="StandardMaterial3D" id="PortalMat"]
albedo_color = Color(0.3, 0.5, 0.95, 1)
emission_enabled = true
emission = Color(0.5, 0.7, 1.0, 1)
emission_energy_multiplier = 1.5
[sub_resource type="SphereShape3D" id="SphereEnter"]
radius = 1.8
[node name="Portal" type="StaticBody3D"]
collision_layer = 8
collision_mask = 0
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.2, 0)
shape = SubResource("CylShape_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
mesh = SubResource("CylMesh_1")
material_override = SubResource("PortalMat")
[node name="NameLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.0, 0)
billboard = 1
text = "Portal"
font_size = 22
outline_size = 3
[node name="EnterArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 2
[node name="EnterShape" type="CollisionShape3D" parent="EnterArea"]
shape = SubResource("SphereEnter")

View File

@@ -0,0 +1,36 @@
extends StaticBody3D
@export var stats_resource: VillageStats
@onready var mesh: MeshInstance3D = $Mesh
@onready var label: Label3D = $Label
@onready var healthbar: MeshInstance3D = $Healthbar
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
add_to_group("village")
if stats_resource == null:
stats_resource = VillageStats.new()
stats_resource.max_health = 1000.0
Stats.register(self, stats_resource)
EventBus.health_changed.connect(_on_health_changed)
EventBus.entity_died.connect(_on_entity_died)
label.text = "Village"
func _exit_tree() -> void:
Stats.deregister(self)
func _on_health_changed(entity: Node, current: float, max: float) -> void:
if entity != self:
return
EventBus.village_damaged.emit(current, max)
var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0)
healthbar.scale.x = max(0.01, ratio * 4.0)
func _on_entity_died(entity: Node) -> void:
if entity != self:
return
EventBus.village_destroyed.emit()
EventBus.game_over.emit()

View File

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

View File

@@ -0,0 +1,47 @@
[gd_scene load_steps=6 format=3 uid="uid://b0village001"]
[ext_resource type="Script" path="res://scenes/entities/village/village.gd" id="1"]
[sub_resource type="BoxShape3D" id="BoxShape_1"]
size = Vector3(6, 4, 6)
[sub_resource type="BoxMesh" id="BoxMesh_1"]
size = Vector3(6, 4, 6)
[sub_resource type="StandardMaterial3D" id="VillageMat"]
albedo_color = Color(0.55, 0.4, 0.25, 1)
[sub_resource type="QuadMesh" id="QuadMesh_HB"]
size = Vector2(1.0, 0.25)
[sub_resource type="StandardMaterial3D" id="HBMat"]
shading_mode = 0
no_depth_test = true
albedo_color = Color(0.4, 0.85, 0.4, 1)
[node name="Village" type="StaticBody3D"]
collision_layer = 1
collision_mask = 0
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
shape = SubResource("BoxShape_1")
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
mesh = SubResource("BoxMesh_1")
material_override = SubResource("VillageMat")
[node name="Label" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.5, 0)
billboard = 1
text = "Village"
font_size = 28
outline_size = 4
[node name="Healthbar" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.0, 0)
mesh = SubResource("QuadMesh_HB")
material_override = SubResource("HBMat")
gi_mode = 0

497
scenes/hud/hud.gd Normal file
View File

@@ -0,0 +1,497 @@
extends CanvasLayer
@onready var hp_bar: ProgressBar = %HpBar
@onready var hp_label: Label = %HpLabel
@onready var shield_bar: ProgressBar = %ShieldBar
@onready var shield_label: Label = %ShieldLabel
@onready var xp_bar: ProgressBar = %XpBar
@onready var level_label: Label = %LevelLabel
@onready var wave_label: Label = %WaveLabel
@onready var timer_label: Label = %WaveTimer
@onready var village_bar: ProgressBar = %VillageBar
@onready var role_icon: Panel = %RoleIcon
@onready var role_label: Label = %RoleLabel
@onready var ability_box: HBoxContainer = %AbilityBox
@onready var death_overlay: Control = %DeathOverlay
@onready var death_label: Label = %DeathLabel
@onready var chat_log: RichTextLabel = %ChatLog
@onready var chat_input: LineEdit = %ChatInput
@onready var inventory_panel: Control = %InventoryPanel
@onready var inventory_list: VBoxContainer = %InventoryList
@onready var crafting_panel: Control = %CraftingPanel
@onready var crafting_list: VBoxContainer = %CraftingList
@onready var build_panel: Control = %BuildPanel
@onready var build_list: HBoxContainer = %BuildList
@onready var dialog_panel: Control = %DialogPanel
@onready var dialog_npc: Label = %DialogNpc
@onready var dialog_log: RichTextLabel = %DialogLog
@onready var dialog_input: LineEdit = %DialogInput
@onready var map_panel: Control = %MapPanel
@onready var map_canvas: Control = %MapCanvas
@onready var minimap_canvas: Control = %MinimapCanvas
@onready var pause_panel: Control = %PausePanel
@onready var game_over_overlay: Control = %GameOverOverlay
@onready var ability_buttons: Array = []
var local_player: Node = null
var dialog_npc_node: Node = null
var build_selected: int = 0
var build_rotation: float = 0.0
var build_preview: MeshInstance3D = null
var build_active: bool = false
var _minimap_accum: float = 0.0
func _ready() -> void:
add_to_group("dialog_ui")
EventBus.health_changed.connect(_on_health_changed)
EventBus.shield_changed.connect(_on_shield_changed)
EventBus.cooldown_tick.connect(_on_cooldown_tick)
EventBus.role_changed.connect(_on_role_changed)
EventBus.entity_died.connect(_on_entity_died)
EventBus.entity_respawned.connect(_on_respawned)
EventBus.wave_started.connect(_on_wave_started)
EventBus.wave_timer_tick.connect(_on_wave_tick)
EventBus.village_damaged.connect(_on_village_damaged)
EventBus.village_destroyed.connect(_on_village_destroyed)
EventBus.invasion_started.connect(_on_invasion_started)
EventBus.xp_gained.connect(_on_xp_gained)
EventBus.level_up.connect(_on_level_up)
EventBus.dialog_opened.connect(_on_dialog_opened)
EventBus.inventory_changed.connect(_on_inventory_changed)
EventBus.chat_message.connect(_on_chat)
chat_input.text_submitted.connect(_on_chat_submitted)
dialog_input.text_submitted.connect(_on_dialog_submitted)
death_overlay.visible = false
inventory_panel.visible = false
crafting_panel.visible = false
build_panel.visible = false
dialog_panel.visible = false
map_panel.visible = false
pause_panel.visible = false
game_over_overlay.visible = false
set_process(true)
_wire_ability_buttons()
call_deferred("_populate_build_list")
func _process(_delta: float) -> void:
if local_player == null:
local_player = _find_local_player()
if local_player:
var role: int = int(Stats.get_stat(local_player, "role", GameState.ROLE_DAMAGE))
_on_role_changed(local_player, role)
_refresh_vitals()
if build_active:
_update_build_preview()
_update_minimap()
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("inventory"):
_toggle_panel(inventory_panel)
_refresh_inventory()
elif event.is_action_pressed("crafting"):
_toggle_panel(crafting_panel)
_refresh_crafting()
elif event.is_action_pressed("build_mode"):
_toggle_build_mode()
elif event.is_action_pressed("map"):
_toggle_panel(map_panel)
elif event.is_action_pressed("chat"):
chat_input.grab_focus()
_capture_ui(true)
elif event.is_action_pressed("pause"):
if dialog_panel.visible:
dialog_panel.visible = false
_capture_ui(false)
elif build_active:
_toggle_build_mode()
elif inventory_panel.visible or crafting_panel.visible or map_panel.visible:
inventory_panel.visible = false
crafting_panel.visible = false
map_panel.visible = false
_capture_ui(false)
else:
_toggle_pause()
elif build_active:
if event.is_action_pressed("rotate_build"):
build_rotation = wrapf(build_rotation + PI * 0.5, 0.0, TAU)
elif event.is_action_pressed("ability_1"):
_select_build(0)
elif event.is_action_pressed("ability_2"):
_select_build(1)
elif event.is_action_pressed("ability_3"):
_select_build(2)
elif event.is_action_pressed("ability_4"):
_select_build(3)
elif event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_LEFT:
_try_place()
elif event.button_index == MOUSE_BUTTON_MIDDLE:
_try_remove()
func _wire_ability_buttons() -> void:
for c in ability_box.get_children():
if c is Button:
ability_buttons.append(c)
func _toggle_panel(panel: Control) -> void:
panel.visible = not panel.visible
_capture_ui(_any_panel_visible())
func _any_panel_visible() -> bool:
return inventory_panel.visible or crafting_panel.visible or dialog_panel.visible or pause_panel.visible
func _capture_ui(v: bool) -> void:
if local_player and local_player.has_method("set_ui_capturing"):
local_player.set_ui_capturing(v)
func _find_local_player() -> Node:
for p in get_tree().get_nodes_in_group("player"):
if p.is_multiplayer_authority():
return p
return null
func _refresh_vitals() -> void:
if local_player == null:
return
var hp: float = float(Stats.get_stat(local_player, "health", 0.0))
var max_hp: float = float(Stats.get_stat(local_player, "max_health", 1.0))
_on_health_changed(local_player, hp, max_hp)
var shield: float = float(Stats.get_stat(local_player, "shield", 0.0))
var max_shield: float = float(Stats.get_stat(local_player, "max_shield", 0.0))
_on_shield_changed(local_player, shield, max_shield)
var xp: float = float(Stats.get_stat(local_player, "xp", 0.0))
var to_next: float = float(Stats.get_stat(local_player, "xp_to_next", 50.0))
xp_bar.max_value = to_next
xp_bar.value = xp
level_label.text = "Lv %d" % int(Stats.get_stat(local_player, "level", 1))
func _on_health_changed(entity: Node, current: float, max: float) -> void:
if entity != local_player:
return
hp_bar.max_value = max
hp_bar.value = current
hp_label.text = "%d / %d" % [int(current), int(max)]
func _on_shield_changed(entity: Node, current: float, max: float) -> void:
if entity != local_player:
return
shield_bar.max_value = max if max > 0 else 1
shield_bar.value = current
shield_label.text = "%d / %d" % [int(current), int(max)]
func _on_cooldown_tick(entity: Node, cds: PackedFloat32Array, _max_cds: PackedFloat32Array, gcd: float) -> void:
if entity != local_player:
return
for i in range(min(ability_buttons.size(), cds.size())):
var btn: Button = ability_buttons[i]
if cds[i] > 0.0:
btn.text = "%d\n%.1f" % [i + 1, cds[i]]
btn.disabled = true
elif gcd > 0.0:
btn.text = "%d\nGCD" % (i + 1)
btn.disabled = true
else:
btn.text = "%d" % (i + 1)
btn.disabled = false
func _on_role_changed(player: Node, role: int) -> void:
if player != local_player and player != _find_local_player():
return
if local_player == null:
local_player = player
match role:
GameState.ROLE_TANK:
role_label.text = "T"
role_icon.modulate = Color(0.3, 0.5, 0.95)
GameState.ROLE_DAMAGE:
role_label.text = "D"
role_icon.modulate = Color(0.95, 0.3, 0.3)
GameState.ROLE_HEALER:
role_label.text = "H"
role_icon.modulate = Color(0.4, 0.85, 0.4)
func _on_entity_died(entity: Node) -> void:
if entity != local_player:
return
death_overlay.visible = true
death_label.text = "Respawning..."
func _on_respawned(entity: Node) -> void:
if entity != local_player:
return
death_overlay.visible = false
func _on_wave_started(wave: int) -> void:
wave_label.text = "Wave %d" % wave
func _on_wave_tick(seconds: float) -> void:
var m := int(seconds) / 60
var s := int(seconds) % 60
timer_label.text = "%02d:%02d" % [m, s]
func _on_village_damaged(current: float, max: float) -> void:
village_bar.max_value = max
village_bar.value = current
func _on_village_destroyed() -> void:
game_over_overlay.visible = true
func _on_invasion_started() -> void:
timer_label.modulate = Color(1.0, 0.4, 0.3)
func _on_xp_gained(player: Node, _amount: float) -> void:
if player != local_player:
return
var xp: float = float(Stats.get_stat(player, "xp", 0.0))
var to_next: float = float(Stats.get_stat(player, "xp_to_next", 50.0))
xp_bar.max_value = to_next
xp_bar.value = xp
func _on_level_up(player: Node, new_level: int) -> void:
if player != local_player:
return
level_label.text = "Lv %d" % new_level
_refresh_vitals()
func _on_dialog_opened(player: Node, npc: Node) -> void:
if player != local_player:
return
dialog_panel.visible = true
dialog_npc_node = npc
dialog_npc.text = npc.profile.display_name
dialog_log.text = "[i]" + npc.profile.greeting + "[/i]\n"
dialog_input.text = ""
dialog_input.grab_focus()
_capture_ui(true)
func _on_dialog_submitted(text: String) -> void:
if dialog_npc_node == null:
return
var dialog_sys: Node = _find_system("DialogSystem")
if dialog_sys == null:
return
dialog_log.append_text("[b]Du:[/b] " + text + "\n[i]...[/i]\n")
dialog_input.text = ""
dialog_sys.ask(dialog_npc_node, local_player, text)
func show_answer(text: String) -> void:
if dialog_npc_node == null:
return
dialog_log.text = dialog_log.text.replace("[i]...[/i]\n", "")
dialog_log.append_text("[b]" + dialog_npc_node.profile.display_name + ":[/b] " + text + "\n")
func _on_inventory_changed(player: Node) -> void:
if player != local_player:
return
_refresh_inventory()
func _refresh_inventory() -> void:
for c in inventory_list.get_children():
c.queue_free()
if local_player == null:
return
var inv_sys: Node = _find_system("InventorySystem")
if inv_sys == null:
return
var inv: Dictionary = inv_sys.get_inventory(local_player)
if inv.is_empty():
var lbl := Label.new()
lbl.text = "(empty)"
inventory_list.add_child(lbl)
return
for k in inv.keys():
var lbl := Label.new()
lbl.text = "%s: %d" % [str(k), inv[k]]
inventory_list.add_child(lbl)
func _refresh_crafting() -> void:
for c in crafting_list.get_children():
c.queue_free()
if local_player == null:
return
var c_sys: Node = _find_system("CraftingSystem")
var inv_sys: Node = _find_system("InventorySystem")
if c_sys == null or inv_sys == null:
return
for r in c_sys.get_recipes():
var btn := Button.new()
var inputs_str: String = ""
for k in r.inputs.keys():
inputs_str += "%s x%d " % [str(k), r.inputs[k]]
btn.text = "%s (%s)" % [r.name, inputs_str.strip_edges()]
btn.disabled = not c_sys.can_craft(local_player, r)
btn.pressed.connect(func(): c_sys.craft(local_player, r.id); _refresh_crafting())
crafting_list.add_child(btn)
func _populate_build_list() -> void:
for c in build_list.get_children():
c.queue_free()
var b_sys: Node = _find_system("BuildingSystem")
if b_sys == null:
return
var bps: Array = b_sys.get_blueprints()
for i in range(bps.size()):
var btn := Button.new()
btn.text = "%d %s\n%s x%d" % [i + 1, bps[i].name, str(bps[i].material), bps[i].cost]
btn.toggle_mode = true
btn.button_pressed = (i == 0)
btn.pressed.connect(func(): _select_build(i))
build_list.add_child(btn)
func _select_build(idx: int) -> void:
build_selected = idx
var btns := build_list.get_children()
for i in range(btns.size()):
if btns[i] is Button:
(btns[i] as Button).button_pressed = (i == idx)
if build_preview:
_update_preview_mesh()
func _toggle_build_mode() -> void:
build_active = not build_active
build_panel.visible = build_active
if local_player and local_player.has_method("set_build_mode"):
local_player.set_build_mode(build_active)
if build_active:
_create_build_preview()
elif build_preview:
build_preview.queue_free()
build_preview = null
func _create_build_preview() -> void:
if build_preview:
build_preview.queue_free()
var world: Node = get_tree().current_scene
if world == null:
return
build_preview = MeshInstance3D.new()
build_preview.cast_shadow = MeshInstance3D.SHADOW_CASTING_SETTING_OFF
world.add_child(build_preview)
_update_preview_mesh()
func _update_preview_mesh() -> void:
if build_preview == null:
return
var b_sys: Node = _find_system("BuildingSystem")
if b_sys == null:
return
var bp: Dictionary = b_sys.get_blueprints()[build_selected]
var box := BoxMesh.new()
box.size = bp.size
build_preview.mesh = box
var mat := StandardMaterial3D.new()
var c: Color = bp.color
c.a = 0.5
mat.albedo_color = c
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.flags_unshaded = true
build_preview.material_override = mat
func _update_build_preview() -> void:
if build_preview == null or local_player == null:
return
var b_sys: Node = _find_system("BuildingSystem")
if b_sys == null:
return
var pos: Vector3 = _ground_under_cursor()
var snapped: Vector3 = b_sys.snap_position(pos)
var bp: Dictionary = b_sys.get_blueprints()[build_selected]
build_preview.global_position = snapped + Vector3(0, bp.size.y * 0.5, 0)
build_preview.rotation.y = build_rotation
func _ground_under_cursor() -> Vector3:
var cam: Camera3D = local_player.camera if local_player.has_method("get") else null
if cam == null:
return Vector3.ZERO
var mouse := get_viewport().get_mouse_position()
var from := cam.project_ray_origin(mouse)
var dir := cam.project_ray_normal(mouse)
if abs(dir.y) < 0.001:
return from
var t: float = -from.y / dir.y
if t <= 0:
return from
return from + dir * t
func _try_place() -> void:
if local_player == null:
return
var b_sys: Node = _find_system("BuildingSystem")
if b_sys == null:
return
var bps: Array = b_sys.get_blueprints()
var bp: Dictionary = bps[build_selected]
var pos: Vector3 = _ground_under_cursor()
b_sys.place(local_player, bp.id, pos, build_rotation)
func _try_remove() -> void:
if local_player == null:
return
var cam: Camera3D = local_player.camera if local_player.has_method("get") else null
if cam == null:
return
var mouse := get_viewport().get_mouse_position()
var from := cam.project_ray_origin(mouse)
var to := from + cam.project_ray_normal(mouse) * 100.0
var space: PhysicsDirectSpaceState3D = local_player.get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(from, to)
query.collision_mask = 16
var hit: Dictionary = space.intersect_ray(query)
if hit.is_empty():
return
var node: Node = hit.collider
while node and not node.is_in_group("buildings"):
node = node.get_parent()
if node:
var b_sys: Node = _find_system("BuildingSystem")
if b_sys:
b_sys.remove(local_player, node.get_path())
func _on_chat(_peer_id: int, sender: String, text: String) -> void:
chat_log.append_text("[b]%s:[/b] %s\n" % [sender, text])
func _on_chat_submitted(text: String) -> void:
var c_sys: Node = _find_system("ChatSystem")
if c_sys:
c_sys.send(text)
chat_input.text = ""
chat_input.release_focus()
_capture_ui(_any_panel_visible())
func _toggle_pause() -> void:
pause_panel.visible = not pause_panel.visible
if pause_panel.visible and multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
GameState.set_paused(true)
else:
GameState.set_paused(false)
_capture_ui(_any_panel_visible())
func _on_resume_pressed() -> void:
pause_panel.visible = false
GameState.set_paused(false)
_capture_ui(_any_panel_visible())
func _on_quit_pressed() -> void:
Net.disconnect_net()
GameState.set_paused(false)
GameState.change_scene(GameState.SCENE_MAIN_MENU)
func _on_game_over_restart() -> void:
Net.disconnect_net()
GameState.set_paused(false)
GameState.change_scene(GameState.SCENE_MAIN_MENU)
func _update_minimap() -> void:
_minimap_accum += get_process_delta_time()
if _minimap_accum < 0.20:
return
_minimap_accum = 0.0
minimap_canvas.queue_redraw()
if map_panel.visible:
map_canvas.queue_redraw()
func _find_system(name: String) -> Node:
var n: Node = get_tree().root.get_node_or_null("World/Systems/" + name)
if n == null:
n = get_tree().root.get_node_or_null("Dungeon/Systems/" + name)
return n

1
scenes/hud/hud.gd.uid Normal file
View File

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

View File

@@ -1,237 +1,411 @@
[gd_scene format=3]
[gd_scene load_steps=3 format=3 uid="uid://b0hud00001"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ability_active"]
bg_color = Color(0.2, 0.2, 0.2, 0.8)
border_width_bottom = 2
border_width_left = 2
border_width_right = 2
border_width_top = 2
border_color = Color(0.8, 0.8, 0.8, 1)
[ext_resource type="Script" path="res://scenes/hud/hud.gd" id="1"]
[ext_resource type="Script" path="res://scenes/hud/minimap.gd" id="2"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ability_round"]
bg_color = Color(0.2, 0.2, 0.2, 0.8)
border_width_bottom = 2
border_width_left = 2
border_width_right = 2
border_width_top = 2
border_color = Color(0.8, 0.8, 0.8, 1)
corner_radius_top_left = 22
corner_radius_top_right = 22
corner_radius_bottom_right = 22
corner_radius_bottom_left = 22
[node name="HUD" type="CanvasLayer"]
script = ExtResource("1")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_bg"]
bg_color = Color(0.3, 0.1, 0.1, 1)
[node name="VitalsPanel" type="PanelContainer" parent="."]
offset_left = 12.0
offset_top = 12.0
offset_right = 312.0
offset_bottom = 130.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_health_fill"]
bg_color = Color(0.2, 0.8, 0.2, 1)
[node name="VBox" type="VBoxContainer" parent="VitalsPanel"]
layout_mode = 2
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_bg"]
bg_color = Color(0.1, 0.1, 0.3, 1)
[node name="HpRow" type="HBoxContainer" parent="VitalsPanel/VBox"]
layout_mode = 2
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_shield_fill"]
bg_color = Color(0.2, 0.5, 0.9, 1)
[node name="HUD" type="CanvasLayer" groups=["hud"]]
[node name="HealthBar" type="ProgressBar" parent="."]
offset_left = 10.0
offset_top = 10.0
offset_right = 210.0
offset_bottom = 30.0
theme_override_styles/background = SubResource("StyleBoxFlat_health_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_health_fill")
[node name="HpBar" type="ProgressBar" parent="VitalsPanel/VBox/HpRow"]
unique_name_in_owner = true
custom_minimum_size = Vector2(220, 18)
layout_mode = 2
max_value = 100.0
value = 100.0
show_percentage = false
[node name="HealthLabel" type="Label" parent="HealthBar"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 12
[node name="HpLabel" type="Label" parent="VitalsPanel/VBox/HpRow"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "100/100"
horizontal_alignment = 1
vertical_alignment = 1
[node name="ShieldBar" type="ProgressBar" parent="."]
offset_left = 10.0
offset_top = 35.0
offset_right = 210.0
offset_bottom = 55.0
theme_override_styles/background = SubResource("StyleBoxFlat_shield_bg")
theme_override_styles/fill = SubResource("StyleBoxFlat_shield_fill")
[node name="ShieldRow" type="HBoxContainer" parent="VitalsPanel/VBox"]
layout_mode = 2
[node name="ShieldBar" type="ProgressBar" parent="VitalsPanel/VBox/ShieldRow"]
unique_name_in_owner = true
custom_minimum_size = Vector2(220, 14)
layout_mode = 2
max_value = 100.0
value = 0.0
[node name="ShieldLabel" type="Label" parent="VitalsPanel/VBox/ShieldRow"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "0/0"
horizontal_alignment = 1
[node name="XpRow" type="HBoxContainer" parent="VitalsPanel/VBox"]
layout_mode = 2
[node name="LevelLabel" type="Label" parent="VitalsPanel/VBox/XpRow"]
unique_name_in_owner = true
layout_mode = 2
text = "Lv 1"
[node name="XpBar" type="ProgressBar" parent="VitalsPanel/VBox/XpRow"]
unique_name_in_owner = true
custom_minimum_size = Vector2(180, 12)
layout_mode = 2
size_flags_horizontal = 3
max_value = 50.0
value = 50.0
show_percentage = false
value = 0.0
[node name="ShieldLabel" type="Label" parent="ShieldBar"]
[node name="VillageRow" type="HBoxContainer" parent="VitalsPanel/VBox"]
layout_mode = 2
[node name="VillageLabelStatic" type="Label" parent="VitalsPanel/VBox/VillageRow"]
layout_mode = 2
text = "Village"
[node name="VillageBar" type="ProgressBar" parent="VitalsPanel/VBox/VillageRow"]
unique_name_in_owner = true
custom_minimum_size = Vector2(180, 12)
layout_mode = 2
size_flags_horizontal = 3
max_value = 1000.0
value = 1000.0
[node name="WaveBox" type="PanelContainer" parent="."]
anchor_left = 0.5
anchor_right = 0.5
offset_left = -120.0
offset_top = 12.0
offset_right = 120.0
offset_bottom = 64.0
[node name="VBox" type="VBoxContainer" parent="WaveBox"]
layout_mode = 2
[node name="WaveLabel" type="Label" parent="WaveBox/VBox"]
unique_name_in_owner = true
layout_mode = 2
horizontal_alignment = 1
text = "Wave 1"
theme_override_font_sizes/font_size = 18
[node name="WaveTimer" type="Label" parent="WaveBox/VBox"]
unique_name_in_owner = true
layout_mode = 2
horizontal_alignment = 1
text = "10:00"
[node name="MinimapPanel" type="PanelContainer" parent="."]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -212.0
offset_top = 12.0
offset_right = -12.0
offset_bottom = 212.0
[node name="MinimapCanvas" type="Control" parent="MinimapPanel"]
unique_name_in_owner = true
layout_mode = 2
script = ExtResource("2")
world_size = 80.0
[node name="AbilityBox" type="HBoxContainer" parent="."]
unique_name_in_owner = true
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -200.0
offset_top = -76.0
offset_right = 280.0
offset_bottom = -12.0
theme_override_constants/separation = 6
[node name="RoleIcon" type="Panel" parent="AbilityBox"]
unique_name_in_owner = true
custom_minimum_size = Vector2(56, 56)
layout_mode = 2
[node name="RoleLabel" type="Label" parent="AbilityBox/RoleIcon"]
unique_name_in_owner = true
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
text = "D"
horizontal_alignment = 1
vertical_alignment = 1
theme_override_font_sizes/font_size = 28
[node name="A1" type="Button" parent="AbilityBox"]
custom_minimum_size = Vector2(56, 56)
layout_mode = 2
text = "1"
[node name="A2" type="Button" parent="AbilityBox"]
custom_minimum_size = Vector2(56, 56)
layout_mode = 2
text = "2"
[node name="A3" type="Button" parent="AbilityBox"]
custom_minimum_size = Vector2(56, 56)
layout_mode = 2
text = "3"
[node name="A4" type="Button" parent="AbilityBox"]
custom_minimum_size = Vector2(56, 56)
layout_mode = 2
text = "4"
[node name="ChatPanel" type="PanelContainer" parent="."]
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 12.0
offset_top = -200.0
offset_right = 360.0
offset_bottom = -12.0
[node name="VBox" type="VBoxContainer" parent="ChatPanel"]
layout_mode = 2
[node name="ChatLog" type="RichTextLabel" parent="ChatPanel/VBox"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 140)
layout_mode = 2
bbcode_enabled = true
scroll_following = true
fit_content = true
[node name="ChatInput" type="LineEdit" parent="ChatPanel/VBox"]
unique_name_in_owner = true
layout_mode = 2
placeholder_text = "Press Y to chat, Enter to send"
[node name="DeathOverlay" type="Control" parent="."]
unique_name_in_owner = true
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Background" type="ColorRect" parent="DeathOverlay"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 12
text = "50/50"
color = Color(0.0, 0.0, 0.0, 0.5)
[node name="DeathLabel" type="Label" parent="DeathOverlay"]
unique_name_in_owner = true
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -200.0
offset_top = -40.0
offset_right = 200.0
offset_bottom = 40.0
text = "Respawning..."
horizontal_alignment = 1
vertical_alignment = 1
theme_override_font_sizes/font_size = 32
[node name="RespawnTimer" type="Label" parent="."]
anchors_preset = 8
[node name="InventoryPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -30.0
offset_right = 50.0
offset_bottom = 30.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 48
horizontal_alignment = 1
vertical_alignment = 1
offset_left = -200.0
offset_top = -200.0
offset_right = 200.0
offset_bottom = 200.0
[node name="AbilityBar" type="HBoxContainer" parent="."]
anchors_preset = 7
[node name="VBox" type="VBoxContainer" parent="InventoryPanel"]
layout_mode = 2
[node name="Title" type="Label" parent="InventoryPanel/VBox"]
layout_mode = 2
text = "Inventory (I)"
horizontal_alignment = 1
[node name="InventoryList" type="VBoxContainer" parent="InventoryPanel/VBox"]
unique_name_in_owner = true
layout_mode = 2
[node name="CraftingPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
anchor_left = 0.5
anchor_top = 1.0
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -260.0
offset_top = -240.0
offset_right = 260.0
offset_bottom = 240.0
[node name="VBox" type="VBoxContainer" parent="CraftingPanel"]
layout_mode = 2
[node name="Title" type="Label" parent="CraftingPanel/VBox"]
layout_mode = 2
text = "Crafting (C)"
horizontal_alignment = 1
[node name="CraftingList" type="VBoxContainer" parent="CraftingPanel/VBox"]
unique_name_in_owner = true
layout_mode = 2
[node name="BuildPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = -130.0
offset_top = -60.0
offset_right = 130.0
offset_bottom = -10.0
grow_horizontal = 2
grow_vertical = 0
theme_override_constants/separation = 5
offset_left = 12.0
offset_top = -260.0
offset_right = 360.0
offset_bottom = -210.0
[node name="ClassIcon" type="Panel" parent="AbilityBar"]
custom_minimum_size = Vector2(45, 45)
theme_override_styles/panel = SubResource("StyleBoxFlat_ability_round")
[node name="VBox" type="VBoxContainer" parent="BuildPanel"]
layout_mode = 2
[node name="Label" type="Label" parent="AbilityBar/ClassIcon"]
[node name="Title" type="Label" parent="BuildPanel/VBox"]
layout_mode = 2
text = "Build Mode (B)"
horizontal_alignment = 1
[node name="BuildList" type="HBoxContainer" parent="BuildPanel/VBox"]
unique_name_in_owner = true
layout_mode = 2
[node name="DialogPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -300.0
offset_top = -200.0
offset_right = 300.0
offset_bottom = 200.0
[node name="VBox" type="VBoxContainer" parent="DialogPanel"]
layout_mode = 2
[node name="DialogNpc" type="Label" parent="DialogPanel/VBox"]
unique_name_in_owner = true
layout_mode = 2
text = "NPC"
horizontal_alignment = 1
theme_override_font_sizes/font_size = 22
[node name="DialogLog" type="RichTextLabel" parent="DialogPanel/VBox"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 280)
layout_mode = 2
bbcode_enabled = true
scroll_following = true
fit_content = true
[node name="DialogInput" type="LineEdit" parent="DialogPanel/VBox"]
unique_name_in_owner = true
layout_mode = 2
placeholder_text = "Frage stellen, Enter senden"
[node name="MapPanel" type="Control" parent="."]
unique_name_in_owner = true
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Background" type="ColorRect" parent="MapPanel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 20
text = "D"
color = Color(0.0, 0.0, 0.0, 0.7)
[node name="MapCanvas" type="Control" parent="MapPanel"]
unique_name_in_owner = true
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -300.0
offset_top = -300.0
offset_right = 300.0
offset_bottom = 300.0
script = ExtResource("2")
world_size = 200.0
draw_labels = true
[node name="PausePanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -150.0
offset_top = -120.0
offset_right = 150.0
offset_bottom = 120.0
[node name="VBox" type="VBoxContainer" parent="PausePanel"]
layout_mode = 2
[node name="Title" type="Label" parent="PausePanel/VBox"]
layout_mode = 2
text = "Paused"
horizontal_alignment = 1
vertical_alignment = 1
theme_override_font_sizes/font_size = 28
[node name="Ability1" type="Panel" parent="AbilityBar"]
custom_minimum_size = Vector2(45, 45)
theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active")
[node name="Resume" type="Button" parent="PausePanel/VBox"]
layout_mode = 2
text = "Resume"
[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability1"]
layout_mode = 1
[node name="QuitToMenu" type="Button" parent="PausePanel/VBox"]
layout_mode = 2
text = "Quit to Menu"
[node name="GameOverOverlay" type="Control" parent="."]
unique_name_in_owner = true
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.6)
visible = false
[node name="Label" type="Label" parent="AbilityBar/Ability1"]
[node name="Background" type="ColorRect" parent="GameOverOverlay"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
text = "1"
color = Color(0.2, 0.0, 0.0, 0.8)
[node name="VBox" type="VBoxContainer" parent="GameOverOverlay"]
anchor_left = 0.5
anchor_right = 0.5
anchor_top = 0.5
anchor_bottom = 0.5
offset_left = -200.0
offset_top = -100.0
offset_right = 200.0
offset_bottom = 100.0
[node name="Title" type="Label" parent="GameOverOverlay/VBox"]
layout_mode = 2
text = "GAME OVER"
horizontal_alignment = 1
vertical_alignment = 1
theme_override_font_sizes/font_size = 48
[node name="Ability2" type="Panel" parent="AbilityBar"]
custom_minimum_size = Vector2(45, 45)
theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active")
[node name="Restart" type="Button" parent="GameOverOverlay/VBox"]
layout_mode = 2
text = "Back to Main Menu"
[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability2"]
layout_mode = 1
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.6)
visible = false
[node name="Label" type="Label" parent="AbilityBar/Ability2"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
text = "2"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Ability3" type="Panel" parent="AbilityBar"]
custom_minimum_size = Vector2(45, 45)
theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active")
[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability3"]
layout_mode = 1
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.6)
visible = false
[node name="Label" type="Label" parent="AbilityBar/Ability3"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
text = "3"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Ability4" type="Panel" parent="AbilityBar"]
custom_minimum_size = Vector2(45, 45)
theme_override_styles/panel = SubResource("StyleBoxFlat_ability_active")
[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability4"]
layout_mode = 1
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.6)
visible = false
[node name="Label" type="Label" parent="AbilityBar/Ability4"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
text = "4"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Ability5" type="Panel" parent="AbilityBar"]
custom_minimum_size = Vector2(45, 45)
theme_override_styles/panel = SubResource("StyleBoxFlat_ability_round")
[node name="CooldownOverlay" type="ColorRect" parent="AbilityBar/Ability5"]
layout_mode = 1
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.6)
visible = false
[node name="Label" type="Label" parent="AbilityBar/Ability5"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
text = "P"
horizontal_alignment = 1
vertical_alignment = 1
[connection signal="pressed" from="PausePanel/VBox/Resume" to="." method="_on_resume_pressed"]
[connection signal="pressed" from="PausePanel/VBox/QuitToMenu" to="." method="_on_quit_pressed"]
[connection signal="pressed" from="GameOverOverlay/VBox/Restart" to="." method="_on_game_over_restart"]

40
scenes/hud/minimap.gd Normal file
View File

@@ -0,0 +1,40 @@
extends Control
@export var world_size: float = 200.0
@export var label_text: String = ""
@export var draw_labels: bool = false
func _ready() -> void:
set_process(false)
func _draw() -> void:
var view_size: Vector2 = get_size()
draw_rect(Rect2(Vector2.ZERO, view_size), Color(0.05, 0.05, 0.08, 0.85))
draw_rect(Rect2(Vector2.ZERO, view_size), Color(0.6, 0.6, 0.6, 0.5), false, 2.0)
var local: Node = _local_player()
var center_offset: Vector2 = Vector2.ZERO
if local:
center_offset = Vector2((local as Node3D).global_position.x, (local as Node3D).global_position.z)
var map_sys: Node = get_tree().root.get_node_or_null("World/Systems/MapSystem")
if map_sys == null:
return
for entry in map_sys.get_marker_data():
var pos: Vector3 = entry.pos
var px: Vector2 = _to_pixel(Vector2(pos.x, pos.z), view_size, center_offset)
if px.x < 0 or px.x > view_size.x or px.y < 0 or px.y > view_size.y:
continue
draw_circle(px, 3.0, entry.color)
if draw_labels and entry.label != "":
var f: Font = ThemeDB.fallback_font
draw_string(f, px + Vector2(5, -5), entry.label, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, Color.WHITE)
func _to_pixel(world: Vector2, view_size: Vector2, center: Vector2) -> Vector2:
var rel: Vector2 = world - center
var s: float = view_size.x / world_size
return view_size * 0.5 + rel * s
func _local_player() -> Node:
for p in get_tree().get_nodes_in_group("player"):
if p.is_multiplayer_authority():
return p
return null

View File

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

View File

@@ -1,21 +0,0 @@
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

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

View File

@@ -1,41 +0,0 @@
[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")

55
scenes/menu/lobby.gd Normal file
View File

@@ -0,0 +1,55 @@
extends Control
@onready var player_list: ItemList = %PlayerList
@onready var status_label: Label = %StatusLabel
@onready var start_button: Button = %StartButton
func _ready() -> void:
Net.peer_connected.connect(_refresh)
Net.peer_disconnected.connect(_refresh)
Net.server_disconnected.connect(_on_disconnect)
start_button.visible = Net.is_host()
if Net.is_host():
status_label.text = "Hosting on port %d — press Start when ready" % Net.DEFAULT_PORT
else:
status_label.text = "Joined — waiting for host to start"
_refresh(0)
func _refresh(_id: int) -> void:
player_list.clear()
var ids: Array = []
if multiplayer.multiplayer_peer == null:
ids.append(1)
else:
ids.append(multiplayer.get_unique_id())
for p in multiplayer.get_peers():
if not p in ids:
ids.append(p)
ids.sort()
for id in ids:
var n: String = Net.player_names.get(id, "Player %d" % id)
player_list.add_item("[%d] %s" % [id, n])
func _on_start_pressed() -> void:
if not Net.is_host():
return
_start_world.rpc()
@rpc("authority", "call_local", "reliable")
func _start_world() -> void:
GameState.change_scene(GameState.SCENE_WORLD)
var _leaving: bool = false
func _on_back_pressed() -> void:
if _leaving:
return
_leaving = true
Net.disconnect_net()
GameState.change_scene(GameState.SCENE_MAIN_MENU)
func _on_disconnect() -> void:
if _leaving:
return
_leaving = true
GameState.change_scene(GameState.SCENE_MAIN_MENU)

1
scenes/menu/lobby.gd.uid Normal file
View File

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

58
scenes/menu/lobby.tscn Normal file
View File

@@ -0,0 +1,58 @@
[gd_scene load_steps=2 format=3 uid="uid://b0lobby0001"]
[ext_resource type="Script" path="res://scenes/menu/lobby.gd" id="1"]
[node name="Lobby" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1")
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
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="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="VBox" type="VBoxContainer" parent="Center"]
layout_mode = 2
custom_minimum_size = Vector2(420, 0)
[node name="Title" type="Label" parent="Center/VBox"]
layout_mode = 2
text = "Lobby"
horizontal_alignment = 1
theme_override_font_sizes/font_size = 36
[node name="StatusLabel" type="Label" parent="Center/VBox"]
unique_name_in_owner = true
layout_mode = 2
horizontal_alignment = 1
text = ""
[node name="PlayerList" type="ItemList" parent="Center/VBox"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(0, 180)
[node name="StartButton" type="Button" parent="Center/VBox"]
unique_name_in_owner = true
layout_mode = 2
text = "Start"
custom_minimum_size = Vector2(0, 40)
[node name="BackButton" type="Button" parent="Center/VBox"]
layout_mode = 2
text = "Back"
custom_minimum_size = Vector2(0, 40)
[connection signal="pressed" from="Center/VBox/StartButton" to="." method="_on_start_pressed"]
[connection signal="pressed" from="Center/VBox/BackButton" to="." method="_on_back_pressed"]

View File

@@ -1,32 +1,47 @@
extends CanvasLayer
extends Control
@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
@onready var name_input: LineEdit = %NameInput
@onready var ip_input: LineEdit = %IpInput
@onready var status_label: Label = %StatusLabel
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
name_input.text = Net.local_name
Net.connected_to_server.connect(_on_connected)
Net.connection_failed.connect(_on_failed)
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_singleplayer_pressed() -> void:
Net.local_name = name_input.text.strip_edges() if name_input.text.strip_edges() != "" else "Player"
GameState.reset_run()
if Net.host_singleplayer():
GameState.change_scene(GameState.SCENE_WORLD)
func _on_host() -> void:
pass
func _on_host_pressed() -> void:
Net.local_name = name_input.text.strip_edges() if name_input.text.strip_edges() != "" else "Host"
GameState.reset_run()
if Net.host():
status_label.text = "Hosting on port %d" % Net.DEFAULT_PORT
GameState.change_scene(GameState.SCENE_LOBBY)
else:
status_label.text = "Hosting failed."
func _on_join() -> void:
pass
func _on_join_pressed() -> void:
Net.local_name = name_input.text.strip_edges() if name_input.text.strip_edges() != "" else "Client"
var addr := ip_input.text.strip_edges()
if addr == "":
addr = "127.0.0.1"
status_label.text = "Connecting to %s..." % addr
if not Net.join(addr):
status_label.text = "Join failed."
func _on_quit() -> void:
func _on_options_pressed() -> void:
GameState.change_scene(GameState.SCENE_OPTIONS)
func _on_quit_pressed() -> void:
get_tree().quit()
func _on_connected() -> void:
status_label.text = "Connected."
GameState.change_scene(GameState.SCENE_LOBBY)
func _on_failed() -> void:
status_label.text = "Connection failed."

View File

@@ -1 +1 @@
uid://b4m6byh4k2mg7
uid://d0k5a5qreg1uu

View File

@@ -1,57 +1,115 @@
[gd_scene format=3]
[gd_scene format=3 uid="uid://bm5gsrlveyvnk"]
[ext_resource type="Script" path="res://scenes/menu/main_menu.gd" id="1"]
[ext_resource type="Script" uid="uid://d0k5a5qreg1uu" 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"]
[node name="MainMenu" type="Control" unique_id=282057399]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")
[node name="Background" type="ColorRect" parent="."]
[node name="Background" type="ColorRect" parent="." unique_id=1892952438]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.08, 0.08, 0.12, 1)
grow_horizontal = 2
grow_vertical = 2
color = Color(0.1, 0.1, 0.15, 1)
[node name="Center" type="CenterContainer" parent="."]
[node name="Center" type="CenterContainer" parent="." unique_id=303933157]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBox" type="VBoxContainer" parent="Center"]
custom_minimum_size = Vector2(320, 0)
theme_override_constants/separation = 20
[node name="VBox" type="VBoxContainer" parent="Center" unique_id=576802246]
custom_minimum_size = Vector2(420, 0)
layout_mode = 2
[node name="Title" type="Label" parent="Center/VBox"]
[node name="Title" type="Label" parent="Center/VBox" unique_id=1058778255]
layout_mode = 2
theme_override_font_sizes/font_size = 56
text = "MMO"
horizontal_alignment = 1
theme_override_font_sizes/font_size = 64
[node name="Spacer" type="Control" parent="Center/VBox"]
[node name="Spacer1" type="Control" parent="Center/VBox" unique_id=707057681]
custom_minimum_size = Vector2(0, 24)
layout_mode = 2
[node name="NameRow" type="HBoxContainer" parent="Center/VBox" unique_id=1693567332]
layout_mode = 2
[node name="NameLabel" type="Label" parent="Center/VBox/NameRow" unique_id=1012504876]
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
text = "Name:"
[node name="NameInput" type="LineEdit" parent="Center/VBox/NameRow" unique_id=318387796]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Player"
[node name="IpRow" type="HBoxContainer" parent="Center/VBox" unique_id=674693362]
layout_mode = 2
[node name="IpLabel" type="Label" parent="Center/VBox/IpRow" unique_id=222908556]
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
text = "Server IP:"
[node name="IpInput" type="LineEdit" parent="Center/VBox/IpRow" unique_id=99390081]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "127.0.0.1"
[node name="Spacer2" type="Control" parent="Center/VBox" unique_id=979762154]
custom_minimum_size = Vector2(0, 16)
layout_mode = 2
[node name="Singleplayer" type="Button" parent="Center/VBox" unique_id=156537849]
custom_minimum_size = Vector2(0, 40)
[node name="SingleplayerButton" type="Button" parent="Center/VBox"]
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
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="Host" type="Button" parent="Center/VBox" unique_id=1117468477]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
text = "Host"
[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="Join" type="Button" parent="Center/VBox" unique_id=1669660415]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
text = "Join"
[node name="QuitButton" type="Button" parent="Center/VBox"]
custom_minimum_size = Vector2(0, 48)
[node name="Options" type="Button" parent="Center/VBox" unique_id=1739276223]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
text = "Options"
[node name="Quit" type="Button" parent="Center/VBox" unique_id=182300116]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
text = "Quit"
theme_override_styles/normal = SubResource("StyleBoxFlat_button")
[node name="Spacer3" type="Control" parent="Center/VBox" unique_id=1580062158]
custom_minimum_size = Vector2(0, 16)
layout_mode = 2
[node name="StatusLabel" type="Label" parent="Center/VBox" unique_id=643981255]
unique_name_in_owner = true
layout_mode = 2
horizontal_alignment = 1
[connection signal="pressed" from="Center/VBox/Singleplayer" to="." method="_on_singleplayer_pressed"]
[connection signal="pressed" from="Center/VBox/Host" to="." method="_on_host_pressed"]
[connection signal="pressed" from="Center/VBox/Join" to="." method="_on_join_pressed"]
[connection signal="pressed" from="Center/VBox/Options" to="." method="_on_options_pressed"]
[connection signal="pressed" from="Center/VBox/Quit" to="." method="_on_quit_pressed"]

View File

@@ -0,0 +1,49 @@
extends Control
const SETTINGS_PATH: String = "user://settings.cfg"
@onready var master: HSlider = %MasterSlider
@onready var music: HSlider = %MusicSlider
@onready var sfx: HSlider = %SfxSlider
@onready var sens: HSlider = %SensSlider
func _ready() -> void:
load_settings()
master.value_changed.connect(_on_master_changed)
music.value_changed.connect(_on_music_changed)
sfx.value_changed.connect(_on_sfx_changed)
func load_settings() -> void:
var cfg := ConfigFile.new()
if cfg.load(SETTINGS_PATH) == OK:
master.value = cfg.get_value("audio", "master", 1.0)
music.value = cfg.get_value("audio", "music", 1.0)
sfx.value = cfg.get_value("audio", "sfx", 1.0)
sens.value = cfg.get_value("input", "sens", 1.0)
_apply_audio()
func save_settings() -> void:
var cfg := ConfigFile.new()
cfg.set_value("audio", "master", master.value)
cfg.set_value("audio", "music", music.value)
cfg.set_value("audio", "sfx", sfx.value)
cfg.set_value("input", "sens", sens.value)
cfg.save(SETTINGS_PATH)
func _on_master_changed(_v: float) -> void:
_apply_audio()
save_settings()
func _on_music_changed(_v: float) -> void:
_apply_audio()
save_settings()
func _on_sfx_changed(_v: float) -> void:
_apply_audio()
save_settings()
func _apply_audio() -> void:
AudioServer.set_bus_volume_db(0, linear_to_db(max(0.001, master.value)))
func _on_back_pressed() -> void:
GameState.change_scene(GameState.SCENE_MAIN_MENU)

View File

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

View File

@@ -0,0 +1,108 @@
[gd_scene load_steps=2 format=3 uid="uid://b0options0001"]
[ext_resource type="Script" path="res://scenes/menu/options_menu.gd" id="1"]
[node name="OptionsMenu" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1")
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.1, 0.1, 0.15, 1)
[node name="Center" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="VBox" type="VBoxContainer" parent="Center"]
layout_mode = 2
custom_minimum_size = Vector2(420, 0)
[node name="Title" type="Label" parent="Center/VBox"]
layout_mode = 2
text = "Options"
horizontal_alignment = 1
theme_override_font_sizes/font_size = 36
[node name="MasterRow" type="HBoxContainer" parent="Center/VBox"]
layout_mode = 2
[node name="MasterLabel" type="Label" parent="Center/VBox/MasterRow"]
layout_mode = 2
custom_minimum_size = Vector2(110, 0)
text = "Master"
[node name="MasterSlider" type="HSlider" parent="Center/VBox/MasterRow"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
min_value = 0.0
max_value = 1.0
step = 0.05
value = 1.0
[node name="MusicRow" type="HBoxContainer" parent="Center/VBox"]
layout_mode = 2
[node name="MusicLabel" type="Label" parent="Center/VBox/MusicRow"]
layout_mode = 2
custom_minimum_size = Vector2(110, 0)
text = "Music"
[node name="MusicSlider" type="HSlider" parent="Center/VBox/MusicRow"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
min_value = 0.0
max_value = 1.0
step = 0.05
value = 1.0
[node name="SfxRow" type="HBoxContainer" parent="Center/VBox"]
layout_mode = 2
[node name="SfxLabel" type="Label" parent="Center/VBox/SfxRow"]
layout_mode = 2
custom_minimum_size = Vector2(110, 0)
text = "SFX"
[node name="SfxSlider" type="HSlider" parent="Center/VBox/SfxRow"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
min_value = 0.0
max_value = 1.0
step = 0.05
value = 1.0
[node name="SensRow" type="HBoxContainer" parent="Center/VBox"]
layout_mode = 2
[node name="SensLabel" type="Label" parent="Center/VBox/SensRow"]
layout_mode = 2
custom_minimum_size = Vector2(110, 0)
text = "Mouse Sens"
[node name="SensSlider" type="HSlider" parent="Center/VBox/SensRow"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
min_value = 0.1
max_value = 3.0
step = 0.1
value = 1.0
[node name="Back" type="Button" parent="Center/VBox"]
layout_mode = 2
text = "Back"
custom_minimum_size = Vector2(0, 40)
[connection signal="pressed" from="Center/VBox/Back" to="." method="_on_back_pressed"]

View File

@@ -1,9 +0,0 @@
extends Node
@onready var player: CharacterBody3D = get_parent()
func _unhandled_input(event: InputEvent) -> void:
for i in range(5):
if event.is_action_pressed("ability_%s" % (i + 1)):
EventBus.ability_use.emit(player, i)
return

View File

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

View File

@@ -1,30 +0,0 @@
extends Node3D
const SENSITIVITY := 0.003
const PITCH_MIN := -80.0
const PITCH_MAX := 80.0
var pitch := -0.3
var camera_yaw := 0.0
var player_yaw := -2.356
func _ready() -> void:
get_parent().rotation.y = player_yaw
rotation = Vector3(pitch, camera_yaw, 0)
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
var lmb := Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
var rmb := Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)
if lmb or rmb:
pitch -= event.relative.y * SENSITIVITY
pitch = clamp(pitch, deg_to_rad(PITCH_MIN), deg_to_rad(PITCH_MAX))
if rmb:
player_yaw -= event.relative.x * SENSITIVITY
get_parent().rotation.y = player_yaw
else:
camera_yaw -= event.relative.x * SENSITIVITY
rotation = Vector3(pitch, camera_yaw, 0)

View File

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

View File

@@ -1,59 +0,0 @@
extends CharacterBody3D
@export var stats: PlayerStats
var anim_player: AnimationPlayer = null
var current_anim: String = ""
var attack_lock_until: float = 0.0
func _ready() -> void:
add_to_group("player")
PlayerData.init_from_resource(stats)
if PlayerData.returning_from_dungeon:
global_position = PlayerData.portal_position + Vector3(0, 1, -5)
PlayerData.returning_from_dungeon = false
elif PlayerData.dungeon_cleared:
PlayerData.clear_cache()
anim_player = get_node_or_null("Mesh/Model/AnimationPlayer")
_play_anim("Idle")
EventBus.attack_executed.connect(_on_attack_executed)
EventBus.entity_died.connect(_on_entity_died)
EventBus.player_respawned.connect(_on_player_respawned)
func _process(_delta: float) -> void:
if not PlayerData.alive:
return
var now: float = Time.get_ticks_msec() / 1000.0
if now < attack_lock_until:
return
if velocity.length() > 0.1:
_play_anim("Running_A")
else:
_play_anim("Idle")
func _play_anim(anim_name: String, loop: bool = true) -> void:
if not anim_player:
return
if current_anim == anim_name:
return
if not anim_player.has_animation(anim_name):
return
var anim: Animation = anim_player.get_animation(anim_name)
if anim:
anim.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE
anim_player.play(anim_name)
current_anim = anim_name
func _on_attack_executed(attacker: Node, _pos: Vector3, _dir: Vector3, _damage: float) -> void:
if attacker != self:
return
_play_anim("1H_Melee_Attack_Chop", false)
attack_lock_until = Time.get_ticks_msec() / 1000.0 + 0.5
func _on_entity_died(entity: Node) -> void:
if entity != PlayerData:
return
_play_anim("Death_A", false)
func _on_player_respawned(_player: Node) -> void:
_play_anim("Idle")

View File

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

View File

@@ -1,37 +0,0 @@
extends Node
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
@onready var player: CharacterBody3D = get_parent()
func _physics_process(delta: float) -> void:
if not player.is_on_floor():
player.velocity.y -= gravity * delta
if Input.is_action_just_pressed("ui_accept") and player.is_on_floor():
player.velocity.y = PlayerData.jump_velocity
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var camera_pivot := player.get_node("CameraPivot") as Node3D
var forward := -camera_pivot.global_transform.basis.z
forward.y = 0
forward = forward.normalized()
var right := camera_pivot.global_transform.basis.x
right.y = 0
right = right.normalized()
var direction := (forward * -input_dir.y + right * input_dir.x).normalized()
if direction:
player.velocity.x = direction.x * PlayerData.speed
player.velocity.z = direction.z * PlayerData.speed
var world_yaw: float = atan2(-direction.x, -direction.z)
var local_yaw: float = world_yaw - player.rotation.y
var mesh: Node3D = player.get_node_or_null("Mesh") as Node3D
if mesh:
mesh.rotation.y = lerp_angle(mesh.rotation.y, local_yaw, 10.0 * delta)
else:
player.velocity.x = move_toward(player.velocity.x, 0, PlayerData.speed)
player.velocity.z = move_toward(player.velocity.z, 0, PlayerData.speed)
player.move_and_slide()

View File

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

View File

@@ -1,45 +0,0 @@
[gd_scene format=3 uid="uid://cdnkbt1f0db7e"]
[ext_resource type="Script" uid="uid://cx6k5473yxno" path="res://scenes/player/init.gd" id="1"]
[ext_resource type="Script" uid="uid://cohjyjge1kqxb" path="res://scenes/player/camera.gd" id="2"]
[ext_resource type="Script" uid="uid://fg87dh8fbc8" path="res://scenes/player/movement.gd" id="3"]
[ext_resource type="Script" uid="uid://hh5yw7vcjdqr" path="res://scenes/player/ability.gd" id="4"]
[ext_resource type="Script" uid="uid://b05nkuryipwny" path="res://scenes/player/targeting.gd" id="8"]
[ext_resource type="Script" uid="uid://dhomrampxola4" path="res://scenes/player/role/role.gd" id="10"]
[ext_resource type="Resource" uid="uid://btd0g0oiulssq" path="res://scenes/player/player_stats.tres" id="14"]
[ext_resource type="PackedScene" path="res://assets/models/characters/Knight.glb" id="15"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
radius = 0.3
height = 1.8
[node name="Player" type="CharacterBody3D" unique_id=197716516]
script = ExtResource("1")
stats = ExtResource("14")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=311205642]
shape = SubResource("CapsuleShape3D_1")
[node name="Mesh" type="Node3D" parent="." unique_id=1514179122]
[node name="Model" parent="Mesh" instance=ExtResource("15")]
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, -0.9, 0)
[node name="CameraPivot" type="Node3D" parent="." unique_id=1881685457]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
script = ExtResource("2")
[node name="Camera3D" type="Camera3D" parent="CameraPivot" unique_id=2062990383]
transform = Transform3D(1, 0, 0, 0, 0.966, 0.259, 0, -0.259, 0.966, 0, 2, 5)
[node name="Movement" type="Node" parent="." unique_id=811179177]
script = ExtResource("3")
[node name="Ability" type="Node" parent="." unique_id=1184596245]
script = ExtResource("4")
[node name="Targeting" type="Node" parent="." unique_id=1974574662]
script = ExtResource("8")
[node name="Role" type="Node" parent="." unique_id=1637643687]
script = ExtResource("10")

View File

@@ -1,10 +0,0 @@
extends BaseStats
class_name PlayerStats
@export var speed := 5.0
@export var jump_velocity := 4.5
@export var target_range := 20.0
@export var combat_timeout := 3.0
@export var respawn_time := 3.0
@export var gcd_time := 0.5
@export var aa_cooldown := 0.5

View File

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

View File

@@ -1,8 +0,0 @@
[gd_resource type="Resource" script_class="PlayerStats" format=3 uid="uid://btd0g0oiulssq"]
[ext_resource type="Script" uid="uid://ypyntbavbsto" path="res://scenes/player/player_stats.gd" id="1"]
[resource]
script = ExtResource("1")
health_regen = 1.0
max_shield = 50.0

View File

@@ -1,16 +0,0 @@
extends Resource
class_name Ability
enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE }
@export var ability_name: String = ""
@export var type: Type = Type.SINGLE
@export var damage: float = 0.0
@export var ability_range: float = 3.0
@export var cooldown: float = 0.0
@export var uses_gcd: bool = true
@export var aoe_radius: float = 0.0
@export var icon: String = ""
@export var is_heal: bool = false
@export var passive_stat: String = "damage"
@export var element: int = 0

View File

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

View File

@@ -1,7 +0,0 @@
extends Resource
class_name AbilitySet
@export var abilities: Array[Ability] = []
@export var aa_damage: float = 10.0
@export var aa_range: float = 10.0
@export var aa_is_heal: bool = false

View File

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

View File

@@ -1,13 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://bpx3l13iuynfv"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "AOE"
type = 1
damage = 20.0
ability_range = 5.0
cooldown = 3.0
icon = "2"
element = 1

View File

@@ -1,12 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://dadpl32yujwhe"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Damage Boost"
type = 4
damage = 50.0
ability_range = 0.0
uses_gcd = false
icon = "P"

View File

@@ -1,12 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://dwvc8b3cmce8l"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Single"
damage = 30.0
ability_range = 20.0
cooldown = 2.0
icon = "1"
element = 1

View File

@@ -1,14 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://s32wvlww2ls2"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Burst"
type = 3
damage = 10.0
ability_range = 20.0
cooldown = 15.0
aoe_radius = 3.0
icon = "4"
element = 1

View File

@@ -1,12 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Shield Reset"
type = 2
ability_range = 0.0
cooldown = 5.0
uses_gcd = false
icon = "3"

View File

@@ -1,14 +0,0 @@
[gd_resource type="Resource" script_class="AbilitySet" format=3 uid="uid://beodknb6i1pm4"]
[ext_resource type="Script" uid="uid://voedgs25cwrb" path="res://scenes/player/role/ability_set.gd" id="1"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1_ability"]
[ext_resource type="Resource" uid="uid://dwvc8b3cmce8l" path="res://scenes/player/role/damage/abilities/single.tres" id="2"]
[ext_resource type="Resource" uid="uid://bpx3l13iuynfv" path="res://scenes/player/role/damage/abilities/aoe.tres" id="3"]
[ext_resource type="Resource" path="res://scenes/player/role/damage/abilities/utility.tres" id="4"]
[ext_resource type="Resource" uid="uid://s32wvlww2ls2" path="res://scenes/player/role/damage/abilities/ult.tres" id="5"]
[ext_resource type="Resource" uid="uid://dadpl32yujwhe" path="res://scenes/player/role/damage/abilities/passive.tres" id="6"]
[resource]
script = ExtResource("1")
abilities = Array[ExtResource("1_ability")]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")])
aa_damage = 25.0

View File

@@ -1,13 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://m1kgk2uugnex"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Circle of Healing"
type = 1
damage = 10.0
ability_range = 20.0
cooldown = 3.0
icon = "2"
is_heal = true

View File

@@ -1,13 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Heal Aura"
type = 4
damage = 50.0
ability_range = 0.0
uses_gcd = false
icon = "P"
passive_stat = "heal"

View File

@@ -1,12 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://cqw1jy6kqvmnj"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Heal"
damage = 15.0
ability_range = 20.0
cooldown = 2.0
icon = "1"
is_heal = true

View File

@@ -1,14 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3 uid="uid://d04nu1leyki16"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Divine Light"
type = 3
damage = 25.0
ability_range = 20.0
cooldown = 15.0
aoe_radius = 3.0
icon = "4"
is_heal = true

View File

@@ -1,12 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Shield Reset"
type = 2
ability_range = 0.0
cooldown = 5.0
uses_gcd = false
icon = "3"

View File

@@ -1,16 +0,0 @@
[gd_resource type="Resource" script_class="AbilitySet" format=3 uid="uid://kcwuhnqy34mj"]
[ext_resource type="Script" uid="uid://voedgs25cwrb" path="res://scenes/player/role/ability_set.gd" id="1"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1_ability"]
[ext_resource type="Resource" path="res://scenes/player/role/healer/abilities/single.tres" id="2"]
[ext_resource type="Resource" path="res://scenes/player/role/healer/abilities/aoe.tres" id="3"]
[ext_resource type="Resource" path="res://scenes/player/role/healer/abilities/utility.tres" id="4"]
[ext_resource type="Resource" path="res://scenes/player/role/healer/abilities/ult.tres" id="5"]
[ext_resource type="Resource" path="res://scenes/player/role/healer/abilities/passive.tres" id="6"]
[resource]
script = ExtResource("1")
abilities = Array[ExtResource("1_ability")]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")])
aa_damage = 1.0
aa_range = 20.0
aa_is_heal = true

View File

@@ -1,14 +0,0 @@
extends Node
@onready var player: CharacterBody3D = get_parent()
func _ready() -> void:
EventBus.role_change_requested.emit(player, PlayerData.current_role)
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("class_tank"):
EventBus.role_change_requested.emit(player, PlayerData.Role.TANK)
elif event.is_action_pressed("class_damage"):
EventBus.role_change_requested.emit(player, PlayerData.Role.DAMAGE)
elif event.is_action_pressed("class_healer"):
EventBus.role_change_requested.emit(player, PlayerData.Role.HEALER)

View File

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

View File

@@ -1,12 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Tank AOE"
type = 1
damage = 10.0
ability_range = 10.0
cooldown = 3.0
icon = "2"

View File

@@ -1,13 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Shield Aura"
type = 4
damage = 50.0
ability_range = 0.0
uses_gcd = false
icon = "P"
passive_stat = "shield"

View File

@@ -1,11 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Tank Strike"
damage = 15.0
ability_range = 3.0
cooldown = 2.0
icon = "1"

View File

@@ -1,11 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Fortress"
type = 2
damage = 300.0
cooldown = 20.0
icon = "4"

View File

@@ -1,12 +0,0 @@
[gd_resource type="Resource" script_class="Ability" format=3]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1"]
[resource]
script = ExtResource("1")
ability_name = "Shield Reset"
type = 2
ability_range = 0.0
cooldown = 5.0
uses_gcd = false
icon = "3"

View File

@@ -1,15 +0,0 @@
[gd_resource type="Resource" script_class="AbilitySet" format=3 uid="uid://cgxtn7dfs40bh"]
[ext_resource type="Script" uid="uid://voedgs25cwrb" path="res://scenes/player/role/ability_set.gd" id="1"]
[ext_resource type="Script" uid="uid://c03xbbf3yhfl3" path="res://scenes/player/role/ability.gd" id="1_ability"]
[ext_resource type="Resource" path="res://scenes/player/role/tank/abilities/single.tres" id="2"]
[ext_resource type="Resource" path="res://scenes/player/role/tank/abilities/aoe.tres" id="3"]
[ext_resource type="Resource" path="res://scenes/player/role/tank/abilities/utility.tres" id="4"]
[ext_resource type="Resource" path="res://scenes/player/role/tank/abilities/ult.tres" id="5"]
[ext_resource type="Resource" path="res://scenes/player/role/tank/abilities/passive.tres" id="6"]
[resource]
script = ExtResource("1")
abilities = Array[ExtResource("1_ability")]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5"), ExtResource("6")])
aa_damage = 5.0
aa_range = 3.0

View File

@@ -1,60 +0,0 @@
extends Node
const TARGET_RANGE := 100.0
var mouse_press_pos: Vector2 = Vector2.ZERO
@onready var player: CharacterBody3D = get_parent()
@onready var camera: Camera3D = get_parent().get_node("CameraPivot/Camera3D")
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
mouse_press_pos = event.position
else:
var drag: float = event.position.distance_to(mouse_press_pos)
if drag < 5.0:
_try_target_under_mouse(event.position)
if event.is_action_pressed("target_next"):
_cycle_target()
func _try_target_under_mouse(mouse_pos: Vector2) -> void:
var from := camera.project_ray_origin(mouse_pos)
var to := from + camera.project_ray_normal(mouse_pos) * TARGET_RANGE
var space := player.get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(from, to)
query.collision_mask = 4
query.collide_with_areas = true
query.collide_with_bodies = false
var result := space.intersect_ray(query)
if result:
var hit_target := result.collider.get_parent() as Node3D
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 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])
return
var idx := targets.find(current)
var next_idx := (idx + 1) % targets.size()
EventBus.target_requested.emit(player, targets[next_idx])

View File

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

View File

@@ -1,36 +0,0 @@
extends StaticBody3D
@export var target_scene: String = "res://scenes/dungeon/dungeon.tscn"
@export var is_exit: bool = false
var active := false
var dungeon_variant: int = 0
func _ready() -> void:
if not is_exit:
if PlayerData.dungeon_cleared:
queue_free()
return
get_tree().create_timer(0.5).timeout.connect(_check_overlapping)
else:
get_tree().create_timer(1.0).timeout.connect(func() -> void: active = true)
func _check_overlapping() -> void:
active = true
for body in $GateArea.get_overlapping_bodies():
_on_gate_area_body_entered(body)
func _on_gate_area_body_entered(body: Node3D) -> void:
if not active:
return
if body is CharacterBody3D and body.name == "Player":
PlayerData.save_cache()
if is_exit:
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:
get_tree().change_scene_to_file(target_scene)

View File

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

View File

@@ -1,36 +0,0 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scenes/portal/gate.gd" id="1"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
albedo_color = Color(0.1, 0.9, 0.3, 1)
emission_enabled = true
emission = Color(0.1, 0.9, 0.3, 1)
emission_energy_multiplier = 0.5
[sub_resource type="CylinderMesh" id="CylinderMesh_1"]
top_radius = 1.0
bottom_radius = 1.0
height = 2.0
material = SubResource("StandardMaterial3D_1")
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 3.0
[node name="Gate" type="StaticBody3D"]
script = ExtResource("1")
collision_layer = 0
collision_mask = 0
[node name="Mesh" type="MeshInstance3D" parent="."]
mesh = SubResource("CylinderMesh_1")
[node name="GateArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 1
monitoring = true
[node name="CollisionShape3D" type="CollisionShape3D" parent="GateArea"]
shape = SubResource("SphereShape3D_1")
[connection signal="body_entered" from="GateArea" to="." method="_on_gate_area_body_entered"]

View File

@@ -1,37 +0,0 @@
extends StaticBody3D
@export var stats: PortalStats
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 +0,0 @@
uid://dlexijybbqxop

Some files were not shown because too many files have changed in this diff Show More