extends Control class_name AbilityChip ## A dash or grapple readout: a leaning chip that drains while the ability is on ## cooldown and snaps back to charged when it is ready again. ## ## It replaces a 32 px JPEG icon with a text label under it, tinted red or green ## by a colour multiply — an approach with three problems. The icon was a photo ## in a game drawn entirely in flat ink; the tint said "not ready" but not HOW not ## ready; and the number that did say it was rendered at 12 px, which is below ## what anyone reads mid-fight. ## ## What a player actually needs from a cooldown is one bit at a glance (can I go?) ## and one magnitude in peripheral vision (how soon?). So the chip answers the bit ## with COLOUR — volt when charged, ink when not, the same volt-means-now rule the ## rest of the UI runs on — and the magnitude with a WIPE across the chip, which ## can be read without focusing on it because it is a shape changing size rather ## than a number changing value. ## ## The ready transition overshoots slightly before settling. A cooldown that ends ## by silently going bright is easy to miss while looking somewhere else; one that ## pops is not, and it costs a tween. ## Ability name, drawn on the chip. var label: String = "DASH": set(v): label = v queue_redraw() ## 0 = fully charged, 1 = just used. Written each frame. var cooldown: float = 0.0: set(v): var c := clampf(v, 0.0, 1.0) var was_ready := cooldown <= 0.001 cooldown = c if was_ready and c > 0.001: _pop = 0.0 elif not was_ready and c <= 0.001: # Just came back. Fire the overshoot. _pop = 1.0 queue_redraw() ## Set while the ability is actively in use (mid-grapple), which is a third state ## and reads as neither charged nor recharging. var active: bool = false: set(v): if v != active: active = v queue_redraw() const SHEAR := 7.0 const INK_W := 3.0 const POP_DECAY := 4.0 var _pop: float = 0.0 func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE custom_minimum_size = Vector2(96, 34) func _process(delta: float) -> void: if _pop <= 0.0: return _pop = maxf(_pop - delta * POP_DECAY, 0.0) queue_redraw() func _draw() -> void: var w := size.x var h := size.y if w <= 1.0 or h <= 1.0: return var ready := cooldown <= 0.001 # The overshoot: a brief lift in the fill, decaying to the resting colour. var lift: float = _pop * _pop var fill: Color = UITheme.INK var text: Color = UITheme.PAPER_DIM if active: # In use. Cyan, so it cannot be confused with either of the other two. fill = UITheme.CYAN text = UITheme.ink_for(UITheme.CYAN) elif ready: fill = UITheme.VOLT.lerp(UITheme.PAPER, lift * 0.5) text = UITheme.ink_for(UITheme.VOLT) _shear(0.0, w, fill, h) # The recharge wipe, left to right over the dark chip. if not ready and not active: _shear(0.0, w * (1.0 - cooldown), UITheme.PAPAYA, h) # Anything the wipe has reached is papaya, anything it has not is ink, # and one text colour has to sit on both. Ink loses against ink; paper # wins against both, and the outline below covers the papaya case. text = UITheme.PAPER _shear_outline(0.0, w, UITheme.INK, h) var font := get_theme_default_font() if font == null: return var fs := 17 var text_size := font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, fs) var at := Vector2((w - text_size.x) * 0.5, (h + text_size.y * 0.62) * 0.5) # Ink outline under the glyphs, same as every Label in the theme, so the # label survives the wipe passing underneath it. draw_string_outline(font, at, label, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, 5, UITheme.INK) draw_string(font, at, label, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, text) func _shear(x0: float, x1: float, col: Color, h: float) -> void: if x1 - x0 < 0.5: return draw_colored_polygon(PackedVector2Array([ Vector2(x0 + SHEAR, 0.0), Vector2(x1 + SHEAR, 0.0), Vector2(x1, h), Vector2(x0, h), ]), col) func _shear_outline(x0: float, x1: float, col: Color, h: float) -> void: draw_polyline(PackedVector2Array([ Vector2(x0 + SHEAR, 0.0), Vector2(x1 + SHEAR, 0.0), Vector2(x1, h), Vector2(x0, h), Vector2(x0 + SHEAR, 0.0), ]), col, INK_W)