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