ShitboxInfinity/src/callout.gd
m3ultra 1753b4c0b0 SPOTTO: knock the fuel cap off a yellow one
Burnout's yellow warning diamonds are the game telling you it noticed,
and half of why it feels good. src/callout.gd builds them: diamond,
outlined text, punch-scale in (overshoot then snap, which reads as
IMPACT), stack of three with the older ones drifting up and out.
TAKEDOWN!, NEAR MISS, BIG AIR with the airborne time -- and the new
Australian one.

Roughly one traffic car in six is now painted yellow, deterministically
per car so junction convoys stay learnable puzzles, via a surface
OVERRIDE -- painting the shared fleet material would have turned every
copy of that model yellow across the whole city, the same trap the
garage's stat upgrades hit.

Sideswipe any car hard enough and its FUEL CAP pings off: a real
physics prop, chrome (or yellow), spat off the correct flank behind the
midline with sparks, tumbling into the road for ten seconds. Do it to a
yellow one and it's SPOTTO! -- 0.45 boost, cash or crash-score, a
running session tally, and a sting pitched up 35%. Merely near-missing
a yellow one at speed still earns the shout, because that IS the game
as played in the back of every Falcon on the Bruce Highway.

Sideness is judged in the victim's local frame before it goes dynamic
(|x| vs |z|), so a rear-ender is a shunt, not a sideswipe -- asserted,
along with one-cap-per-car and the cap prop actually existing.

Airtime rides along: car.gd tracks time with no wheel down and emits
landed() past 0.55 s, which pays boost and shouts BIG AIR.

Callout placement note: Control.position is relative to the PARENT, not
the anchor, so anchoring the row and then tweening position:y yanked
every badge to the top of the screen. Screen-space maths instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 13:51:09 +10:00

83 lines
3.0 KiB
GDScript

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())