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

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