class_name Callout extends CanvasLayer ## Burnout 3's yellow warning-diamond callouts -- NUDGE! RUBBIN'! RIGHTSIDE ## SLIDE! -- which are the game telling you it noticed. Half of Burnout's feel ## is that nothing you do goes unremarked; the badge IS the reward. ## ## Ours are Australian: SPOTTO! when you knock the fuel cap off a yellow car. ## Stack of three, newest at the bottom, older ones drifting up and out. const HOLD := 1.05 const MAX_STACK := 3 var _stack: Array[Control] = [] func _init() -> void: layer = 2 # above the HUD; a callout that loses to the speedo is useless func say(text: String, gold := true) -> void: # Explicit screen-space placement, no anchors: Control.position is relative # to the PARENT, not to the anchor, so the stack tween below would yank an # anchored row to the top of the screen the moment it ran. var vp := Vector2(1600, 900) if get_viewport(): vp = get_viewport().get_visible_rect().size var base_y := vp.y * 0.44 var row := Control.new() row.position = Vector2(vp.x * 0.5, base_y) row.set_meta("base_y", base_y) row.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(row) # the diamond: a square on its corner, dark rim then bright fill for spec in [[34.0, Color(0.12, 0.10, 0.06)], [27.0, Color(1.0, 0.78, 0.10) if gold else Color(0.95, 0.35, 0.15)]]: var d := ColorRect.new() d.color = spec[1] d.size = Vector2(spec[0], spec[0]) d.pivot_offset = d.size * 0.5 d.position = Vector2(-d.size.x * 0.5 - 150.0, -d.size.y * 0.5) d.rotation = PI * 0.25 d.mouse_filter = Control.MOUSE_FILTER_IGNORE row.add_child(d) var bang := Label.new() bang.text = "!" bang.add_theme_font_size_override("font_size", 26) bang.add_theme_color_override("font_color", Color(0.15, 0.12, 0.05)) bang.position = Vector2(-156.0, -20.0) row.add_child(bang) var lbl := Label.new() lbl.text = text lbl.add_theme_font_size_override("font_size", 40) lbl.add_theme_color_override("font_color", Color(1.0, 0.86, 0.25) if gold else Color(1.0, 0.55, 0.25)) lbl.add_theme_color_override("font_outline_color", Color(0.08, 0.06, 0.03)) lbl.add_theme_constant_override("outline_size", 10) lbl.position = Vector2(-110.0, -28.0) row.add_child(lbl) # shove the older ones up the screen; drop anything past the stack limit _stack.append(row) while _stack.size() > MAX_STACK: var old: Control = _stack.pop_front() if is_instance_valid(old): old.queue_free() for i in _stack.size(): var c := _stack[i] if is_instance_valid(c): var lift: float = float(c.get_meta("base_y")) - 52.0 * (_stack.size() - 1 - i) create_tween().tween_property(c, "position:y", lift, 0.18) \ .set_trans(Tween.TRANS_CUBIC) # punch in: overshoot the scale, snap back. Cheap, and it reads as IMPACT. row.scale = Vector2(1.7, 0.6) var tw := create_tween() tw.tween_property(row, "scale", Vector2.ONE, 0.16).set_trans(Tween.TRANS_BACK) \ .set_ease(Tween.EASE_OUT) tw.tween_interval(HOLD) tw.tween_property(row, "modulate:a", 0.0, 0.35) tw.tween_callback(func() -> void: _stack.erase(row) row.queue_free())