class_name ImplantPending extends Control ## Diegetic "pending / working" indicator for the implant overlay (D-169). ## ## An animated bracket sweep — `[ >>>>········ ]` — with a caption above it. ## Linear left-to-right fill that loops; reads as a bounded machine process, ## not an OS spinner (Araminta's spec, #960). ## ## LAYER SEPARATION: this belongs to the IMPLANT (diegetic, in-fiction) UI layer ## only. It styles exclusively from ImplantTheme. The global / non-diegetic UI ## layer must NOT reuse these visuals — if that layer needs a busy indicator it ## gets its own component with its own look. Sharing the sweep *logic* is fine; ## resting on the same theme or glyphs is not. ## ## API: start(label) shows + animates; stop() hides + resets. Starts hidden, so ## at rest the player sees nothing. const SWEEP_PERIOD: float = 1.2 # seconds per left-to-right pass (linear, looping) const INTERIOR_WIDTH: float = 160.0 # px between the brackets const SWEEP_WIDTH: float = 28.0 # px width of the moving fill const DEFAULT_WIDTH: float = 220.0 var _label_text: String = "WORKING" var _sweep_t: float = 0.0 # normalized 0..1 sweep position var _theme: ImplantTheme = null var _font: Font = null func _ready() -> void: _font = ThemeDB.fallback_font custom_minimum_size = Vector2(DEFAULT_WIDTH, 40.0) mouse_filter = Control.MOUSE_FILTER_IGNORE visible = false set_process(false) ## Show the indicator and begin animating. `label` is uppercased for display. func start(label: String = "WORKING") -> void: _label_text = label.to_upper() _sweep_t = 0.0 visible = true set_process(true) queue_redraw() ## Hide the indicator and stop animating. func stop() -> void: visible = false set_process(false) _sweep_t = 0.0 func apply_implant_theme(t: ImplantTheme) -> void: _theme = t queue_redraw() func _process(delta: float) -> void: _sweep_t = fmod(_sweep_t + delta / SWEEP_PERIOD, 1.0) queue_redraw() func _draw() -> void: if _font == null: return var dim: Color = _theme.text_dim if _theme else Color("#667788") var accent: Color = _theme.accent_active if _theme else Color("#f0d060") accent.a = 0.6 var fs: int = _theme.font_body if _theme else 11 var line_h: float = float(_theme.line_height if _theme else 18) var w: float = size.x if size.x > 0.0 else DEFAULT_WIDTH # Caption — centered, dim. var cap_w: float = _font.get_string_size(_label_text, HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x draw_string( _font, Vector2((w - cap_w) * 0.5, float(fs)), _label_text, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, dim ) # Bracket row — centered group: [ ] var glyph_w: float = _font.get_string_size("[", HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x var group_w: float = glyph_w * 2.0 + INTERIOR_WIDTH var gx: float = (w - group_w) * 0.5 var baseline_y: float = line_h + float(fs) draw_string(_font, Vector2(gx, baseline_y), "[", HORIZONTAL_ALIGNMENT_LEFT, -1, fs, dim) draw_string( _font, Vector2(gx + glyph_w + INTERIOR_WIDTH, baseline_y), "]", HORIZONTAL_ALIGNMENT_LEFT, -1, fs, dim ) # Sweep fill — a thin bar travelling across the bracket interior. var interior_x: float = gx + glyph_w var travel: float = INTERIOR_WIDTH - SWEEP_WIDTH var sweep_x: float = interior_x + _sweep_t * travel var sweep_y: float = baseline_y - float(fs) * 0.35 draw_rect(Rect2(sweep_x, sweep_y, SWEEP_WIDTH, 1.0), accent)