feat: implement procedural arena generation system and support infrastructure
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
@tool
|
||||
extends Node3D
|
||||
class_name ArenaGenerator
|
||||
|
||||
@export var generate: bool = false:
|
||||
set(value):
|
||||
if value:
|
||||
if not is_inside_tree():
|
||||
call_deferred("_generate_tactical_arena")
|
||||
else:
|
||||
_generate_tactical_arena()
|
||||
generate = false
|
||||
|
||||
@export var clear: bool = false:
|
||||
set(value):
|
||||
if value:
|
||||
_clear_arena()
|
||||
clear = false
|
||||
|
||||
@export var sector_size = 24.0
|
||||
@export var step_height = 3.0
|
||||
@export var wall_height = 12.0
|
||||
|
||||
@export var floor_color = Color(0.2, 0.22, 0.25)
|
||||
@export var wall_color = Color(0.3, 0.4, 0.5)
|
||||
@export var obstacle_color = Color(0.6, 0.35, 0.15)
|
||||
@export var ramp_color = Color(0.7, 0.4, 0.2)
|
||||
|
||||
func _clear_arena() -> void:
|
||||
var children = get_children()
|
||||
for c in children:
|
||||
remove_child(c)
|
||||
c.free()
|
||||
|
||||
func _generate_tactical_arena() -> void:
|
||||
_clear_arena()
|
||||
|
||||
var cols = 3 # Left, Mid, Right
|
||||
var rows = 5 # Spawn A, Conn A, Mid, Conn B, Spawn B
|
||||
|
||||
# Generate elevations (symmetric for fairness)
|
||||
var elevations = []
|
||||
for x in range(cols):
|
||||
elevations.append([])
|
||||
for z in range(rows):
|
||||
elevations[x].append(0)
|
||||
|
||||
# Randomize half, mirror other half
|
||||
for x in range(cols):
|
||||
for z in range(3): # 0, 1, 2
|
||||
elevations[x][z] = randi_range(0, 2)
|
||||
|
||||
# Mirror to the other side (point symmetry)
|
||||
# Or mirror horizontally (Spawn A == Spawn B setup)
|
||||
# A standard mirror across mid:
|
||||
for x in range(cols):
|
||||
elevations[x][3] = elevations[cols - 1 - x][1]
|
||||
elevations[x][4] = elevations[cols - 1 - x][0]
|
||||
|
||||
# Build Sector Floors
|
||||
var _sec_size = float(sector_size if sector_size != null else 24.0)
|
||||
var _step_h = float(step_height if step_height != null else 3.0)
|
||||
var _wall_h = float(wall_height if wall_height != null else 12.0)
|
||||
|
||||
var _f_col = floor_color if floor_color != null else Color(0.2, 0.22, 0.25)
|
||||
var _w_col = wall_color if wall_color != null else Color(0.3, 0.4, 0.5)
|
||||
|
||||
var hw = (cols * _sec_size) / 2.0
|
||||
var hl = (rows * _sec_size) / 2.0
|
||||
|
||||
# Global origin is center of grid
|
||||
for x in range(cols):
|
||||
for z in range(rows):
|
||||
var cx = -hw + x * _sec_size + _sec_size / 2.0
|
||||
var cz = -hl + z * _sec_size + _sec_size / 2.0
|
||||
var y = float(elevations[x][z]) * _step_h
|
||||
|
||||
# Make floors incredibly thick (40 units) so they act as solid pillars all the way down
|
||||
# This prevents players from walking underneath higher sectors and falling into the void
|
||||
_create_box("Sector_%d_%d" % [x, z], Vector3(cx, y - 20.0, cz), Vector3(_sec_size, 40.0, _sec_size), _f_col)
|
||||
|
||||
# Add cover in sector (except spawns z=0 and z=4)
|
||||
if z > 0 and z < 4:
|
||||
_generate_cover("Cover_%d_%d" % [x, z], Vector3(cx, y, cz))
|
||||
|
||||
# Ramps between Z adjacent (North/South)
|
||||
if z < rows - 1:
|
||||
var diff = elevations[x][z+1] - elevations[x][z]
|
||||
if diff != 0:
|
||||
_create_ramp_z("RampZ_%d_%d" % [x, z], cx, cz + _sec_size / 2.0, y, diff)
|
||||
|
||||
# Ramps/Walls between X adjacent (East/West)
|
||||
if x < cols - 1:
|
||||
var is_connector = randf() > 0.4 # 60% chance to be an open connector
|
||||
var diff = elevations[x+1][z] - elevations[x][z]
|
||||
|
||||
if is_connector:
|
||||
if diff != 0:
|
||||
_create_ramp_x("RampX_%d_%d" % [x, z], cx + _sec_size / 2.0, cz, y, diff)
|
||||
else:
|
||||
# Solid wall
|
||||
var max_y = max(elevations[x][z], elevations[x+1][z]) * _step_h
|
||||
_create_box("InnerWall_%d_%d" % [x, z], Vector3(cx + _sec_size / 2.0, max_y + _wall_h/2, cz), Vector3(1.0, _wall_h, _sec_size), _w_col)
|
||||
|
||||
# Outer Walls
|
||||
_create_box("Wall_North", Vector3(0, _wall_h * 0.5, -hl), Vector3(hw * 2, _wall_h, 1.0), _w_col)
|
||||
_create_box("Wall_South", Vector3(0, _wall_h * 0.5, hl), Vector3(hw * 2, _wall_h, 1.0), _w_col)
|
||||
_create_box("Wall_East", Vector3(hw, _wall_h * 0.5, 0), Vector3(1.0, _wall_h, hl * 2), _w_col)
|
||||
_create_box("Wall_West", Vector3(-hw, _wall_h * 0.5, 0), Vector3(1.0, _wall_h, hl * 2), _w_col)
|
||||
|
||||
|
||||
func _generate_cover(base_name: String, center: Vector3) -> void:
|
||||
var _sec_size = float(sector_size if sector_size != null else 24.0)
|
||||
var _o_col = obstacle_color if obstacle_color != null else Color(0.6, 0.35, 0.15)
|
||||
# Generate 1-3 blocks of cover
|
||||
var count = randi_range(1, 3)
|
||||
for i in range(count):
|
||||
var ox = randf_range(-_sec_size*0.3, _sec_size*0.3)
|
||||
var oz = randf_range(-_sec_size*0.3, _sec_size*0.3)
|
||||
var sx = randf_range(2.0, 6.0)
|
||||
var sy = randf_range(1.0, 4.0)
|
||||
var sz = randf_range(2.0, 6.0)
|
||||
_create_box("%s_%d" % [base_name, i], center + Vector3(ox, sy * 0.5, oz), Vector3(sx, sy, sz), _o_col)
|
||||
|
||||
func _create_ramp_z(node_name: String, cx: float, cz: float, base_y: float, diff_levels: int) -> void:
|
||||
var _step_h = float(step_height if step_height != null else 3.0)
|
||||
var _sec_size = float(sector_size if sector_size != null else 24.0)
|
||||
var _r_col = ramp_color if ramp_color != null else Color(0.7, 0.4, 0.2)
|
||||
# Ramp along Z axis (North/South)
|
||||
var h = abs(diff_levels) * _step_h
|
||||
var length = _sec_size * 0.4 # Ramp takes up 40% of sector depth
|
||||
var center_y = base_y + h / 2.0
|
||||
|
||||
var dir = sign(diff_levels)
|
||||
var rot_x = 0
|
||||
if dir > 0:
|
||||
rot_x = rad_to_deg(atan2(h, length))
|
||||
else:
|
||||
rot_x = -rad_to_deg(atan2(h, length))
|
||||
|
||||
var true_length = sqrt(length*length + h*h)
|
||||
_create_ramp(node_name, Vector3(cx, center_y, cz), Vector3(_sec_size * 0.5, 0.5, true_length), Vector3(rot_x, 0, 0), _r_col)
|
||||
|
||||
func _create_ramp_x(node_name: String, cx: float, cz: float, base_y: float, diff_levels: int) -> void:
|
||||
var _step_h = float(step_height if step_height != null else 3.0)
|
||||
var _sec_size = float(sector_size if sector_size != null else 24.0)
|
||||
var _r_col = ramp_color if ramp_color != null else Color(0.7, 0.4, 0.2)
|
||||
# Ramp along X axis (East/West)
|
||||
var h = abs(diff_levels) * _step_h
|
||||
var length = _sec_size * 0.4
|
||||
var center_y = base_y + h / 2.0
|
||||
|
||||
var dir = sign(diff_levels)
|
||||
var rot_z = 0
|
||||
if dir > 0:
|
||||
rot_z = -rad_to_deg(atan2(h, length))
|
||||
else:
|
||||
rot_z = rad_to_deg(atan2(h, length))
|
||||
|
||||
var true_length = sqrt(length*length + h*h)
|
||||
_create_ramp(node_name, Vector3(cx, center_y, cz), Vector3(true_length, 0.5, _sec_size * 0.5), Vector3(0, 0, rot_z), _r_col)
|
||||
|
||||
|
||||
func _create_box(node_name: String, pos: Vector3, size: Vector3, color: Color) -> void:
|
||||
var body := StaticBody3D.new()
|
||||
body.name = node_name
|
||||
body.position = pos
|
||||
|
||||
var shape := CollisionShape3D.new()
|
||||
shape.name = "CollisionShape3D"
|
||||
shape.shape = BoxShape3D.new()
|
||||
shape.shape.size = size
|
||||
body.add_child(shape)
|
||||
|
||||
var mesh := MeshInstance3D.new()
|
||||
mesh.name = "MeshInstance3D"
|
||||
mesh.mesh = BoxMesh.new()
|
||||
mesh.mesh.size = size
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = color
|
||||
mat.roughness = 0.8
|
||||
mesh.mesh.surface_set_material(0, mat)
|
||||
body.add_child(mesh)
|
||||
|
||||
add_child(body)
|
||||
if Engine.is_editor_hint():
|
||||
var scene_root = get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
body.owner = scene_root
|
||||
shape.owner = scene_root
|
||||
mesh.owner = scene_root
|
||||
|
||||
func _create_ramp(node_name: String, pos: Vector3, size: Vector3, rot_deg: Vector3, color: Color) -> void:
|
||||
var body := StaticBody3D.new()
|
||||
body.name = node_name
|
||||
body.position = pos
|
||||
body.rotation_degrees = rot_deg
|
||||
|
||||
var shape := CollisionShape3D.new()
|
||||
shape.name = "CollisionShape3D"
|
||||
shape.shape = BoxShape3D.new()
|
||||
shape.shape.size = size
|
||||
body.add_child(shape)
|
||||
|
||||
var mesh := MeshInstance3D.new()
|
||||
mesh.name = "MeshInstance3D"
|
||||
mesh.mesh = BoxMesh.new()
|
||||
mesh.mesh.size = size
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = color
|
||||
mat.roughness = 0.8
|
||||
mesh.mesh.surface_set_material(0, mat)
|
||||
body.add_child(mesh)
|
||||
|
||||
add_child(body)
|
||||
if Engine.is_editor_hint():
|
||||
var scene_root = get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
body.owner = scene_root
|
||||
shape.owner = scene_root
|
||||
mesh.owner = scene_root
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxf17ncr1fxd1
|
||||
@@ -0,0 +1,5 @@
|
||||
[map]
|
||||
name="Procedural Arena"
|
||||
scene_path="res://scenes/maps/procedural_arena/procedural_arena.tscn"
|
||||
color1=Color(0.15, 0.4, 0.2, 1.0)
|
||||
color2=Color(0.2, 0.6, 0.3, 1.0)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,477 @@
|
||||
extends Node3D
|
||||
|
||||
|
||||
var _speed_label: Label
|
||||
var _state_label: Label
|
||||
var _chain_label: Label
|
||||
var _weapon_label: Label
|
||||
var _grapple_icon: TextureRect
|
||||
var _dash_icon: TextureRect
|
||||
var _grapple_label: Label
|
||||
var _dash_label: Label
|
||||
var _fps_label: Label
|
||||
var _player: CharacterBody3D
|
||||
var _debug_ui_panel: PanelContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Hide mouse
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
_build_hud()
|
||||
|
||||
# Multiplayer Spawning
|
||||
var spawner = MultiplayerSpawner.new()
|
||||
spawner.name = "PlayerSpawner"
|
||||
spawner.spawn_path = "." # Spawn directly as children of the level
|
||||
spawner.spawn_function = _spawn_player
|
||||
add_child(spawner)
|
||||
|
||||
var nm = get_node_or_null("/root/NetworkManager")
|
||||
if nm and nm.multiplayer.is_server():
|
||||
# Spawn existing players
|
||||
for pid in nm.player_stats.keys():
|
||||
spawner.spawn(pid)
|
||||
|
||||
# Spawn late joiners
|
||||
nm.player_connected.connect(func(pid):
|
||||
spawner.spawn(pid)
|
||||
)
|
||||
|
||||
# Despawn on disconnect
|
||||
nm.player_disconnected.connect(func(pid):
|
||||
var p = get_node_or_null(str(pid))
|
||||
if p:
|
||||
p.queue_free()
|
||||
)
|
||||
|
||||
|
||||
# ── Player ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
var player := CharacterBody3D.new()
|
||||
player.name = str(pid)
|
||||
player.set_multiplayer_authority(pid)
|
||||
|
||||
# Alternate spawn sides (Team A / Team B style)
|
||||
var spawn_z = 40.0 if (pid % 2 == 0) else -40.0
|
||||
player.position = Vector3(randf_range(-6, 6), 10.0, spawn_z + randf_range(-4, 4))
|
||||
|
||||
# Server Synchronizer (Host is the ground truth)
|
||||
var server_sync = MultiplayerSynchronizer.new()
|
||||
server_sync.name = "ServerSynchronizer"
|
||||
server_sync.set_multiplayer_authority(1) # Host always controls these
|
||||
var server_rep_config = SceneReplicationConfig.new()
|
||||
server_rep_config.add_property(":position")
|
||||
server_rep_config.add_property(":synced_movement_state")
|
||||
server_rep_config.add_property(":synced_movement_speed")
|
||||
server_rep_config.add_property(":synced_is_crouching")
|
||||
server_rep_config.add_property(":health")
|
||||
server_rep_config.add_property(":shield")
|
||||
server_rep_config.add_property(":is_dead")
|
||||
server_rep_config.add_property(":synced_grapple_point")
|
||||
server_rep_config.add_property(":synced_is_grapple_shooting")
|
||||
server_sync.replication_config = server_rep_config
|
||||
player.add_child(server_sync)
|
||||
|
||||
# Client Synchronizer (Client dictates their aim and loadout setup)
|
||||
var client_sync = MultiplayerSynchronizer.new()
|
||||
client_sync.name = "MultiplayerSynchronizer"
|
||||
client_sync.set_multiplayer_authority(pid)
|
||||
var client_rep_config = SceneReplicationConfig.new()
|
||||
client_rep_config.add_property(":rotation")
|
||||
client_rep_config.add_property("HeadPivot:rotation")
|
||||
client_rep_config.add_property(":synced_weapon_path")
|
||||
client_rep_config.add_property(":synced_loadout_p1")
|
||||
client_rep_config.add_property(":synced_loadout_p2")
|
||||
client_rep_config.add_property(":synced_loadout_sp")
|
||||
client_rep_config.add_property(":synced_loadout_melee")
|
||||
client_rep_config.add_property(":synced_loadout_ready")
|
||||
client_rep_config.add_property(":synced_is_targeting")
|
||||
client_rep_config.add_property(":synced_homing_target_pos")
|
||||
client_sync.replication_config = client_rep_config
|
||||
player.add_child(client_sync)
|
||||
|
||||
# Collision shape
|
||||
var col_shape := CollisionShape3D.new()
|
||||
col_shape.name = "CollisionShape3D"
|
||||
var capsule := CapsuleShape3D.new()
|
||||
capsule.radius = 0.4
|
||||
capsule.height = 1.8
|
||||
col_shape.shape = capsule
|
||||
player.add_child(col_shape)
|
||||
|
||||
# Apply movement controller script
|
||||
var mover_script = preload("res://movement/player_movement_controller.gd")
|
||||
if mover_script:
|
||||
player.set_script(mover_script)
|
||||
|
||||
# Humanoid Model for Player (Shadows only locally, visible to others)
|
||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||
humanoid.name = "HumanoidModel"
|
||||
humanoid.color = Color(0.2, 0.4, 0.8) # Blueish for player
|
||||
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
||||
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
||||
player.add_child(humanoid)
|
||||
|
||||
# Movement State Machine
|
||||
var sm := Node.new()
|
||||
sm.name = "MovementStateMachine"
|
||||
player.add_child(sm)
|
||||
|
||||
var sm_script = preload("res://movement/movement_state_machine.gd")
|
||||
if sm_script:
|
||||
sm.set_script(sm_script)
|
||||
|
||||
# Add all movement states
|
||||
var state_scripts := {
|
||||
"ground": "res://movement/states/state_ground.gd",
|
||||
"air": "res://movement/states/state_air.gd",
|
||||
"wall_run": "res://movement/states/state_wall_run.gd",
|
||||
"wall_cling": "res://movement/states/state_wall_cling.gd",
|
||||
"slide": "res://movement/states/state_slide.gd",
|
||||
"dash": "res://movement/states/state_dash.gd",
|
||||
}
|
||||
for state_name in state_scripts:
|
||||
var st := Node.new()
|
||||
st.name = "state_" + state_name
|
||||
sm.add_child(st)
|
||||
var st_script = load(state_scripts[state_name])
|
||||
if st_script:
|
||||
st.set_script(st_script)
|
||||
|
||||
# FPS Camera Rig (HeadPivot → Camera3D)
|
||||
var head_pivot := Node3D.new()
|
||||
head_pivot.name = "HeadPivot"
|
||||
head_pivot.position = Vector3(0, 0.7, 0) # Eye height
|
||||
player.add_child(head_pivot)
|
||||
|
||||
var camera := Camera3D.new()
|
||||
camera.name = "Camera3D"
|
||||
camera.current = (pid == multiplayer.get_unique_id())
|
||||
camera.fov = 90.0
|
||||
head_pivot.add_child(camera)
|
||||
|
||||
# ── Weapon Setup ────────────────────────────────────────────────────────────
|
||||
var wmanager_script = preload("res://weapons/weapon_manager.gd")
|
||||
if wmanager_script:
|
||||
var wman = wmanager_script.new()
|
||||
wman.name = "WeaponManager"
|
||||
wman.player = player
|
||||
wman.camera = camera
|
||||
camera.add_child(wman)
|
||||
|
||||
var rig_script = preload("res://characters/player/fps_camera_rig.gd")
|
||||
if rig_script:
|
||||
head_pivot.set_script(rig_script)
|
||||
|
||||
# Store original capsule height
|
||||
sm.original_capsule_height = capsule.height
|
||||
|
||||
# ── Dependencies ────────────────────────────────────────────────────────
|
||||
sm.player = player
|
||||
sm.params = player.params
|
||||
head_pivot.params = player.params
|
||||
|
||||
# Authority configuration
|
||||
var is_local = (pid == multiplayer.get_unique_id())
|
||||
if is_local:
|
||||
_player = player
|
||||
player.set_physics_process(true)
|
||||
player.set_process(true)
|
||||
sm.set_physics_process(true)
|
||||
else:
|
||||
player.set_physics_process(false)
|
||||
player.set_process(true)
|
||||
player.set_process_input(false)
|
||||
sm.set_physics_process(false)
|
||||
sm.set_process(false)
|
||||
sm.set_process_input(false)
|
||||
head_pivot.set_process_input(false)
|
||||
head_pivot.set_process(false)
|
||||
head_pivot.set_physics_process(false)
|
||||
var wman = camera.get_node_or_null("WeaponManager")
|
||||
if wman:
|
||||
wman.set_process_input(false)
|
||||
wman.set_process(false)
|
||||
wman.set_physics_process(false)
|
||||
wman.visible = false
|
||||
var cv = wman.get("canvas_layer")
|
||||
if cv:
|
||||
cv.visible = false
|
||||
|
||||
return player
|
||||
|
||||
|
||||
# ── HUD ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_hud() -> void:
|
||||
var canvas := CanvasLayer.new()
|
||||
canvas.name = "UI"
|
||||
add_child(canvas)
|
||||
|
||||
# ── Crosshair ─────────────────────────────────────────────────────────
|
||||
var crosshair := Control.new()
|
||||
crosshair.name = "Crosshair"
|
||||
crosshair.set_anchors_preset(Control.PRESET_CENTER)
|
||||
crosshair.custom_minimum_size = Vector2(20, 20)
|
||||
canvas.add_child(crosshair)
|
||||
|
||||
var ch_dot := ColorRect.new()
|
||||
ch_dot.name = "Dot"
|
||||
ch_dot.color = Color(1, 1, 1, 0.8)
|
||||
ch_dot.size = Vector2(4, 4)
|
||||
ch_dot.position = Vector2(-2, -2)
|
||||
crosshair.add_child(ch_dot)
|
||||
|
||||
# Crosshair lines
|
||||
for data in [
|
||||
{"pos": Vector2(-10, -1), "size": Vector2(6, 2)}, # Left
|
||||
{"pos": Vector2(4, -1), "size": Vector2(6, 2)}, # Right
|
||||
{"pos": Vector2(-1, -10), "size": Vector2(2, 6)}, # Top
|
||||
{"pos": Vector2(-1, 4), "size": Vector2(2, 6)}, # Bottom
|
||||
]:
|
||||
var line := ColorRect.new()
|
||||
line.color = Color(1, 1, 1, 0.6)
|
||||
line.position = data["pos"]
|
||||
line.size = data["size"]
|
||||
crosshair.add_child(line)
|
||||
|
||||
# ── Info panel background ─────────────────────────────────────────────
|
||||
_fps_label = Label.new()
|
||||
_fps_label.name = "FPSLabel"
|
||||
_fps_label.text = "FPS: 0"
|
||||
_fps_label.add_theme_font_size_override("font_size", 24)
|
||||
_fps_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0.2))
|
||||
_fps_label.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||
_fps_label.add_theme_constant_override("outline_size", 4)
|
||||
_fps_label.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
_fps_label.position = Vector2(12, 12)
|
||||
canvas.add_child(_fps_label)
|
||||
|
||||
_debug_ui_panel = PanelContainer.new()
|
||||
_debug_ui_panel.name = "InfoPanel"
|
||||
_debug_ui_panel.offset_left = 12
|
||||
_debug_ui_panel.offset_top = 50
|
||||
_debug_ui_panel.offset_right = 400
|
||||
_debug_ui_panel.offset_bottom = 160
|
||||
var panel_style := StyleBoxFlat.new()
|
||||
panel_style.bg_color = Color(0, 0, 0, 0.55)
|
||||
panel_style.corner_radius_top_left = 8
|
||||
panel_style.corner_radius_top_right = 8
|
||||
panel_style.corner_radius_bottom_left = 8
|
||||
panel_style.corner_radius_bottom_right = 8
|
||||
panel_style.content_margin_left = 12
|
||||
panel_style.content_margin_top = 8
|
||||
panel_style.content_margin_right = 12
|
||||
panel_style.content_margin_bottom = 8
|
||||
_debug_ui_panel.add_theme_stylebox_override("panel", panel_style)
|
||||
canvas.add_child(_debug_ui_panel)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.name = "InfoVBox"
|
||||
_debug_ui_panel.add_child(vbox)
|
||||
|
||||
_speed_label = Label.new()
|
||||
_speed_label.name = "SpeedLabel"
|
||||
_speed_label.text = "Speed: 0.0 m/s"
|
||||
_speed_label.add_theme_font_size_override("font_size", 18)
|
||||
_speed_label.add_theme_color_override("font_color", Color(0.3, 1.0, 0.5))
|
||||
vbox.add_child(_speed_label)
|
||||
|
||||
_state_label = Label.new()
|
||||
_state_label.name = "StateLabel"
|
||||
_state_label.text = "State: ground"
|
||||
_state_label.add_theme_font_size_override("font_size", 16)
|
||||
_state_label.add_theme_color_override("font_color", Color(0.7, 0.85, 1.0))
|
||||
vbox.add_child(_state_label)
|
||||
|
||||
_chain_label = Label.new()
|
||||
_chain_label.name = "ChainLabel"
|
||||
_chain_label.text = "Chain: 0 (+0%)"
|
||||
_chain_label.add_theme_font_size_override("font_size", 16)
|
||||
_chain_label.add_theme_color_override("font_color", Color(1.0, 0.8, 0.3))
|
||||
vbox.add_child(_chain_label)
|
||||
|
||||
|
||||
# ── Weapon & Ammo Panel ───────────────────────────────────────────────
|
||||
var wp_panel := PanelContainer.new()
|
||||
wp_panel.name = "WeaponPanel"
|
||||
wp_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
wp_panel.offset_left = -250
|
||||
wp_panel.offset_top = -100
|
||||
wp_panel.offset_right = -20
|
||||
wp_panel.offset_bottom = -20
|
||||
var wp_style := StyleBoxFlat.new()
|
||||
wp_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
wp_style.corner_radius_top_left = 8
|
||||
wp_style.corner_radius_top_right = 8
|
||||
wp_style.corner_radius_bottom_left = 8
|
||||
wp_style.corner_radius_bottom_right = 8
|
||||
wp_style.content_margin_left = 16
|
||||
wp_style.content_margin_top = 12
|
||||
wp_style.content_margin_right = 16
|
||||
wp_style.content_margin_bottom = 12
|
||||
wp_panel.add_theme_stylebox_override("panel", wp_style)
|
||||
wp_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(wp_panel)
|
||||
|
||||
_weapon_label = Label.new()
|
||||
_weapon_label.name = "WeaponLabel"
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
_weapon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_weapon_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||
_weapon_label.add_theme_font_size_override("font_size", 24)
|
||||
wp_panel.add_child(_weapon_label)
|
||||
|
||||
# ── Utilities Panel ───────────────────────────────────────────────────────
|
||||
var util_panel := PanelContainer.new()
|
||||
util_panel.name = "UtilPanel"
|
||||
util_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
util_panel.offset_left = -200
|
||||
util_panel.offset_top = -180
|
||||
util_panel.offset_right = -20
|
||||
util_panel.offset_bottom = -110
|
||||
var util_style := StyleBoxFlat.new()
|
||||
util_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
util_style.corner_radius_top_left = 8
|
||||
util_style.corner_radius_top_right = 8
|
||||
util_style.corner_radius_bottom_left = 8
|
||||
util_style.corner_radius_bottom_right = 8
|
||||
util_style.content_margin_left = 12
|
||||
util_style.content_margin_top = 8
|
||||
util_style.content_margin_right = 12
|
||||
util_style.content_margin_bottom = 8
|
||||
util_panel.add_theme_stylebox_override("panel", util_style)
|
||||
util_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(util_panel)
|
||||
|
||||
var util_hbox := HBoxContainer.new()
|
||||
util_hbox.name = "UtilHBox"
|
||||
util_hbox.add_theme_constant_override("separation", 20)
|
||||
util_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_panel.add_child(util_hbox)
|
||||
|
||||
var grapple_vbox := VBoxContainer.new()
|
||||
grapple_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(grapple_vbox)
|
||||
|
||||
_grapple_icon = TextureRect.new()
|
||||
_grapple_icon.texture = load("res://assets/ui/grapple_icon.jpg")
|
||||
_grapple_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_grapple_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_grapple_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
grapple_vbox.add_child(_grapple_icon)
|
||||
|
||||
_grapple_label = Label.new()
|
||||
_grapple_label.text = "Grapple"
|
||||
_grapple_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_grapple_label.add_theme_font_size_override("font_size", 12)
|
||||
grapple_vbox.add_child(_grapple_label)
|
||||
|
||||
var dash_vbox := VBoxContainer.new()
|
||||
dash_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(dash_vbox)
|
||||
|
||||
_dash_icon = TextureRect.new()
|
||||
_dash_icon.texture = load("res://assets/ui/dash_icon.jpg")
|
||||
_dash_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_dash_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_dash_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
dash_vbox.add_child(_dash_icon)
|
||||
|
||||
_dash_label = Label.new()
|
||||
_dash_label.text = "Ready"
|
||||
_dash_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_dash_label.add_theme_font_size_override("font_size", 12)
|
||||
dash_vbox.add_child(_dash_label)
|
||||
|
||||
# ── Utility: Key Name ─────────────────────────────────────────────────────────
|
||||
|
||||
func _get_key_name(action: String) -> String:
|
||||
if not InputMap.has_action(action):
|
||||
return "?"
|
||||
var events = InputMap.action_get_events(action)
|
||||
for e in events:
|
||||
if e is InputEventKey:
|
||||
var code = e.physical_keycode if e.physical_keycode != 0 else e.keycode
|
||||
return OS.get_keycode_string(code)
|
||||
elif e is InputEventMouseButton:
|
||||
if e.button_index == MOUSE_BUTTON_LEFT: return "LClick"
|
||||
elif e.button_index == MOUSE_BUTTON_RIGHT: return "RClick"
|
||||
elif e.button_index == MOUSE_BUTTON_MIDDLE: return "MClick"
|
||||
return "?"
|
||||
|
||||
# ── HUD Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not _player or not is_instance_valid(_player):
|
||||
return
|
||||
|
||||
if _debug_ui_panel:
|
||||
_debug_ui_panel.visible = SettingsManager.show_debug_ui
|
||||
|
||||
if _fps_label:
|
||||
_fps_label.visible = SettingsManager.show_fps
|
||||
if _fps_label.visible:
|
||||
_fps_label.text = "FPS: %d" % Engine.get_frames_per_second()
|
||||
|
||||
var vel: Vector3 = _player.velocity
|
||||
var hspeed := Vector2(vel.x, vel.z).length()
|
||||
var total_speed := vel.length()
|
||||
|
||||
if _speed_label:
|
||||
_speed_label.text = "Speed: %.1f m/s (total: %.1f)" % [hspeed, total_speed]
|
||||
|
||||
if _state_label:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_state_label.text = "State: %s" % sm.current_state
|
||||
|
||||
if _chain_label:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_chain_label.text = "Chain: %d (+%d%%)" % [sm.chain_count, int(sm.current_chain_bonus * 100)]
|
||||
|
||||
# Update utility indicators
|
||||
if sm.current_state == "grapple" or sm.is_grapple_shooting:
|
||||
_grapple_icon.modulate = Color(0.2, 1.0, 0.4)
|
||||
_grapple_label.text = "Grappling"
|
||||
else:
|
||||
_grapple_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_grapple_label.text = "Ready"
|
||||
|
||||
var dash_rem = sm.get_dash_cooldown_remaining()
|
||||
if dash_rem > 0.0:
|
||||
_dash_icon.modulate = Color(1.0, 0.3, 0.3)
|
||||
_dash_label.text = "%.1f" % dash_rem
|
||||
else:
|
||||
_dash_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_dash_label.text = "Ready"
|
||||
|
||||
if _weapon_label:
|
||||
var wman = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager")
|
||||
if wman and wman.weapons.has(wman.active_slot):
|
||||
var active_weapon = wman.weapons[wman.active_slot]
|
||||
var w_name = "Weapon"
|
||||
var cur_ammo = 0
|
||||
var max_ammo = 0
|
||||
|
||||
if "weapon_name" in active_weapon:
|
||||
w_name = active_weapon.weapon_name
|
||||
elif active_weapon is DoubleBarrelShotgun:
|
||||
w_name = "Double Barrel Shotgun"
|
||||
|
||||
if "current_ammo" in active_weapon:
|
||||
cur_ammo = active_weapon.current_ammo
|
||||
max_ammo = active_weapon.max_ammo
|
||||
elif "shells" in active_weapon:
|
||||
cur_ammo = active_weapon.shells
|
||||
max_ammo = 2
|
||||
|
||||
if "reloading" in active_weapon and active_weapon.reloading:
|
||||
_weapon_label.text = "%s\nReloading..." % w_name
|
||||
else:
|
||||
_weapon_label.text = "%s\n%d / %d" % [w_name, cur_ammo, max_ammo]
|
||||
else:
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
@@ -0,0 +1 @@
|
||||
uid://1oenqnfde4yw
|
||||
Reference in New Issue
Block a user