Made more enemies, easier to kill, more slashy. You get passive health regen, health orbs. Little key bindings tooltip.

This commit is contained in:
2026-06-30 22:57:11 +01:00
parent 7fa2efabaf
commit 97ebbb1618
17 changed files with 404 additions and 6 deletions
+1 -1
View File
@@ -133,7 +133,7 @@ _spring_arm = NodePath("SpringArm3D")
[node name="SpringArm3D" type="SpringArm3D" parent="SpringArmOffset"] [node name="SpringArm3D" type="SpringArm3D" parent="SpringArmOffset"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
spring_length = 5.0 spring_length = 10.0
[node name="Camera3D" type="Camera3D" parent="SpringArmOffset/SpringArm3D"] [node name="Camera3D" type="Camera3D" parent="SpringArmOffset/SpringArm3D"]
current = true current = true
+1 -1
View File
@@ -25,7 +25,7 @@ script = ExtResource("1_basic_enemy")
move_speed = 3.5 move_speed = 3.5
attack_damage = 5.0 attack_damage = 5.0
detection_range = 500.0 detection_range = 500.0
max_health = 20.0 max_health = 10.0
[node name="Mesh" type="MeshInstance3D" parent="."] [node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(-0.29981995, -0.01038597, -0.00033419454, 0.00636219, -0.19110087, 0.23117083, -0.008215994, 0.23102501, 0.19120643, 0, 0.41641736, 0) transform = Transform3D(-0.29981995, -0.01038597, -0.00033419454, 0.00636219, -0.19110087, 0.23117083, -0.008215994, 0.23102501, 0.19120643, 0, 0.41641736, 0)
+1 -1
View File
@@ -13,7 +13,7 @@ shading_mode = 0
script = ExtResource("1_spawner") script = ExtResource("1_spawner")
spawn_radius = 20.0 spawn_radius = 20.0
spawn_height = 0.5 spawn_height = 0.5
enemies_per_wave = 3 enemies_per_wave = 10
auto_start_next_wave = false auto_start_next_wave = false
wave_delay = 5.0 wave_delay = 5.0
enemy_scenes = Array[PackedScene]([ExtResource("2_basic_enemy")]) enemy_scenes = Array[PackedScene]([ExtResource("2_basic_enemy")])
+31
View File
@@ -0,0 +1,31 @@
[gd_scene load_steps=5 format=3]
[ext_resource type="Script" path="res://level/scripts/health_orb.gd" id="1_orb"]
[sub_resource type="SphereMesh" id="SphereMesh_orb"]
radius = 0.35
height = 0.7
[sub_resource type="StandardMaterial3D" id="Mat_orb"]
albedo_color = Color(0.2, 1, 0.35, 1)
emission_enabled = true
emission = Color(0.2, 1, 0.35, 1)
emission_energy_multiplier = 2.0
[sub_resource type="SphereShape3D" id="Shape_orb"]
radius = 0.8
[node name="HealthOrb" type="Area3D"]
script = ExtResource("1_orb")
[node name="Mesh" type="MeshInstance3D" parent="."]
mesh = SubResource("SphereMesh_orb")
surface_material_override/0 = SubResource("Mat_orb")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("Shape_orb")
[node name="Glow" type="OmniLight3D" parent="."]
light_color = Color(0.4, 1, 0.5, 1)
light_energy = 1.5
omni_range = 3.0
+1 -1
View File
@@ -5055,7 +5055,7 @@ _spring_arm = NodePath("SpringArm3D")
[node name="SpringArm3D" type="SpringArm3D" parent="SpringArmOffset"] [node name="SpringArm3D" type="SpringArm3D" parent="SpringArmOffset"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
spring_length = 5.0 spring_length = 10.0
[node name="Camera3D" type="Camera3D" parent="SpringArmOffset/SpringArm3D"] [node name="Camera3D" type="Camera3D" parent="SpringArmOffset/SpringArm3D"]
current = true current = true
+29
View File
@@ -11,15 +11,29 @@ signal respawned()
@export var max_health: float = 100.0 @export var max_health: float = 100.0
@export var can_respawn: bool = true @export var can_respawn: bool = true
@export var respawn_delay: float = 3.0 @export var respawn_delay: float = 3.0
@export var health_regen: float = 0.0 ## HP restored per second by passive regen (server-applied). 0 = disabled.
@export var regen_delay: float = 4.0 ## Seconds to wait after taking damage before regen resumes.
const REGEN_TICK := 0.5 # Seconds between regen applications
var current_health: float = 100.0 var current_health: float = 100.0
var is_dead: bool = false var is_dead: bool = false
var _respawn_point: Vector3 = Vector3.ZERO var _respawn_point: Vector3 = Vector3.ZERO
var _last_damage_time: float = -1000.0
func _ready(): func _ready():
current_health = max_health current_health = max_health
_respawn_point = global_position _respawn_point = global_position
# Server drives a passive regen tick for any unit with health_regen > 0
if multiplayer.is_server() and health_regen > 0.0:
var regen_timer := Timer.new()
regen_timer.name = "RegenTimer"
regen_timer.wait_time = REGEN_TICK
regen_timer.autostart = true
add_child(regen_timer)
regen_timer.timeout.connect(_on_regen_tick)
func _enter_tree(): func _enter_tree():
set_multiplayer_authority(str(name).to_int()) set_multiplayer_authority(str(name).to_int())
@@ -34,6 +48,9 @@ func take_damage(amount: float, attacker_id: int = -1, knockback: float = 0.0, a
if is_dead: if is_dead:
return return
# Record when we were last hit so passive regen can pause briefly
_last_damage_time = Time.get_ticks_msec() / 1000.0
# Apply blocking reduction if applicable (duck typing - check if method exists) # Apply blocking reduction if applicable (duck typing - check if method exists)
var final_damage = amount var final_damage = amount
var final_knockback = knockback var final_knockback = knockback
@@ -68,6 +85,18 @@ func take_damage(amount: float, attacker_id: int = -1, knockback: float = 0.0, a
if current_health <= 0: if current_health <= 0:
_die(attacker_id) _die(attacker_id)
## Server-side passive regeneration tick (driven by RegenTimer)
func _on_regen_tick():
if not multiplayer.is_server():
return
if is_dead or health_regen <= 0.0 or current_health >= max_health:
return
# Hold off regen for a short window after taking damage
var now := Time.get_ticks_msec() / 1000.0
if now - _last_damage_time < regen_delay:
return
heal(health_regen * REGEN_TICK)
## Heal the unit ## Heal the unit
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func heal(amount: float): func heal(amount: float):
+32
View File
@@ -13,6 +13,7 @@ class_name BasicEnemy
@export var attack_damage: float = 15.0 @export var attack_damage: float = 15.0
@export var attack_knockback: float = 10.0 @export var attack_knockback: float = 10.0
@export var attack_cooldown: float = 1.5 @export var attack_cooldown: float = 1.5
@export var health_orb_drop_chance: float = 0.4 ## Chance (0-1) to drop a health orb on death
@export_category("Attack Timing") @export_category("Attack Timing")
@export var attack_startup: float = 0.3 # Wind-up before hit @export var attack_startup: float = 0.3 # Wind-up before hit
@export var attack_active: float = 0.4 # Hit window duration @export var attack_active: float = 0.4 # Hit window duration
@@ -36,6 +37,9 @@ func _enter_tree():
func _ready(): func _ready():
super._ready() super._ready()
# Wave lobsters die for good — no reviving (BaseEnemy defaults this to true)
can_respawn = false
# Find mesh, hitbox, and hurtbox # Find mesh, hitbox, and hurtbox
_mesh = get_node_or_null("Mesh") _mesh = get_node_or_null("Mesh")
_hitbox = get_node_or_null("HitBox") _hitbox = get_node_or_null("HitBox")
@@ -255,6 +259,34 @@ func _reset_material():
if _mesh and _original_material: if _mesh and _original_material:
_mesh.set_surface_override_material(0, _original_material.duplicate()) _mesh.set_surface_override_material(0, _original_material.duplicate())
## Handle death: drop loot, then despawn (server-authoritative, fires once per death)
func _die(killer_id: int):
var already_dead = is_dead
super._die(killer_id)
if not multiplayer.is_server() or already_dead:
return
# Chance to drop a health orb
if randf() < health_orb_drop_chance:
var level = get_tree().get_current_scene()
if level and level.has_method("spawn_health_orb"):
level.spawn_health_orb(global_position + Vector3.UP)
# Non-reviving enemies remove their corpse on every client after a short beat
if not can_respawn:
_schedule_despawn()
## Free this enemy on all peers a couple seconds after death
func _schedule_despawn():
await get_tree().create_timer(2.0).timeout
if is_instance_valid(self):
rpc("_despawn")
@rpc("any_peer", "call_local", "reliable")
func _despawn():
queue_free()
## Death callback ## Death callback
func _on_enemy_died(killer_id: int): func _on_enemy_died(killer_id: int):
super._on_enemy_died(killer_id) super._on_enemy_died(killer_id)
+26
View File
@@ -17,6 +17,11 @@ signal all_enemies_defeated()
@export var auto_start_next_wave: bool = false ## Auto-start next wave when all defeated @export var auto_start_next_wave: bool = false ## Auto-start next wave when all defeated
@export var wave_delay: float = 5.0 ## Delay before next wave auto-starts @export var wave_delay: float = 5.0 ## Delay before next wave auto-starts
@export_category("Timed Waves")
@export var auto_waves: bool = true ## Spawn a fresh wave on a repeating timer
@export var wave_interval: float = 15.0 ## Seconds between timed waves
@export var first_wave_delay: float = 3.0 ## Delay before the very first timed wave
@export_category("Enemy Pool") @export_category("Enemy Pool")
@export var enemy_scenes: Array[PackedScene] = [] ## List of enemy scenes to spawn from @export var enemy_scenes: Array[PackedScene] = [] ## List of enemy scenes to spawn from
@@ -26,6 +31,8 @@ var active_enemies: Array[Node] = []
var _wave_delay_timer: float = 0.0 var _wave_delay_timer: float = 0.0
var _waiting_for_next_wave: bool = false var _waiting_for_next_wave: bool = false
var _enemy_id_counter: int = 0 var _enemy_id_counter: int = 0
var _auto_waves_active: bool = false
var _auto_wave_timer: float = 0.0
## Debug visualization ## Debug visualization
@export var show_spawn_radius: bool = false ## Show spawn radius circle in game @export var show_spawn_radius: bool = false ## Show spawn radius circle in game
@@ -35,6 +42,18 @@ func _ready():
print("[EnemySpawner] Ready. Server: ", multiplayer.is_server()) print("[EnemySpawner] Ready. Server: ", multiplayer.is_server())
_create_debug_circle() _create_debug_circle()
## Begin spawning waves automatically on a timer (server only)
func start_auto_waves():
if not multiplayer.is_server() or not auto_waves:
return
_auto_waves_active = true
_auto_wave_timer = first_wave_delay
print("[EnemySpawner] Auto waves enabled - first wave in ", first_wave_delay, "s, then every ", wave_interval, "s")
## Stop the timed-wave loop
func stop_auto_waves():
_auto_waves_active = false
## Start a new wave ## Start a new wave
func start_wave(): func start_wave():
if not multiplayer.is_server(): if not multiplayer.is_server():
@@ -240,6 +259,13 @@ func _process(_delta):
if multiplayer.multiplayer_peer == null or not multiplayer.is_server(): if multiplayer.multiplayer_peer == null or not multiplayer.is_server():
return return
# Timed waves - spawn a fresh wave on a repeating interval
if _auto_waves_active:
_auto_wave_timer -= _delta
if _auto_wave_timer <= 0:
_auto_wave_timer = wave_interval
start_wave()
# Handle wave delay timer # Handle wave delay timer
if _waiting_for_next_wave: if _waiting_for_next_wave:
_wave_delay_timer -= _delta _wave_delay_timer -= _delta
+51
View File
@@ -0,0 +1,51 @@
extends Area3D
class_name HealthOrb
## Floating pickup that restores health when a player walks into it.
## Server-authoritative pickup; the bob/spin visuals run locally on every peer.
@export var heal_amount: float = 25.0
@export var bob_height: float = 0.3
@export var bob_speed: float = 2.0
@export var spin_speed: float = 2.0
var orb_id: int = -1 # Assigned by Level when spawned
var _base_y: float = 0.0
var _time: float = 0.0
var _collected: bool = false
@onready var _mesh: Node3D = get_node_or_null("Mesh")
func _ready():
_base_y = position.y
_time = randf() * TAU # Random phase so multiple orbs don't bob in unison
# Detect players (physics layer 1) without colliding with anything
collision_layer = 0
collision_mask = 1
# Only the server resolves pickups
if multiplayer.is_server():
body_entered.connect(_on_body_entered)
func _process(delta):
_time += delta
position.y = _base_y + sin(_time * bob_speed) * bob_height
if _mesh:
_mesh.rotate_y(spin_speed * delta)
func _on_body_entered(body: Node3D):
if _collected or not multiplayer.is_server():
return
if not (body is Character) or body.is_dead:
return
# Leave the orb in place if the player is already at full health
if body.current_health >= body.max_health:
return
_collected = true
body.heal(heal_amount)
# Remove the orb from every client via the level's centralized system
var level = get_tree().get_current_scene()
if level and level.has_method("remove_health_orb"):
level.remove_health_orb(orb_id)
+1
View File
@@ -0,0 +1 @@
uid://bbekeah8iio6t
+92
View File
@@ -6,6 +6,7 @@ extends Node3D
@onready var players_container: Node3D = $PlayersContainer @onready var players_container: Node3D = $PlayersContainer
@onready var weapons_container: Node3D = $WeaponsContainer @onready var weapons_container: Node3D = $WeaponsContainer
@onready var enemies_container: Node3D = $EnemiesContainer @onready var enemies_container: Node3D = $EnemiesContainer
var orbs_container: Node3D = null
@onready var enemy_spawner: EnemySpawner = $EnemySpawner @onready var enemy_spawner: EnemySpawner = $EnemySpawner
@onready var player_spawn_points: Node3D = $PlayerSpawnPoints @onready var player_spawn_points: Node3D = $PlayerSpawnPoints
@onready var menu: Control = $Menu @onready var menu: Control = $Menu
@@ -59,6 +60,15 @@ func _ready():
add_child(enemies_container) add_child(enemies_container)
print("Created EnemiesContainer") print("Created EnemiesContainer")
# Create or find health orbs container
if has_node("OrbsContainer"):
orbs_container = get_node("OrbsContainer")
else:
orbs_container = Node3D.new()
orbs_container.name = "OrbsContainer"
add_child(orbs_container)
print("Created OrbsContainer")
# Don't initialize weapons in _ready() - wait for Host/Join to be pressed # Don't initialize weapons in _ready() - wait for Host/Join to be pressed
# This is handled in initialize_multiplayer() which is called after the # This is handled in initialize_multiplayer() which is called after the
# multiplayer peer is properly set up # multiplayer peer is properly set up
@@ -96,6 +106,13 @@ func initialize_multiplayer():
# Spawn armed enemies # Spawn armed enemies
_spawn_armed_enemies() _spawn_armed_enemies()
# Scatter a few health orbs so the pickup is visible from the start
_spawn_initial_health_orbs()
# Begin automatic timed enemy waves
if enemy_spawner:
enemy_spawner.start_auto_waves()
# Spawn the host player (peer ID 1) # Spawn the host player (peer ID 1)
print("[Level] Spawning host player") print("[Level] Spawning host player")
var host_info = Network.players.get(1, {"nick": "Host", "skin": "blue"}) var host_info = Network.players.get(1, {"nick": "Host", "skin": "blue"})
@@ -265,6 +282,11 @@ func _on_player_connected(peer_id, player_info):
print("[Server] Syncing enemy ", enemy.name, " at position ", enemy.global_position, " to peer ", peer_id) print("[Server] Syncing enemy ", enemy.name, " at position ", enemy.global_position, " to peer ", peer_id)
rpc_id(peer_id, "_spawn_dummy_local", enemy_id, enemy.global_position) rpc_id(peer_id, "_spawn_dummy_local", enemy_id, enemy.global_position)
# Sync existing health orbs to the newly joined player
print("[Server] Syncing health orbs to newly connected peer: ", peer_id)
for orb_id in _active_orbs.keys():
rpc_id(peer_id, "_spawn_health_orb_local", orb_id, _active_orbs[orb_id])
# Sync equipped weapons for all existing players to the newly joined player # Sync equipped weapons for all existing players to the newly joined player
print("[Server] Syncing equipped weapons to newly connected peer: ", peer_id) print("[Server] Syncing equipped weapons to newly connected peer: ", peer_id)
for player_node in players_container.get_children(): for player_node in players_container.get_children():
@@ -647,6 +669,76 @@ func _client_spawn_weapon(weapon_data_path: String, spawn_position: Vector3, ini
print("[Client ", multiplayer.get_unique_id(), "] Calling spawn_world_weapon locally") print("[Client ", multiplayer.get_unique_id(), "] Calling spawn_world_weapon locally")
spawn_world_weapon(weapon_data_path, spawn_position, initial_velocity, weapon_id) spawn_world_weapon(weapon_data_path, spawn_position, initial_velocity, weapon_id)
# ---------- HEALTH ORBS ----------
var _orb_spawn_counter: int = 0
var _active_orbs: Dictionary = {} # orb_id -> spawn position (Vector3), for late-join sync
## Spawn a health orb at a position (server only, replicates to all clients)
func spawn_health_orb(spawn_pos: Vector3):
if not multiplayer.is_server():
return
_orb_spawn_counter += 1
rpc("_spawn_health_orb_local", _orb_spawn_counter, spawn_pos)
## Spawn a health orb on all clients (call_local spawns on server too)
@rpc("any_peer", "call_local", "reliable")
func _spawn_health_orb_local(orb_id: int, spawn_pos: Vector3):
if not orbs_container:
push_error("[Level] OrbsContainer not found!")
return
var orb_name = "HealthOrb_" + str(orb_id)
if orbs_container.has_node(orb_name):
return # Already spawned (e.g. duplicate sync)
var orb_scene = load("res://level/scenes/health_orb.tscn")
if not orb_scene:
push_error("[Level] Failed to load health_orb.tscn")
return
var orb = orb_scene.instantiate()
orb.orb_id = orb_id
orb.name = orb_name
orb.position = spawn_pos
orbs_container.add_child(orb, true)
# Server tracks active orbs for late-join sync
if multiplayer.is_server():
_active_orbs[orb_id] = spawn_pos
print("[Peer ", multiplayer.get_unique_id(), "] Spawned health orb ", orb_name, " at ", spawn_pos)
## Remove a health orb from all clients (called by HealthOrb when collected)
func remove_health_orb(orb_id: int):
if not multiplayer.is_server():
return
if _active_orbs.has(orb_id):
_active_orbs.erase(orb_id)
rpc("_remove_orb_on_clients", orb_id)
## RPC to remove an orb on all clients
@rpc("any_peer", "call_local", "reliable")
func _remove_orb_on_clients(orb_id: int):
var orb_name = "HealthOrb_" + str(orb_id)
if orbs_container and orbs_container.has_node(orb_name):
orbs_container.get_node(orb_name).queue_free()
## Scatter a few health orbs at server start
func _spawn_initial_health_orbs():
if not multiplayer.is_server():
return
# Wait a frame for everything to be ready
await get_tree().process_frame
var orb_positions = [
Vector3(6, 1, 6),
Vector3(-6, 1, -6),
Vector3(-6, 1, 6),
]
for pos in orb_positions:
spawn_health_orb(pos)
# ---------- ARMED ENEMY SPAWNING ---------- # ---------- ARMED ENEMY SPAWNING ----------
var _armed_enemy_counter: int = 0 var _armed_enemy_counter: int = 0
+5
View File
@@ -69,6 +69,11 @@ func _enter_tree():
$SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority() $SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority()
func _ready(): func _ready():
# Players passively regenerate health (set before super._ready so the regen timer is created)
if health_regen <= 0.0:
health_regen = 3.0
regen_delay = 5.0
super._ready() super._ready()
# Set respawn point to current position (where we spawned) - base_unit._ready already does this # Set respawn point to current position (where we spawned) - base_unit._ready already does this
# Don't override with a hardcoded position # Don't override with a hardcoded position
+41
View File
@@ -0,0 +1,41 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://level/ui/scripts/keybind_hint.gd" id="1_script"]
[ext_resource type="Theme" uid="uid://dvsh7tuhulnfm" path="res://level/ui/theme/wow_style.tres" id="2_theme"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kb"]
bg_color = Color(0.15, 0.15, 0.15, 0.85)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.6, 0.5, 0.2, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[node name="KeybindHint" type="PanelContainer"]
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -210.0
offset_top = 20.0
offset_right = -20.0
offset_bottom = 20.0
grow_horizontal = 0
grow_vertical = 1
theme = ExtResource("2_theme")
theme_override_styles/panel = SubResource("StyleBoxFlat_kb")
script = ExtResource("1_script")
[node name="Margin" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 8
[node name="Content" type="VBoxContainer" parent="Margin"]
layout_mode = 2
theme_override_constants/separation = 4
+12
View File
@@ -8,6 +8,7 @@ var unit_frame: Control = null
var target_frame: Control = null var target_frame: Control = null
var character_sheet: Control = null var character_sheet: Control = null
var tab_hint: Control = null var tab_hint: Control = null
var keybind_hint: Control = null
var escape_menu: Control = null var escape_menu: Control = null
# Player reference # Player reference
@@ -77,6 +78,7 @@ func _create_ui_components():
_create_unit_frame() _create_unit_frame()
_create_character_sheet() _create_character_sheet()
_create_tab_hint() _create_tab_hint()
_create_keybind_hint()
_create_escape_menu() _create_escape_menu()
## Create action bar at bottom of screen ## Create action bar at bottom of screen
@@ -125,6 +127,16 @@ func _create_tab_hint():
else: else:
push_error("[HUD] Failed to load tab_hint.tscn") push_error("[HUD] Failed to load tab_hint.tscn")
## Create keybind hint (always visible, top-right corner)
func _create_keybind_hint():
var keybind_hint_scene = load("res://level/ui/scenes/keybind_hint.tscn")
if keybind_hint_scene:
keybind_hint = keybind_hint_scene.instantiate()
add_child(keybind_hint)
print("[HUD] Keybind hint created")
else:
push_error("[HUD] Failed to load keybind_hint.tscn")
## Create escape menu (toggle with Escape) ## Create escape menu (toggle with Escape)
func _create_escape_menu(): func _create_escape_menu():
var escape_menu_scene = load("res://level/ui/scenes/escape_menu.tscn") var escape_menu_scene = load("res://level/ui/scenes/escape_menu.tscn")
+75
View File
@@ -0,0 +1,75 @@
extends PanelContainer
## Always-on compact panel listing the current controls.
## Reads bindings straight from the InputMap so it stays accurate if keys are rebound.
# Each row is a friendly label plus the input action(s) that feed it.
# Grouped actions (like movement) show their keys joined together.
const ROWS := [
{ "label": "Move", "actions": ["move_forward", "move_left", "move_backward", "move_right"] },
{ "label": "Jump", "actions": ["jump"] },
{ "label": "Sprint", "actions": ["shift"] },
{ "label": "Dash", "actions": ["dash"] },
{ "label": "Attack", "actions": ["attack"] },
{ "label": "Block", "actions": ["block"] },
{ "label": "Pick Up / Drop", "actions": ["pickup"] },
{ "label": "Character Sheet", "actions": ["toggle_character_sheet"] },
{ "label": "Chat", "actions": ["toggle_chat"] },
{ "label": "Menu", "actions": ["quit"] },
]
func _ready() -> void:
_build()
func _build() -> void:
var content: VBoxContainer = $Margin/Content
var title := Label.new()
title.text = "CONTROLS"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 16)
title.add_theme_color_override("font_color", Color(1, 0.8, 0, 1))
title.add_theme_color_override("font_outline_color", Color.BLACK)
title.add_theme_constant_override("outline_size", 2)
content.add_child(title)
var grid := GridContainer.new()
grid.columns = 2
grid.add_theme_constant_override("h_separation", 12)
grid.add_theme_constant_override("v_separation", 3)
content.add_child(grid)
for row in ROWS:
var keys: Array[String] = []
for action in row["actions"]:
var k := _key_text(action)
if k != "":
keys.append(k)
if keys.is_empty():
continue
grid.add_child(_make_label(" ".join(keys), Color(1, 0.82, 0.2, 1)))
grid.add_child(_make_label(row["label"], Color.WHITE))
func _make_label(text: String, color: Color) -> Label:
var l := Label.new()
l.text = text
l.add_theme_font_size_override("font_size", 13)
l.add_theme_color_override("font_color", color)
l.add_theme_color_override("font_outline_color", Color.BLACK)
l.add_theme_constant_override("outline_size", 1)
return l
## Returns a human-readable name for the first event bound to an action.
func _key_text(action: String) -> String:
if not InputMap.has_action(action):
return ""
for ev in InputMap.action_get_events(action):
if ev is InputEventKey:
var code: int = ev.physical_keycode if ev.physical_keycode != 0 else ev.keycode
return OS.get_keycode_string(code)
elif ev is InputEventMouseButton:
match ev.button_index:
MOUSE_BUTTON_LEFT: return "LMB"
MOUSE_BUTTON_RIGHT: return "RMB"
MOUSE_BUTTON_MIDDLE: return "MMB"
_: return "Mouse %d" % ev.button_index
return ""
+1
View File
@@ -0,0 +1 @@
uid://dh85b10pfih5e
+4 -2
View File
@@ -24,8 +24,10 @@ HUD="*res://level/ui/scripts/hud_manager.gd"
[display] [display]
window/size/viewport_width=1280 window/size/viewport_width=1920
window/size/viewport_height=720 window/size/viewport_height=1080
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
[input] [input]