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>
83 lines
3.0 KiB
GDScript
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())
|