Files
mmo/scenes/hud/hud.gd
2026-05-14 19:11:10 +02:00

510 lines
17 KiB
GDScript

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 _is_typing() -> bool:
var f := get_viewport().gui_get_focus_owner()
return f is LineEdit or f is TextEdit
func _unhandled_input(event: InputEvent) -> void:
if _is_typing():
if event.is_action_pressed("pause"):
var f := get_viewport().gui_get_focus_owner()
if f is LineEdit:
(f as LineEdit).release_focus()
_capture_ui(_any_panel_visible())
get_viewport().set_input_as_handled()
return
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