refactor
This commit is contained in:
36
scenes/entities/building/building.gd
Normal file
36
scenes/entities/building/building.gd
Normal 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
|
||||
1
scenes/entities/building/building.gd.uid
Normal file
1
scenes/entities/building/building.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cq63wtaqo3ebt
|
||||
22
scenes/entities/building/building.tscn
Normal file
22
scenes/entities/building/building.tscn
Normal 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")
|
||||
170
scenes/entities/enemy/enemy.gd
Normal file
170
scenes/entities/enemy/enemy.gd
Normal 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
|
||||
1
scenes/entities/enemy/enemy.gd.uid
Normal file
1
scenes/entities/enemy/enemy.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://t28sckpnef15
|
||||
76
scenes/entities/enemy/enemy.tscn
Normal file
76
scenes/entities/enemy/enemy.tscn
Normal 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")
|
||||
80
scenes/entities/gate/gate.gd
Normal file
80
scenes/entities/gate/gate.gd
Normal 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())
|
||||
1
scenes/entities/gate/gate.gd.uid
Normal file
1
scenes/entities/gate/gate.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://yrf2o25mrihx
|
||||
53
scenes/entities/gate/gate.tscn
Normal file
53
scenes/entities/gate/gate.tscn
Normal 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)
|
||||
39
scenes/entities/loot/loot_drop.gd
Normal file
39
scenes/entities/loot/loot_drop.gd
Normal 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()
|
||||
1
scenes/entities/loot/loot_drop.gd.uid
Normal file
1
scenes/entities/loot/loot_drop.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://begk6dfrigj12
|
||||
35
scenes/entities/loot/loot_drop.tscn
Normal file
35
scenes/entities/loot/loot_drop.tscn
Normal 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")
|
||||
52
scenes/entities/npc/npc.gd
Normal file
52
scenes/entities/npc/npc.gd
Normal 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
|
||||
1
scenes/entities/npc/npc.gd.uid
Normal file
1
scenes/entities/npc/npc.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://chul3jysytie2
|
||||
53
scenes/entities/npc/npc.tscn
Normal file
53
scenes/entities/npc/npc.tscn
Normal 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")
|
||||
242
scenes/entities/player/player.gd
Normal file
242
scenes/entities/player/player.gd
Normal 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
|
||||
1
scenes/entities/player/player.gd.uid
Normal file
1
scenes/entities/player/player.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://w43lhoat7ccq
|
||||
58
scenes/entities/player/player.tscn
Normal file
58
scenes/entities/player/player.tscn
Normal 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")
|
||||
62
scenes/entities/portal/portal.gd
Normal file
62
scenes/entities/portal/portal.gd
Normal 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)
|
||||
1
scenes/entities/portal/portal.gd.uid
Normal file
1
scenes/entities/portal/portal.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cny0k5s884uco
|
||||
49
scenes/entities/portal/portal.tscn
Normal file
49
scenes/entities/portal/portal.tscn
Normal 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")
|
||||
36
scenes/entities/village/village.gd
Normal file
36
scenes/entities/village/village.gd
Normal 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()
|
||||
1
scenes/entities/village/village.gd.uid
Normal file
1
scenes/entities/village/village.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://n5yrsrsav4yx
|
||||
47
scenes/entities/village/village.tscn
Normal file
47
scenes/entities/village/village.tscn
Normal 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
|
||||
Reference in New Issue
Block a user