Files
Blockline/scripts/hud.gd
T

236 lines
12 KiB
GDScript
Raw Normal View History

2026-09-12 11:01:37 +02:00
class_name ArenaHUD
extends CanvasLayer
var game: Node3D
var canvas: Control
var font: SystemFont
var feed: Array = []
var shot_time := 0.0
var hit_time := 0.0
var head_hit := false
var hurt_time := 0.0
var hurt_origin := Vector3.ZERO
var elimination_time := 0.0
var elimination_name := ""
var notice_life := -1
static func damage_direction(origin: Vector3, player: Fighter) -> Vector2:
var local_offset := Basis(Vector3.UP, -player.yaw) * (origin - player.eye())
return Vector2(local_offset.x, local_offset.z).normalized()
const WHITE := Color("e7eee8")
const MINT := Color("a5dec8")
const ORANGE := Color("ef9b72")
func _ready() -> void:
layer = 5
font = SystemFont.new()
font.font_names = PackedStringArray(["Bahnschrift", "Segoe UI", "Arial"])
canvas = Control.new()
canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE
canvas.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(canvas)
canvas.draw.connect(render)
func _process(dt: float) -> void:
shot_time = maxf(0, shot_time - dt)
hit_time = maxf(0, hit_time - dt)
hurt_time = maxf(0, hurt_time - dt)
elimination_time = maxf(0, elimination_time - dt)
var p: Fighter = game.players.get(game.local_id)
if not game.active or p == null or p.life != notice_life:
hurt_time = 0
elimination_time = 0
canvas.visible = game.active
canvas.queue_redraw()
func text(pos: Vector2, value: String, size: int = 20, tint: Color = WHITE) -> void:
canvas.draw_string(font, pos + Vector2(1, 2), value, HORIZONTAL_ALIGNMENT_LEFT, -1, size, Color(0, 0, 0, 0.7))
canvas.draw_string(font, pos, value, HORIZONTAL_ALIGNMENT_LEFT, -1, size, tint)
func plate(rect: Rect2, color: Color = Color(0.025, 0.07, 0.1, 0.86)) -> void:
canvas.draw_rect(rect, color)
func render() -> void:
if not game.active:
return
var p: Fighter = game.players.get(game.local_id)
plate(Rect2(628, 28, 344, 88))
text(Vector2(657, 56), "RELAY", 14, MINT)
text(Vector2(878, 56), "EMBER", 14, ORANGE)
text(Vector2(660, 97), "%02d" % game.scores[0], 37, MINT)
text(Vector2(883, 97), "%02d" % game.scores[1], 37, ORANGE)
text(Vector2(755, 70), "%02d:%02d" % [int(game.time_left) / 60, int(game.time_left) % 60], 25)
text(Vector2(759, 96), "%s / %s" % [game.config.mode, game.config.score_limit], 13)
text(Vector2(40, 300), ("DUST 2" if game.arena.map_id == "dust2" else "RELAY DISTRICT"), 16, MINT)
text(Vector2(40, 324), "LOCAL OPERATIONS / 01", 12)
minimap(p)
player_labels(p)
if game.config.mode == "DOM":
for i in game.objectives.points.size():
var point: Dictionary = game.objectives.points[i]
var left: int = 614 + i * 124
var tint := MINT if point.owner == 0 else (ORANGE if point.owner == 1 else WHITE)
plate(Rect2(left, 126, 116, 48))
text(Vector2(left + 10, 151), ["A", "B", "C"][i] + " / " + (["RELAY", "EMBER"][point.owner] if point.owner >= 0 else "NEUTRAL"), 13, tint)
canvas.draw_rect(Rect2(left + 10, 161, 96 * clampf(point.capture / 4.0, 0, 1), 3), MINT if point.team == 0 else ORANGE)
elif game.config.mode == "KC":
text(Vector2(560, 149), "MARKEN SAMMELN: GEGNER BESTÄTIGEN / EIGENE VERWEIGERN", 14, MINT)
var row := 0
for notice in feed:
if notice.until > Time.get_ticks_msec():
text(Vector2(1060, 152 + row * 27), notice.text, 16)
row += 1
text(Vector2(1320, 38), "%s FPS · %s" % [Engine.get_frames_per_second(), "HOST" if game.is_host else "LAN"], 14, MINT)
if not p:
text(Vector2(640, 440), "SYNCHRONISIERE …", 30)
return
plate(Rect2(40, 778, 255, 84))
text(Vector2(60, 807), "RELAY" if p.team == 0 else "EMBER", 14, MINT if p.team == 0 else ORANGE)
text(Vector2(60, 846), "%03d" % ceili(p.hp), 35)
text(Vector2(139, 844), "HP", 17, MINT)
canvas.draw_rect(Rect2(176, 826, 95, 5), Color("405057"))
canvas.draw_rect(Rect2(176, 826, 95 * p.hp / 100, 5), MINT if p.hp > 30 else ORANGE)
plate(Rect2(1280, 761, 280, 101))
text(Vector2(1300, 790), Arsenal.DATA[p.weapon].name, 16, MINT)
text(Vector2(1300, 835), "%02d" % p.magazines[p.weapon], 43)
text(Vector2(1375, 833), "/ %03d" % p.reserves[p.weapon], 22)
text(Vector2(1430, 807), "%s × %s" % [ControlBindings.hint("flash"), p.flashes], 16, WHITE)
text(Vector2(1430, 837), "%s × %s" % [ControlBindings.hint("grenade"), p.grenades], 16, ORANGE)
text(Vector2(485, 866), "%s RELOAD / %s SWITCH / %s FRAG / %s FLASH / %s MELEE / %s SCORE" % [ControlBindings.hint("reload"), ControlBindings.hint("switch"), ControlBindings.hint("grenade"), ControlBindings.hint("flash"), ControlBindings.hint("melee"), ControlBindings.hint("scoreboard")], 13)
text(Vector2(42, 719), "%s / SCHILD %.1f s" % [ControlBindings.hint("shield"), p.shield_left] if p.shield_left > 0 else (ControlBindings.hint("shield") + " / FRONTSCHILD BEREIT" if p.shield_charges > 0 else "SCHILD VERBRAUCHT"), 17, Color("a7eaff"))
rewards(p)
text(Vector2(42, 750), "%s ELIM / %s DEATHS" % [p.kills, p.deaths], 17)
if p.hp > 0 and not game.match_over:
if p.rope_active:
text(Vector2(650, 620), "SEILAUFZUG / %s: LOSLASSEN" % ControlBindings.hint("jump"), 18, MINT)
elif not RopeLift.nearby(p).is_empty():
text(Vector2(665, 620), "%s / SEILAUFZUG BENUTZEN" % ControlBindings.hint("interact"), 18, MINT)
var center := Vector2(800, 450)
if hurt_time > 0:
var direction := damage_direction(hurt_origin, p)
if direction.length_squared() > 0.01:
var angle := direction.angle()
canvas.draw_arc(center, 95, angle - 0.32, angle + 0.32, 16, Color(1, 0.24, 0.12, hurt_time / 0.9), 6, true)
if elimination_time > 0:
text(Vector2(650, 580), "ELIMINIERT · " + elimination_name, 22, ORANGE)
if p.infinite_left <= 0 and p.magazines[p.weapon] <= maxi(1, int(p.weapon_stats().mag) / 4) and p.reload_left <= 0:
text(Vector2(707, 541), "%s · NACHLADEN" % ControlBindings.hint("reload"), 17, ORANGE)
if p.weapon == 3 and p.aim_blend > 0.92 and not ReflexAlignment.equipped(p):
canvas.draw_circle(center, 970, Color(0.01, 0.015, 0.018, 0.98), false, 1370, true)
canvas.draw_circle(center, 285, Color("252d30"), false, 8, true)
canvas.draw_line(center - Vector2(280, 0), center + Vector2(280, 0), Color.BLACK, 1.4)
canvas.draw_line(center - Vector2(0, 280), center + Vector2(0, 280), Color.BLACK, 1.4)
for i in range(-4, 5):
canvas.draw_line(center + Vector2(i * 35, -4), center + Vector2(i * 35, 4), Color.BLACK, 1)
canvas.draw_line(center + Vector2(-4, i * 35), center + Vector2(4, i * 35), Color.BLACK, 1)
canvas.draw_circle(center, 2, ORANGE)
if not p.aiming and p.aim_blend <= 0.01:
var gap := shot_time * 45.0 + lerpf(9.0 + p.velocity.length() * 0.6, 4.0, WeaponHandling.smooth(p.aim_blend))
for dir in [Vector2.UP, Vector2.DOWN, Vector2.LEFT, Vector2.RIGHT]:
canvas.draw_line(center + dir * gap, center + dir * (gap + 6), WHITE, 1.5, true)
if hit_time > 0:
for dir in [Vector2(1, 1), Vector2(-1, 1), Vector2(1, -1), Vector2(-1, -1)]:
canvas.draw_line(center + dir * 9, center + dir * 16, ORANGE if head_hit else WHITE, 2, true)
if p.reload_left > 0:
text(Vector2(730, 513), "RELOADING %.1f" % p.reload_left, 18, MINT)
if p.protection > 0:
text(Vector2(679, 715), "SPAWN PROTECTION %.1f" % p.protection, 17, MINT)
if p.hp < 35:
canvas.draw_rect(Rect2(0, 0, 1600, 900), Color(0.7, 0.08, 0.03, 0.6), false, 13)
if p.hp <= 0:
var killcam: bool = game.is_killcam_visible(p)
# Keep the killer visible above the plate during the shoulder view.
var offset := 265.0 if killcam else 0.0
plate(Rect2(520, 310 + offset, 560, 245))
if killcam:
text(Vector2(715, 294 + offset), "KILLCAM / REPLAY" if game.history.playing else "KILLCAM / LIVE", 18, MINT)
text(Vector2(698, 359 + offset), "KILLED BY", 22, ORANGE)
text(Vector2(600, 410 + offset), p.killer, 38)
text(Vector2(689, 453 + offset), p.killer_weapon, 21)
text(Vector2(654, 510 + offset), "RESPAWN IN %.1f" % maxf(0, p.respawn_left), 24, MINT)
if Input.is_action_pressed("scoreboard") or game.match_over:
scoreboard()
if p.hp > 0 and p.flash_left > 0:
canvas.draw_rect(Rect2(0, 0, 1600, 900), Color(0.94, 0.95, 0.93, clampf(p.flash_left / 2.0, 0, 0.97)))
text(Vector2(709, 530), "FLASHBANG", 19, Color("344444"))
if game.streaks.flash_left > 0:
canvas.draw_rect(Rect2(0, 0, 1600, 900), Color(1, 0.87, 0.62, minf(0.95, game.streaks.flash_left / 3)))
func rewards(p: Fighter) -> void:
plate(Rect2(40, 580, 340, 110))
text(Vector2(54, 605), "SERIE %s / 5 MUN · 10 UAV · 15 KIT · 20 NUKE" % p.streak, 13, MINT)
text(Vector2(54, 631), ControlBindings.hint("ultimate") + (" / CLAYMORE BEREIT" if p.ult_charge >= 4 else " / CLAYMORE %s / 4 KILLS" % p.ult_charge), 17, MINT)
canvas.draw_rect(Rect2(54, 642, 310, 5), Color("405057"))
canvas.draw_rect(Rect2(54, 642, 310 * p.ult_charge / 4.0, 5), MINT)
var status := ""
if p.infinite_left > 0: status += "ENDLOSMUNITION %.0fs " % ceilf(p.infinite_left)
if p.recon_left > 0: status += "UAV %.0fs" % ceilf(p.recon_left)
text(Vector2(54, 675), status, 14, ORANGE)
if game.streaks.nuke_left > 0:
plate(Rect2(530, 188, 540, 72), Color(0.4, 0.04, 0.015, 0.94))
text(Vector2(552, 220), "NUKE EINSCHLAG IN %.1f" % game.streaks.nuke_left, 25, ORANGE)
text(Vector2(552, 246), "AUSGELÖST VON " + game.streaks.nuke_name, 16)
func minimap(local: Fighter) -> void:
var origin := Vector2(40, 40)
plate(Rect2(origin, Vector2(225, 243)))
var scale_map: Vector2 = Vector2(208.0, 220.0) / game.arena.map_size
var offset := origin + Vector2(112, 128)
if game.arena.map_id == "dust2":
canvas.draw_texture_rect(preload("res://assets/scan/dust2/minimap.png"), Rect2(offset - Vector2(104, 110), Vector2(208, 220)), false)
for bounds: AABB in game.arena.obstacles:
if bounds.position.y > 2.5:
continue
var rect := Rect2(offset + Vector2(bounds.position.x, bounds.position.z) * scale_map, Vector2(bounds.size.x, bounds.size.z) * scale_map)
canvas.draw_rect(rect, Color("546866"))
for p: Fighter in game.players.values():
if p.hp <= 0:
continue
if p != local and (not local or (game.enemies(local, p) and p.radar_left <= 0 and local.recon_left <= 0)):
continue
var point := offset + Vector2(p.global_position.x, p.global_position.z) * scale_map
canvas.draw_circle(point, 4 if p == local else 3, WHITE if p == local else (ORANGE if game.enemies(local, p) else MINT))
if p == local:
if game.arena.map_id == "quarry" and p.global_position.y > 3.2:
text(origin + Vector2(12, 234), "DACH" if p.global_position.y > 7 else "1. OBERGESCHOSS", 11, MINT)
canvas.draw_line(point, point + Vector2(-sin(p.yaw), -cos(p.yaw)) * 12, WHITE, 2)
text(origin + Vector2(12, 18), "N ↑ " + ("DUST 2" if game.arena.map_id == "dust2" else "LEFT / MID / RIGHT"), 12)
func scoreboard() -> void:
plate(Rect2(360, 190, 880, 525), Color(0.025, 0.07, 0.1, 0.96))
text(Vector2(401, 237), game.winner if game.match_over else "MATCH / " + MapCatalog.title(game.arena.map_id), 31, MINT)
text(Vector2(402, 281), "OPERATOR", 16)
text(Vector2(924, 281), "ELIM DEATHS", 16)
var roster: Array = game.players.values()
roster.sort_custom(func(a, b): return a.kills > b.kills)
var y := 325
for p: Fighter in roster:
text(Vector2(402, y), (" " if p.peer_id == game.local_id else " ") + p.nickname, 21, MINT if p.team == 0 else ORANGE)
text(Vector2(946, y), "%02d %02d" % [p.kills, p.deaths], 21)
y += 38
if game.match_over:
text(Vector2(402, 691), "MATCH COMPLETE / %s → LEAVE MATCH → PLAY" % ControlBindings.hint("pause"), 16, MINT)
func player_labels(local: Fighter) -> void:
if local == null or local.hp <= 0 or game.history.playing: return
var camera: Camera3D = local.camera
for other: Fighter in game.players.values():
if other == local or other.hp <= 0: continue
var enemy: bool = game.enemies(local, other)
var point := other.eye() + Vector3.UP * 0.35
if camera.is_position_behind(point): continue
var distance := local.eye().distance_to(other.eye())
if enemy and (distance > 35 or not game.visible_between(local.eye(), other.eye(), [local.get_rid(), other.get_rid()])): continue
var at := camera.unproject_position(point)
if at.x < 10 or at.x > 1590 or at.y < 20 or at.y > 875: continue
var tint := ORANGE if enemy else Color("65dcff")
var label := ("▼ " if enemy else "◆ TEAM ") + other.nickname
var width := font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, 17).x
plate(Rect2(at + Vector2(-width / 2 - 6, -20), Vector2(width + 12, 27)), Color(0.015, 0.035, 0.05, 0.8))
text(at + Vector2(-width / 2, 0), label, 17, tint)
if not enemy:
canvas.draw_line(at + Vector2(-20, 10), at + Vector2(20, 10), Color("253f48"), 4)
canvas.draw_line(at + Vector2(-20, 10), at + Vector2(-20 + other.hp * 0.4, 10), tint, 4)