Caribooze Simulator
Godot at-bat from the umpire’s view. Time the swing, put the ball in play, and take seven innings against the Phillies.
Source
Godot 4.7 project — open the source folder, or read the main script below.
extends Node2D
const GOLD := Color(0.886, 0.714, 0.341)
const US_NAME := "CARIBOOZE"
const THEM_NAME := "PHILLIES"
const HANG := 0.28
const SWING_DELAY := 0.18
const BALL_TEX: Texture2D = preload("res://assets/ball.png")
const BAT_TEX: Texture2D = preload("res://assets/bat.png")
const SFX := {
"crack": preload("res://assets/sfx/crack.wav"),
"crack_soft": preload("res://assets/sfx/crack_soft.wav"),
"pop": preload("res://assets/sfx/pop.wav"),
"whiff": preload("res://assets/sfx/whiff.wav"),
"cheer": preload("res://assets/sfx/cheer.wav"),
"cheer_big": preload("res://assets/sfx/cheer_big.wav"),
"cheer_small": preload("res://assets/sfx/cheer_small.wav"),
"groan": preload("res://assets/sfx/groan.wav"),
"call": preload("res://assets/sfx/call.wav"),
"click": preload("res://assets/sfx/click.wav"),
"crowd_loop": preload("res://assets/sfx/crowd_loop.wav"),
}
const PITCHES := [
{"name": "Fastball", "dur": 0.66, "mph": 96, "hx": 0.0, "hy": 0.0},
{"name": "Slider", "dur": 0.79, "mph": 88, "hx": 56.0, "hy": 14.0},
{"name": "Curveball", "dur": 0.96, "mph": 80, "hx": -12.0, "hy": 80.0},
{"name": "Changeup", "dur": 1.08, "mph": 76, "hx": 0.0, "hy": 24.0},
]
const ROSTER := [
"Raj", "Gallo", "Campbell", "Randle E6", "Tucker", "Soso", "Infinity", "France", "Reed", "MVP",
"Tuck's dad", "HRaldo", "PawPaw", "DRE", "Connor", "Farrar", "Luis", "Mark", "Pope", "Mack",
]
const AWAY_LINEUP := ["Schwarber", "Turner", "Harper", "Bohm", "Castellanos", "Realmuto", "Stott", "Marsh", "Sosa"]
const INNINGS := 7
const HOME_BASE := Vector2(640, 588)
const FIRST_BASE := Vector2(1020, 438)
const SECOND_BASE := Vector2(640, 288)
const THIRD_BASE := Vector2(260, 438)
const RUBBER := Vector2(640, 445.5)
const FIELDERS := [
{"name": "3B", "x": 318.0, "y": 438.0, "sx": 312.0, "sy": 366.0},
{"name": "SS", "x": 496.0, "y": 372.0, "sx": 470.0, "sy": 262.0},
{"name": "2B", "x": 784.0, "y": 372.0, "sx": 810.0, "sy": 262.0},
{"name": "1B", "x": 962.0, "y": 438.0, "sx": 986.0, "sy": 384.0},
{"name": "LF", "x": 390.0, "y": 214.0, "sx": 250.0, "sy": 204.0},
{"name": "CF", "x": 640.0, "y": 196.0, "sx": 640.0, "sy": 192.0},
{"name": "RF", "x": 890.0, "y": 214.0, "sx": 1030.0, "sy": 204.0},
]
var font: Font
var zone := Rect2(594, 528, 92, 120)
var start := Vector2(652, 412)
var target := Vector2(640, 512)
var hand_pos := Vector2(624, 280)
var batter_i := 0
var pending_advance := false
var meet := false
var meet_pos := Vector2.ZERO
var hit_from := Vector2.ZERO
var hit_to := Vector2.ZERO
var hit_arc := 40.0
var hit_flight := 0.7
var hit_bases := 1
var hit_radius := 18.0
var on_base: Array = [-1, -1, -1]
var base_power: Array = [false, false, false]
var movers: Array = []
var trot_span := 1
var ball_t := 0.0
var whiff_wind := 0.0
var resolving_whiff := false
var pitch: Dictionary = {}
var pitch_dur := 0.7
var pitch_mph := 0
var play_idx := -1
var cover_idx := -1
var throw_base := -1
var throw_from := Vector2.ZERO
var throw_to := Vector2.ZERO
var throw_dur := 0.4
var last_kind := -1
var phase := "windup"
var phase_t := 0.0
var live := true
var swung := false
var swinging := false
var swing_left := 0.0
var swing_p := 0.0
var power_mode := false
var _styled := -1
var hits := 0
var outs := 0
var inning := 1
var inning_runs := 0
var our_line: Array = []
var their_line: Array = []
var our_hits := 0
var their_hits := 0
var half_outs := 0
var half_runs := 0
var half_hits := 0
var half_wait := 0.0
var opp_on: Array = [false, false, false]
var pending_half := false
var lineup: Array = []
var lefty: Array = []
var stats: Array = []
var opp_batter_i := 0
var opp_active := false
var opp_t := 0.0
var opp_lefty := false
var opp_result: Dictionary = {}
var opp_applied := false
var opp_hit_to := Vector2.ZERO
var opp_hit_arc := 30.0
var opp_target := Vector2(640, 588)
var opp_swing_p := 0.0
var opp_swings := true
var balls := 0
var strikes := 0
var message := ""
var ball := Vector2(340, 360)
var contact_btn: Button
var power_btn: Button
var again_btn: Button
var start_btn: Button
var pop_done := false
var react_done := false
var opp_react_done := false
func _ready() -> void:
randomize()
_build_ui()
_shuffle_lineup()
_start_crowd()
live = false
phase = "ready"
print("READY Caribooze Simulator")
func _play(name: String, db := 0.0, pitch := 1.0) -> void:
var stream: AudioStream = SFX.get(name)
if stream == null:
return
var player := AudioStreamPlayer.new()
player.stream = stream
player.volume_db = db
player.pitch_scale = pitch
add_child(player)
player.finished.connect(player.queue_free)
player.play()
func _start_crowd() -> void:
var base: AudioStreamWAV = SFX["crowd_loop"]
var loop := base.duplicate() as AudioStreamWAV
loop.loop_mode = AudioStreamWAV.LOOP_FORWARD
loop.loop_begin = 0
loop.loop_end = int(loop.data.size() / 2)
var player := AudioStreamPlayer.new()
player.stream = loop
player.volume_db = -20.0
add_child(player)
player.play()
func _shuffle_lineup() -> void:
var pool: Array = ROSTER.duplicate()
pool.shuffle()
lineup = pool.slice(0, 9)
lefty = []
stats = []
for i in 9:
lefty.append(randf() < 0.35)
stats.append({"ab": 0, "h": 0, "bb": 0, "r": 0, "rbi": 0, "2b": 0, "3b": 0, "hr": 0, "k": 0})
func _stat(i: int, key: String, amount := 1) -> void:
if i < 0 or i >= stats.size():
return
var row: Dictionary = stats[i]
row[key] = int(row[key]) + amount
func _stat_line(i: int) -> String:
if i < 0 or i >= stats.size():
return ""
var s: Dictionary = stats[i]
var parts: Array[String] = []
if int(s["ab"]) > 0:
parts.append("%d/%d" % [int(s["h"]), int(s["ab"])])
for key in ["bb", "r", "rbi", "2b", "3b", "hr", "k"]:
var n := int(s[key])
if n <= 0:
continue
parts.append(key if n == 1 else "%d %s" % [n, key])
return ", ".join(parts)
func _new_game() -> void:
_shuffle_lineup()
batter_i = 0
opp_batter_i = 0
pending_advance = false
inning = 1
inning_runs = 0
our_line = []
their_line = []
our_hits = 0
their_hits = 0
hits = 0
outs = 0
strikes = 0
balls = 0
on_base = [-1, -1, -1]
base_power = [false, false, false]
movers = []
message = ""
_begin_pitch()
func _process(dt: float) -> void:
dt = minf(dt, 0.05)
phase_t += dt
if phase == "windup" and phase_t >= 0.42:
phase = "flight"
phase_t = 0.0
if swinging and live and (phase == "windup" or phase == "flight"):
swing_left = maxf(swing_left - dt, 0.0)
swing_p = clampf(1.0 - swing_left / SWING_DELAY, 0.0, 1.0)
if swing_left <= 0.0:
swinging = false
_resolve_swing()
elif swing_p > 0.0 and swing_p < 1.4:
swing_p = minf(swing_p + dt * 1.5, 1.4)
if not pop_done:
if (phase == "flight" and not swinging and phase_t >= pitch_dur) or (phase == "whiff" and whiff_wind <= 0.0 and ball_t >= pitch_dur):
pop_done = true
_play("pop", -2.0, randf_range(0.95, 1.05))
if phase == "hit" and not react_done and phase_t >= hit_flight:
react_done = true
_crowd_reacts()
if phase == "flight" and live and not swinging and phase_t >= pitch_dur + HANG:
_take()
elif phase == "hit" and phase_t >= _hit_len():
if outs >= 3:
_begin_opponent()
else:
_begin_pitch()
elif phase == "trot" and phase_t >= 0.2 + float(trot_span) * 0.74:
_begin_pitch()
elif phase == "whiff":
if whiff_wind > 0.0:
whiff_wind = maxf(whiff_wind - dt, 0.0)
else:
ball_t += dt
if whiff_wind <= 0.0 and ball_t >= pitch_dur + HANG:
phase_t = 0.0
if outs >= 3:
_begin_opponent()
else:
phase = "show"
elif phase == "show" and phase_t >= 1.15:
if pending_half or outs >= 3:
_begin_opponent()
else:
_begin_pitch()
elif phase == "opponent":
if opp_active:
_tick_opp_pa(dt)
else:
half_wait = maxf(half_wait - dt, 0.0)
if half_wait <= 0.0:
if half_outs >= 3:
_end_opponent_half()
else:
_start_opp_pa()
_apply_mode()
again_btn.visible = phase == "gameover" or phase == "final"
start_btn.visible = phase == "ready"
queue_redraw()
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
_try_swing()
elif event is InputEventKey:
var key := event as InputEventKey
if key.echo:
return
if key.keycode == KEY_SPACE and key.pressed:
_try_swing()
func _build_ui() -> void:
var layer := CanvasLayer.new()
add_child(layer)
contact_btn = Button.new()
contact_btn.text = "CONTACT"
contact_btn.position = Vector2(800, 16)
contact_btn.size = Vector2(200, 52)
contact_btn.focus_mode = Control.FOCUS_NONE
contact_btn.pressed.connect(_set_contact)
layer.add_child(contact_btn)
power_btn = Button.new()
power_btn.text = "POWER"
power_btn.position = Vector2(1020, 16)
power_btn.size = Vector2(220, 52)
power_btn.focus_mode = Control.FOCUS_NONE
power_btn.pressed.connect(_set_power)
layer.add_child(power_btn)
again_btn = Button.new()
again_btn.text = "NEXT INNING"
again_btn.position = Vector2(490, 640)
again_btn.size = Vector2(300, 60)
again_btn.visible = false
again_btn.focus_mode = Control.FOCUS_NONE
again_btn.pressed.connect(_advance_game)
layer.add_child(again_btn)
_paint(again_btn, true)
for b in [contact_btn, power_btn, again_btn]:
(b as Button).pressed.connect(_play.bind("click"))
start_btn = Button.new()
start_btn.text = "PLAY BALL"
start_btn.position = Vector2(490, 640)
start_btn.size = Vector2(300, 60)
start_btn.focus_mode = Control.FOCUS_NONE
start_btn.pressed.connect(_new_game)
start_btn.pressed.connect(_play.bind("click"))
layer.add_child(start_btn)
_paint(start_btn, true)
func _advance_game() -> void:
if phase == "final":
_new_game()
elif phase == "gameover":
_new_inning()
func _set_contact() -> void:
power_mode = false
func _set_power() -> void:
power_mode = true
func _apply_mode() -> void:
var mode := 1 if power_mode else 0
if _styled == mode:
return
_styled = mode
_paint(contact_btn, not power_mode)
_paint(power_btn, power_mode)
func _paint(button: Button, selected: bool) -> void:
var bg := Color(0.22, 0.17, 0.08) if selected else Color(0.12, 0.13, 0.15)
var border := GOLD if selected else Color(0.42, 0.44, 0.46)
button.add_theme_stylebox_override("normal", _box(bg, border))
button.add_theme_stylebox_override("hover", _box(bg.lightened(0.08), border))
button.add_theme_stylebox_override("pressed", _box(bg.lightened(0.12), GOLD))
button.add_theme_stylebox_override("focus", _box(bg, border))
button.add_theme_color_override("font_color", Color(0.96, 0.94, 0.9))
button.add_theme_color_override("font_hover_color", GOLD)
button.add_theme_font_size_override("font_size", 20)
func _box(bg: Color, border: Color) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = bg
style.border_color = border
style.set_border_width_all(2)
style.set_corner_radius_all(10)
style.content_margin_left = 12
style.content_margin_right = 12
style.content_margin_top = 8
style.content_margin_bottom = 8
return style
func _begin_pitch() -> void:
var index := randi() % PITCHES.size()
if index == last_kind:
index = (index + 1 + randi() % (PITCHES.size() - 1)) % PITCHES.size()
last_kind = index
pitch = PITCHES[index]
pitch_dur = float(pitch["dur"])
pitch_mph = int(pitch["mph"]) + randi_range(-1, 1)
if randf() < 0.64:
target = Vector2(
randf_range(zone.position.x + 6.0, zone.end.x - 6.0),
randf_range(zone.position.y + 8.0, zone.end.y - 8.0)
)
else:
var roll := randi() % 3
if roll == 0:
target = Vector2(randf_range(zone.position.x + 8.0, zone.end.x - 8.0), zone.position.y - randf_range(22.0, 52.0))
elif roll == 1:
target = Vector2(randf_range(zone.position.x + 8.0, zone.end.x - 8.0), zone.end.y + randf_range(18.0, 46.0))
else:
target = Vector2(zone.position.x - randf_range(28.0, 58.0), randf_range(zone.position.y + 12.0, zone.end.y - 12.0))
if pending_advance:
batter_i = (batter_i + 1) % lineup.size()
pending_advance = false
phase = "windup"
phase_t = 0.0
live = true
pop_done = false
react_done = false
swung = false
swinging = false
swing_left = 0.0
swing_p = 0.0
meet = false
message = ""
play_idx = -1
cover_idx = -1
throw_base = -1
func _try_swing() -> void:
if phase != "windup" and phase != "flight":
return
if swung or not live:
return
swung = true
swinging = true
swing_left = SWING_DELAY
swing_p = 0.0
func _resolve_swing() -> void:
if not live:
return
if phase == "windup":
_whiff(str(pitch["name"]) + " EARLY")
return
if phase != "flight":
return
var spot := _ball_pos(phase_t / pitch_dur)
var err := absf(phase_t - pitch_dur)
var in_zone := zone.has_point(target)
var good := 0.18
var edge := 0.25
if power_mode:
good = 0.12
edge = 0.17
var name := str(pitch["name"])
if not in_zone:
_whiff(name + " CHASE")
elif err <= edge and err > good:
_finish(name + " FOUL", false, false, true)
elif err <= good:
var kind := _batted(phase_t - pitch_dur, err, good)
if kind == "foul":
_finish(name + " FOUL", false, false, true)
elif _is_out_kind(kind):
_finish(name + " " + _play_label(kind), false, false, false, false, kind, spot, true)
else:
_finish(name + " " + _play_label(kind), true, false, false, false, kind, spot)
else:
_whiff(name + " MISS")
func _is_out_kind(kind: String) -> bool:
return kind == "groundout" or kind == "flyout" or kind == "lineout" or kind == "popup"
func _play_label(kind: String) -> String:
if kind == "home":
return "HOME RUN"
if kind == "triple":
return "TRIPLE"
if kind == "double":
return "DOUBLE"
if kind == "groundout":
return "GROUNDOUT"
if kind == "flyout":
return "FLY OUT"
if kind == "lineout":
return "LINE OUT"
if kind == "popup":
return "POP OUT"
return "SINGLE"
func _batted(signed: float, err: float, good: float) -> String:
var is_left: bool = lefty[batter_i] == true
var hand := -1.0 if is_left else 1.0
var spray := clampf(signed / good, -1.0, 1.0) * hand
var q := 1.0 - clampf(err / good, 0.0, 1.0)
var carry := pow(q, 0.85)
if absf(spray) > 0.94 and q < 0.28:
return "foul"
var ground_p := clampf(0.5 - carry * 0.32, 0.08, 0.58)
if power_mode:
ground_p = maxf(ground_p - 0.14, 0.05)
if randf() < ground_p:
return _ground_ball(spray, carry)
return _air_ball(spray, carry)
func _ground_ball(spray: float, carry: float) -> String:
spray = _aim_gap(spray, carry, [-0.7, 0.7])
var depth := lerpf(0.12, 0.36, carry)
var arc := lerpf(18.0, 8.0, carry)
var spot := _fair_spot(spray, depth)
var fielder := _nearest_fielder(spot)
var dist := _charge_dist(spot, fielder)
var reach := lerpf(96.0, 32.0, carry * carry)
if dist < reach:
_set_batted(spot, arc, depth, "groundout")
return "groundout"
if carry > 0.82 and dist > reach + 22.0 and randf() < 0.28:
return _place_extra(spray, carry, 26.0, false)
_set_batted(spot, arc, depth, "single")
return "single"
func _air_ball(spray: float, carry: float) -> String:
var liner := carry > 0.48 and randf() < lerpf(0.18, 0.62, carry)
spray = _aim_gap(spray, carry, [-0.64, -0.18, 0.18, 0.64])
var depth := lerpf(0.24, 0.55, carry)
var arc := lerpf(76.0, 46.0, carry)
if liner:
depth = lerpf(0.32, 0.62, carry)
arc = lerpf(32.0, 14.0, carry)
if carry > 0.76:
depth = lerpf(0.95, 0.99, carry)
arc = 40.0 if liner else lerpf(86.0, 104.0, carry)
if power_mode:
depth = minf(depth + 0.02, 0.99)
if not liner and carry > 0.91 and randf() < (0.48 if power_mode else 0.2):
depth = 1.65
arc = 128.0
elif power_mode:
depth = minf(depth + 0.06, 0.68)
if depth >= 1.0:
_set_batted(_fair_spot(spray, depth), arc, depth, "home")
return "home"
var spot := _fair_spot(spray, depth)
var fielder := _nearest_fielder(spot)
var dist := spot.distance_to(_home_spot(fielder))
var reach := lerpf(92.0, 28.0, carry * carry)
if liner:
reach *= lerpf(0.78, 0.48, carry)
if dist < reach:
var kind := "flyout"
if liner:
kind = "lineout"
elif carry < 0.38 and _infielder(fielder):
kind = "popup"
_set_batted(spot, arc, depth, kind)
return kind
if depth >= 0.9:
return _place_extra(spray, carry, arc, absf(spray) > 0.5)
_set_batted(spot, arc, depth, "single")
return "single"
func _place_extra(spray: float, carry: float, arc: float, corner: bool) -> String:
var kind := "double"
var depth := lerpf(0.95, 0.99, carry)
if corner and carry > 0.9 and randf() < 0.22:
kind = "triple"
depth = 1.0
arc = maxf(arc, 100.0)
var pull_side := 1.0 if spray >= 0.0 else -1.0
spray = pull_side * maxf(absf(spray), 0.8)
else:
arc = maxf(arc, 36.0)
_set_batted(_fair_spot(spray, depth), arc, depth, kind)
return kind
func _aim_gap(spray: float, carry: float, lanes: Array) -> float:
if carry < 0.32:
return spray
var best := float(lanes[0])
var best_d := 99.0
for lane in lanes:
var lane_d := absf(spray - float(lane))
if lane_d < best_d:
best_d = lane_d
best = float(lane)
return lerpf(spray, best, clampf((carry - 0.28) * 1.35, 0.0, 0.84))
func _set_batted(spot: Vector2, arc: float, depth: float, kind: String) -> void:
hit_to = spot
hit_arc = arc
hit_flight = lerpf(0.34, 0.58, clampf(depth, 0.0, 1.0))
if kind == "groundout":
hit_flight = lerpf(0.24, 0.34, clampf(depth, 0.0, 1.0))
elif kind == "double":
hit_flight = lerpf(0.8, 0.66, clampf((depth - 0.9) / 0.1, 0.0, 1.0))
elif kind == "triple":
hit_flight = 0.9
elif kind == "home":
hit_flight = 1.02
if kind == "home":
hit_bases = 4
elif kind == "triple":
hit_bases = 3
elif kind == "double":
hit_bases = 2
elif kind == "single":
hit_bases = 1
else:
hit_bases = 0
func _fair_spot(spray: float, depth: float) -> Vector2:
var y := lerpf(505.0, 172.0, clampf(depth, 0.0, 1.0))
if depth > 1.0:
y = lerpf(150.0, 76.0, clampf(depth - 1.0, 0.0, 1.0))
var dy := HOME_BASE.y - y
var half := maxf(dy, 0.0) * (380.0 / 150.0)
var x := 640.0 + clampf(spray, -1.05, 1.05) * half * 0.72
return Vector2(clampf(x, 72.0, 1208.0), y)
func _charge_dist(spot: Vector2, fielder: int) -> float:
return minf(spot.distance_to(_home_spot(fielder)), spot.distance_to(Vector2(640.0, 428.0)))
func _infielder(i: int) -> bool:
var name := str(FIELDERS[i]["name"])
return name == "1B" or name == "2B" or name == "SS" or name == "3B"
func _whiff(text: String) -> void:
resolving_whiff = true
_play("whiff", -2.0, randf_range(0.9, 1.1))
_finish(text, false, true)
func _take() -> void:
if zone.has_point(target):
_play("call", -4.0, randf_range(0.92, 1.08))
_finish(str(pitch["name"]) + " TAKEN", false, true)
else:
_finish(str(pitch["name"]) + " BALL", false, false, false, true)
func _crowd_reacts() -> void:
if throw_base == -1:
return
var is_out := hit_bases == 0
if is_out:
_play("groan", -6.0)
return
var scores := false
for m in movers:
if int(m["dest"]) >= 4:
scores = true
_play("cheer" if scores else "cheer_small", -4.0 if scores else -6.0)
func _finish(text: String, is_hit: bool, is_strike: bool, is_foul := false, is_ball := false, kind := "", spot := Vector2.ZERO, is_out := false) -> void:
if not live:
return
var clock := phase_t
var was_flight := phase == "flight"
live = false
var extra := ""
var pa_over := false
var walking := false
if is_foul:
_play("crack_soft", -6.0, randf_range(0.85, 1.0))
if is_hit:
hits += 1
strikes = 0
balls = 0
pa_over = true
_stat(batter_i, "ab")
_stat(batter_i, "h")
if kind == "double":
_stat(batter_i, "2b")
elif kind == "triple":
_stat(batter_i, "3b")
elif kind == "home":
_stat(batter_i, "hr")
_launch(kind, spot)
elif is_out:
outs += 1
strikes = 0
balls = 0
pa_over = true
_stat(batter_i, "ab")
_launch(kind, spot)
elif is_ball:
balls += 1
if balls >= 4:
extra = " WALK"
strikes = 0
balls = 0
pa_over = true
walking = true
_stat(batter_i, "bb")
_play("cheer_small", -8.0)
_send_runners(1, true)
elif is_foul:
if strikes < 2:
strikes += 1
elif is_strike:
strikes += 1
if strikes >= 3:
extra = " STRIKEOUT"
strikes = 0
balls = 0
outs += 1
pa_over = true
_stat(batter_i, "ab")
_stat(batter_i, "k")
_play("groan", -8.0)
if pa_over:
pending_advance = true
message = text + extra
phase_t = 0.0
if resolving_whiff:
phase = "whiff"
if was_flight:
ball_t = clock
whiff_wind = 0.0
else:
ball_t = 0.0
whiff_wind = maxf(0.42 - clock, 0.0)
if outs >= 3:
message = "INNING OVER"
elif is_hit or is_out:
phase = "hit"
elif outs >= 3:
pending_half = true
phase = "show"
message = "INNING OVER"
elif walking:
phase = "trot"
else:
phase = "show"
resolving_whiff = false
func _launch(kind: String, spot: Vector2) -> void:
meet = true
meet_pos = spot
hit_from = spot
hit_radius = lerpf(7.0, 24.0, clampf(phase_t / pitch_dur, 0.0, 1.0))
if kind == "home" or kind == "triple" or kind == "double" or kind == "lineout":
_play("crack", 0.0, randf_range(0.95, 1.05))
elif kind == "single":
_play("crack", -3.0, randf_range(0.95, 1.1))
else:
_play("crack_soft", -2.0, randf_range(0.9, 1.1))
if kind == "home":
_play("cheer_big", -2.0)
if kind == "groundout":
_force_groundout()
elif _is_out_kind(kind):
movers = []
trot_span = 1
_hold_runners()
else:
_send_runners(hit_bases, false)
_plan_throw(kind)
func _force_groundout() -> void:
var old: Array = [on_base[0], on_base[1], on_base[2]]
var old_pow: Array = [base_power[0] == true, base_power[1] == true, base_power[2] == true]
var nxt: Array = [-1, -1, -1]
var nxt_pow: Array = [false, false, false]
movers = []
trot_span = 1
if int(old[0]) != -1:
if int(old[1]) != -1 and int(old[2]) != -1:
_queue_runner(int(old[2]), 3, 4, old_pow[2])
elif int(old[2]) != -1:
_queue_runner(int(old[2]), 3, 3, old_pow[2])
nxt[2] = old[2]
nxt_pow[2] = old_pow[2]
if int(old[1]) != -1:
_queue_runner(int(old[1]), 2, 3, old_pow[1])
nxt[2] = old[1]
nxt_pow[2] = old_pow[1]
_queue_runner(int(old[0]), 1, 2, old_pow[0])
nxt[1] = old[0]
nxt_pow[1] = old_pow[0]
else:
for slot in [1, 2]:
if int(old[slot]) == -1:
continue
var bag: int = slot + 1
_queue_runner(int(old[slot]), bag, bag, old_pow[slot])
nxt[slot] = old[slot]
nxt_pow[slot] = old_pow[slot]
_queue_runner(batter_i, 0, 1, power_mode)
on_base = nxt
base_power = nxt_pow
func _hold_runners() -> void:
for slot in [0, 1, 2]:
if int(on_base[slot]) == -1:
continue
var bag: int = slot + 1
_queue_runner(int(on_base[slot]), bag, bag, base_power[slot] == true)
func _hit_len() -> float:
var trot := 0.16 + float(trot_span) * 0.74
if throw_base == -2:
return maxf(trot, hit_flight + 0.55)
if throw_base < 0:
return maxf(trot, hit_flight + 0.35)
return maxf(trot, hit_flight + 0.12 + throw_dur + 0.28)
func _plan_throw(kind: String) -> void:
play_idx = _nearest_fielder(hit_to)
if kind == "home":
throw_base = -1
elif kind == "flyout" or kind == "popup" or kind == "lineout":
throw_base = -2
elif kind == "groundout":
throw_base = 1
else:
throw_base = _correct_base(kind, play_idx)
if throw_base < 0:
cover_idx = -1
throw_dur = 0.0
if throw_base == -2:
throw_from = hit_to
return
cover_idx = _cover_fielder(throw_base, play_idx)
throw_from = _glove_at(_catch_spot(play_idx))
throw_to = HOME_BASE + Vector2(0, -8) if throw_base == 0 else _glove_at(_bag_spot(throw_base))
var natural := clampf(throw_from.distance_to(throw_to) / 720.0, 0.28, 0.9)
var eta := _play_eta(throw_base)
var release := hit_flight + 0.1
if kind == "groundout":
throw_dur = clampf(0.68 - (hit_flight + 0.08), 0.16, natural)
elif eta <= 0.0:
throw_dur = natural
else:
throw_dur = clampf(maxf(natural, eta + 0.12 - release), 0.28, 1.25)
func _correct_base(kind: String, fielder: int) -> int:
if kind == "home":
return -1
var lead_start := -1
var lead_dest := 1
for m in movers:
var start_b := int(m["start"])
if start_b >= lead_start:
lead_start = start_b
lead_dest = int(m["dest"])
if lead_dest >= 4:
return 0
var name := str(FIELDERS[fielder]["name"])
var infield := name == "1B" or name == "2B" or name == "SS" or name == "3B"
if infield and lead_dest == 1:
return 1
if not infield and lead_dest == 1:
return 2
return lead_dest
func _play_eta(base: int) -> float:
var gate := 4 if base == 0 else base
var eta := 0.0
var lead := -1
for m in movers:
var start_b := int(m["start"])
var dest_b := int(m["dest"])
if dest_b < gate or start_b >= gate:
continue
if start_b < lead:
continue
lead = start_b
eta = 0.12 + float(gate - start_b) * 0.74
return eta
func _nearest_fielder(spot: Vector2) -> int:
var best := 0
var best_d := 100000.0
for i in FIELDERS.size():
var d := spot.distance_squared_to(_home_spot(i))
if d < best_d:
best_d = d
best = i
return best
func _cover_fielder(base: int, fielder: int) -> int:
if base == 1:
return _fielder_named("1B", fielder, "2B")
if base == 3:
return _fielder_named("3B", fielder, "SS")
if base != 2:
return -1
var prefer := "SS" if hit_to.x >= 640.0 else "2B"
var other := "2B" if prefer == "SS" else "SS"
return _fielder_named(prefer, fielder, other)
func _fielder_named(want: String, busy: int, fallback: String) -> int:
var found := -1
var spare := -1
for i in FIELDERS.size():
var name := str(FIELDERS[i]["name"])
if name == want:
found = i
if name == fallback:
spare = i
if found >= 0 and found != busy:
return found
return spare
func _home_spot(i: int) -> Vector2:
return Vector2(float(FIELDERS[i]["x"]), float(FIELDERS[i]["y"]))
func _stand_spot(i: int) -> Vector2:
return Vector2(float(FIELDERS[i]["sx"]), float(FIELDERS[i]["sy"]))
func _fielder_scale(y: float) -> float:
return clampf(0.52 + (y - 180.0) / 620.0, 0.6, 0.9)
func _catch_spot(i: int) -> Vector2:
var home := _home_spot(i)
if throw_base == -2:
var sc := _fielder_scale(hit_to.y + 20.0)
return hit_to - Vector2(8, -26) * sc
if throw_base < 0:
return home.lerp(hit_to, 0.35)
var dir := home - hit_to
if dir.length() < 1.0:
dir = Vector2(0, 1)
return hit_to + dir.normalized() * 18.0
func _bag_spot(base: int) -> Vector2:
var bag := _base_at(base)
if base == 1:
return bag + Vector2(-30, 24)
if base == 3:
return bag + Vector2(30, 24)
return bag + Vector2(18, 26)
func _glove_at(feet: Vector2) -> Vector2:
return feet + Vector2(8, -26) * _fielder_scale(feet.y)
func _smooth(u: float) -> float:
return u * u * (3.0 - 2.0 * u)
func _fielder_spot(i: int) -> Vector2:
var home := _stand_spot(i)
if phase != "hit" or play_idx < 0:
return home
if i == play_idx:
return home.lerp(_catch_spot(i), _smooth(clampf(phase_t / maxf(hit_flight, 0.05), 0.0, 1.0)))
if i == cover_idx and throw_base > 0:
var u := _smooth(clampf((phase_t - 0.06) / maxf(hit_flight * 0.65, 0.2), 0.0, 1.0))
return home.lerp(_bag_spot(throw_base), u)
return home
func _new_inning() -> void:
inning += 1
inning_runs = 0
hits = 0
outs = 0
strikes = 0
balls = 0
on_base = [-1, -1, -1]
base_power = [false, false, false]
movers = []
message = ""
_begin_pitch()
func _begin_opponent() -> void:
if our_line.size() < inning:
our_line.append(inning_runs)
our_hits += hits
pending_half = false
opp_on = [false, false, false]
on_base = [-1, -1, -1]
base_power = [false, false, false]
movers = []
meet = false
swung = false
swinging = false
swing_left = 0.0
swing_p = 0.0
half_outs = 0
half_runs = 0
half_hits = 0
half_wait = 0.5
opp_active = false
opp_swing_p = 0.0
live = false
phase = "opponent"
phase_t = 0.0
message = THEM_NAME + " BATTING"
func _end_opponent_half() -> void:
if their_line.size() < inning:
their_line.append(half_runs)
their_hits += half_hits
phase_t = 0.0
opp_active = false
var us := _tally(our_line)
var them := _tally(their_line)
if inning >= INNINGS and us != them:
phase = "final"
var verdict := "WIN" if us > them else "LOSS"
message = "FINAL %s %d %s %d %s" % [US_NAME, us, THEM_NAME, them, verdict]
again_btn.text = "NEW GAME"
_play("cheer_big" if us > them else "groan", 0.0)
else:
phase = "gameover"
message = "END %d %s %d %s %d" % [inning, US_NAME, us, THEM_NAME, them]
again_btn.text = "NEXT INNING"
func _start_opp_pa() -> void:
var roll := randf()
var kind := "out"
if roll >= 0.985:
kind = "home"
elif roll >= 0.96:
kind = "triple"
elif roll >= 0.90:
kind = "double"
elif roll >= 0.72:
kind = "single"
elif roll >= 0.62:
kind = "walk"
elif roll >= 0.40:
kind = "strikeout"
var spray := randf_range(-0.9, 0.9)
var depth := 0.3
opp_hit_arc = 30.0
if kind == "out":
var fielder := randi() % FIELDERS.size()
opp_hit_to = _stand_spot(fielder)
opp_hit_arc = 18.0 if _infielder(fielder) else 70.0
elif kind == "single":
depth = randf_range(0.28, 0.55)
opp_hit_arc = randf_range(14.0, 40.0)
elif kind == "double":
depth = randf_range(0.9, 0.98)
spray = signf(spray) * randf_range(0.45, 0.85)
opp_hit_arc = 46.0
elif kind == "triple":
depth = 1.0
spray = signf(spray) * randf_range(0.8, 1.0)
opp_hit_arc = 40.0
elif kind == "home":
depth = 1.6
spray = randf_range(-0.7, 0.7)
opp_hit_arc = 90.0
if kind != "out":
opp_hit_to = _fair_spot(spray, depth)
opp_swings = kind != "walk"
opp_lefty = randf() < 0.4
opp_target = Vector2(randf_range(zone.position.x + 10.0, zone.end.x - 10.0), randf_range(zone.position.y + 12.0, zone.end.y - 12.0))
if kind == "walk":
opp_target.x = zone.position.x - 40.0 if opp_lefty else zone.end.x + 40.0
opp_result = {"kind": kind}
opp_applied = false
opp_t = 0.0
opp_swing_p = 0.0
opp_active = true
message = "%s %s" % [THEM_NAME, AWAY_LINEUP[opp_batter_i]]
func _tick_opp_pa(dt: float) -> void:
opp_t += dt
var contact := 0.5
if opp_swings:
opp_swing_p = clampf((opp_t - (contact - 0.16)) / 0.16, 0.0, 1.0)
if opp_swing_p >= 1.0:
opp_swing_p = minf(1.0 + (opp_t - contact) * 1.5, 1.4)
if not opp_applied and opp_t >= contact:
opp_applied = true
opp_react_done = false
var kind := str(opp_result.get("kind", "out"))
if kind == "walk" or kind == "strikeout":
_play("pop", -4.0)
if kind == "strikeout":
_play("call", -6.0)
else:
_play("crack" if kind != "out" else "crack_soft", -5.0, randf_range(0.95, 1.1))
_apply_opp_result()
if opp_applied and not opp_react_done and opp_t >= contact + 0.3:
opp_react_done = true
var kind := str(opp_result.get("kind", "out"))
if kind == "out" or kind == "strikeout":
_play("cheer_small", -8.0)
elif kind == "home":
_play("groan", -4.0)
elif kind != "walk":
_play("groan", -9.0)
if opp_t >= contact + 0.55:
opp_active = false
opp_batter_i = (opp_batter_i + 1) % AWAY_LINEUP.size()
half_wait = 0.12
func _apply_opp_result() -> void:
var before := half_runs
var kind := str(opp_result["kind"])
var who: String = AWAY_LINEUP[opp_batter_i]
var note := ""
if kind == "out" or kind == "strikeout":
half_outs += 1
note = ("STRIKEOUT" if kind == "strikeout" else "OUT") + " %d OUT" % half_outs
elif kind == "walk":
_opp_advance(1, true)
note = "WALK"
else:
half_hits += 1
if kind == "single":
_opp_advance(1, false)
note = "SINGLE"
elif kind == "double":
_opp_advance(2, false)
note = "DOUBLE"
elif kind == "triple":
_opp_advance(3, false)
note = "TRIPLE"
else:
_opp_advance(4, false)
note = "HOME RUN"
var scored := half_runs - before
message = "%s %s %s" % [THEM_NAME, who, note]
if scored == 1:
message += " RUN SCORES"
elif scored > 1:
message += " %d RUNS" % scored
func _opp_advance(bases_taken: int, force_only: bool) -> void:
var old: Array = [opp_on[0], opp_on[1], opp_on[2]]
var nxt: Array = [false, false, false]
if force_only:
if old[0]:
if old[1]:
if old[2]:
half_runs += 1
nxt[2] = old[1]
else:
nxt[2] = old[2]
nxt[1] = old[0]
else:
nxt[1] = old[1]
nxt[2] = old[2]
nxt[0] = true
else:
for slot in [2, 1, 0]:
if not old[slot]:
continue
var dest: int = slot + 1 + bases_taken
if dest >= 4:
half_runs += 1
else:
nxt[dest - 1] = true
if bases_taken >= 4:
half_runs += 1
else:
nxt[bases_taken - 1] = true
opp_on = nxt
func _tally(line: Array) -> int:
var sum := 0
for v in line:
sum += int(v)
return sum
func _send_runners(bases_taken: int, force_only: bool) -> void:
var old: Array = [on_base[0], on_base[1], on_base[2]]
var old_pow: Array = [base_power[0] == true, base_power[1] == true, base_power[2] == true]
var nxt: Array = [-1, -1, -1]
var nxt_pow: Array = [false, false, false]
movers = []
trot_span = 1
if force_only:
if int(old[0]) != -1:
if int(old[1]) != -1:
if int(old[2]) != -1:
_queue_runner(int(old[2]), 3, 4, old_pow[2])
_queue_runner(int(old[1]), 2, 3, old_pow[1])
nxt[2] = old[1]
nxt_pow[2] = old_pow[1]
else:
nxt[2] = old[2]
nxt_pow[2] = old_pow[2]
_queue_runner(int(old[0]), 1, 2, old_pow[0])
nxt[1] = old[0]
nxt_pow[1] = old_pow[0]
else:
nxt[1] = old[1]
nxt_pow[1] = old_pow[1]
nxt[2] = old[2]
nxt_pow[2] = old_pow[2]
_queue_runner(batter_i, 0, 1, power_mode)
nxt[0] = batter_i
nxt_pow[0] = power_mode
else:
for slot in [2, 1, 0]:
if int(old[slot]) == -1:
continue
var start_b: int = slot + 1
var dest_b := mini(start_b + bases_taken, 4)
_queue_runner(int(old[slot]), start_b, dest_b, old_pow[slot])
if dest_b <= 3:
nxt[dest_b - 1] = old[slot]
nxt_pow[dest_b - 1] = old_pow[slot]
var batter_dest := mini(bases_taken, 4)
_queue_runner(batter_i, 0, batter_dest, power_mode)
if batter_dest <= 3:
nxt[batter_dest - 1] = batter_i
nxt_pow[batter_dest - 1] = power_mode
on_base = nxt
base_power = nxt_pow
func _queue_runner(who: int, start_b: int, dest_b: int, powered: bool) -> void:
movers.append({"id": who, "start": start_b, "dest": dest_b, "power": powered})
trot_span = maxi(trot_span, dest_b - start_b)
if dest_b >= 4 and dest_b > start_b:
inning_runs += 1
_stat(who, "r")
_stat(batter_i, "rbi")
func _ball_pos(t: float) -> Vector2:
var u := clampf(t, 0.0, 1.0)
var point := start.lerp(target, u)
var hump := sin(u * PI)
point.x += float(pitch["hx"]) * hump
point.y += float(pitch["hy"]) * hump
return point
func _draw() -> void:
if font == null:
font = ThemeDB.fallback_font
var home := HOME_BASE
var first := FIRST_BASE
var second := SECOND_BASE
var third := THIRD_BASE
_draw_sky()
_draw_stands()
_draw_outfield()
var dirt := Color(0.64, 0.46, 0.28)
var skin := _infield_skin()
draw_colored_polygon(skin, dirt)
draw_polyline(skin, dirt.darkened(0.18), 2.0, true)
draw_colored_polygon(PackedVector2Array([
_skirt(third, home, first, -26.0),
_skirt(home, first, second, -20.0),
_skirt(first, second, third, -16.0),
_skirt(second, third, home, -20.0),
]), Color(0.16, 0.46, 0.20))
_fill_ellipse(home + Vector2(0, 30), 232, 70, dirt)
_fill_ellipse(RUBBER + Vector2(0, 3), 56, 20, dirt.darkened(0.08))
_fill_ellipse(RUBBER, 30, 10, Color(0.58, 0.41, 0.24))
draw_rect(Rect2(RUBBER.x - 14, RUBBER.y - 3, 28, 5), Color(0.96, 0.96, 0.97))
var chalk := Color(0.96, 0.97, 0.95)
var apex := home + Vector2(0, 8)
var box_top := 556.0
var exit_l := apex + (third - apex) * ((apex.y - box_top) / (apex.y - third.y))
var exit_r := apex + (first - apex) * ((apex.y - box_top) / (apex.y - first.y))
draw_line(_ray(home, third, 0.0), exit_l, chalk, 2.5, true)
draw_line(_ray(home, first, 1280.0), exit_r, chalk, 2.5, true)
_chalk_box(_batter_box(-1.0))
_chalk_box(_batter_box(1.0))
_bag(second, first, third, 11.0)
_bag(first, home, second, 15.0)
_bag(third, second, home, 15.0)
_home_plate(home)
_draw_fielders(true)
_draw_pitcher()
_draw_fielders(false)
if phase != "trot" and (phase != "hit" or swing_p < 1.28):
_draw_batter()
_draw_baserunners()
_draw_catcher()
var hot := phase == "flight" and absf(phase_t - pitch_dur) < 0.1
_draw_zone(GOLD if hot else Color(0.96, 0.97, 0.95))
_draw_ball()
_draw_hud()
func _ray(a: Vector2, b: Vector2, edge_x: float) -> Vector2:
var dir := b - a
if absf(dir.x) < 0.001:
return b
var t := (edge_x - a.x) / dir.x
return a + dir * t
func _skirt(prev: Vector2, corner: Vector2, nxt: Vector2, dist: float) -> Vector2:
var a := prev - corner
var b := nxt - corner
if a.length() < 0.001 or b.length() < 0.001:
return corner
var bis := a.normalized() + b.normalized()
if bis.length() < 0.01:
bis = Vector2(-(nxt - prev).y, (nxt - prev).x)
bis = bis.normalized()
if bis.dot(corner - (HOME_BASE + SECOND_BASE) * 0.5) < 0.0:
bis = -bis
return corner + bis * dist
func _stroke(a: Vector2, b: Vector2, color: Color, width: float, overshoot: float) -> void:
var dir := b - a
var len := dir.length()
if len < 0.001:
return
dir /= len
draw_line(a - dir * overshoot, b + dir * overshoot, color, width, true)
func _field_to_screen(fx: float, fy: float) -> Vector2:
return Vector2(640.0 + fx * (380.0 / 63.64), HOME_BASE.y - fy * (150.0 / 63.64))
func _infield_skin() -> PackedVector2Array:
var pts := PackedVector2Array()
pts.append(_field_to_screen(-3.0, -3.0))
for i in 38:
var ang := deg_to_rad(lerpf(-77.0, 77.0, float(i) / 37.0))
pts.append(_field_to_screen(85.0 * sin(ang), 60.5 + 85.0 * cos(ang)))
pts.append(_field_to_screen(3.0, -3.0))
return pts
func _batter_box(side: float) -> PackedVector2Array:
var inner := 640.0 + 54.0 * side
var outer := 640.0 + 200.0 * side
var top := 556.0
var bottom := 652.0
return PackedVector2Array([
Vector2(inner, top),
Vector2(outer, top),
Vector2(outer, bottom),
Vector2(inner, bottom),
])
func _fill_ellipse(center: Vector2, rx: float, ry: float, color: Color) -> void:
var pts := PackedVector2Array()
for i in 28:
var ang := TAU * float(i) / 28.0
pts.append(center + Vector2(cos(ang) * rx, sin(ang) * ry))
draw_colored_polygon(pts, color)
func _bag(at: Vector2, prev: Vector2, nxt: Vector2, size: float) -> void:
var a := (prev - at).normalized() * size
var b := (nxt - at).normalized() * size
var center := at - (a + b) * 0.5
var top := PackedVector2Array([center + (a + b) * 0.5, center + (a - b) * 0.5, center - (a + b) * 0.5, center + (b - a) * 0.5])
var lift := Vector2(0, 3)
var lowest := 0
for i in top.size():
if top[i].y > top[lowest].y:
lowest = i
var l := top[(lowest + 3) % 4]
var r := top[(lowest + 1) % 4]
var bottom := top[lowest]
draw_colored_polygon(PackedVector2Array([l, bottom, bottom + lift, l + lift]), Color(0.74, 0.75, 0.78))
draw_colored_polygon(PackedVector2Array([bottom, r, r + lift, bottom + lift]), Color(0.66, 0.67, 0.71))
draw_colored_polygon(top, Color(0.97, 0.97, 0.98))
var edge := top.duplicate()
edge.append(top[0])
draw_polyline(edge, Color(0.7, 0.71, 0.75), 1.2, true)
func _home_plate(at: Vector2) -> void:
var plate := PackedVector2Array([
at + Vector2(-20, -8),
at + Vector2(20, -8),
at + Vector2(20, 8),
at + Vector2(0, 24),
at + Vector2(-20, 8),
])
draw_colored_polygon(plate, Color(0.97, 0.97, 0.96))
var ink := Color(0.15, 0.13, 0.12)
for i in plate.size():
_stroke(plate[i], plate[(i + 1) % plate.size()], ink, 2.0, 1.0)
func _chalk_box(pts: PackedVector2Array) -> void:
var closed := pts.duplicate()
closed.append(pts[0])
draw_polyline(closed, Color(0.96, 0.97, 0.95), 2.5, true)
func _draw_sky() -> void:
draw_polygon(PackedVector2Array([
Vector2(0, 0), Vector2(1280, 0), Vector2(1280, 150), Vector2(0, 150),
]), PackedColorArray([
Color(0.33, 0.58, 0.90),
Color(0.33, 0.58, 0.90),
Color(0.68, 0.85, 0.97),
Color(0.68, 0.85, 0.97),
]))
func _draw_outfield() -> void:
draw_rect(Rect2(0, 166, 1280, 554), Color(0.15, 0.46, 0.19))
for i in 11:
var t0 := float(i) / 11.0
var t1 := float(i + 1) / 11.0
var y0 := lerpf(168.0, 720.0, pow(t0, 1.4))
var y1 := lerpf(168.0, 720.0, pow(t1, 1.4))
if i % 2 == 0:
continue
var shade := Color(0.10, 0.34, 0.14).lerp(Color(0.22, 0.58, 0.26), t0)
draw_rect(Rect2(0, minf(y0, y1), 1280, maxf(absf(y1 - y0), 1.0)), shade)
draw_rect(Rect2(0, 150, 1280, 18), Color(0.62, 0.49, 0.31))
draw_rect(Rect2(0, 150, 1280, 3), Color(0.78, 0.64, 0.42))
func _draw_stands() -> void:
draw_rect(Rect2(0, 0, 1280, 34), Color(0.07, 0.09, 0.12))
draw_rect(Rect2(0, 34, 1280, 84), Color(0.13, 0.15, 0.20))
var shirts: Array[Color] = [
Color(0.86, 0.22, 0.18), Color(0.95, 0.78, 0.28), Color(0.22, 0.42, 0.86),
Color(0.93, 0.94, 0.96), Color(0.16, 0.52, 0.36),
]
var skins: Array[Color] = [Color(0.93, 0.76, 0.58), Color(0.62, 0.42, 0.28), Color(0.36, 0.24, 0.16)]
var bob := Time.get_ticks_msec() * 0.004
for row in 3:
var count := 30 - row * 2
var gap := 1280.0 / float(count)
var y := 48.0 + float(row) * 20.0
var rad := 4.4 - float(row) * 0.4
for i in count:
var at := Vector2(gap * (float(i) + 0.5), y + sin(bob + float(i + row * 5)) * 1.1)
var shirt: Color = shirts[(i + row * 2) % shirts.size()]
draw_circle(at + Vector2(0, rad + 2.0), rad + 1.4, shirt)
var skin: Color = skins[(i + row) % skins.size()]
draw_circle(at, rad, skin)
draw_circle(at + Vector2(0, -1.4), rad + 0.5, shirt.darkened(0.42))
draw_rect(Rect2(0, 112, 1280, 38), Color(0.07, 0.32, 0.16))
draw_rect(Rect2(0, 112, 1280, 5), Color(0.95, 0.82, 0.34))
for i in 9:
draw_line(Vector2(float(i) * 142.0, 117), Vector2(float(i) * 142.0, 150), Color(0, 0, 0, 0.18), 2.0)
func _draw_fielders(behind: bool) -> void:
var order: Array[int] = []
for i in FIELDERS.size():
order.append(i)
order.sort_custom(func(a: int, b: int) -> bool: return _fielder_spot(a).y < _fielder_spot(b).y)
for n in order:
var i := int(n)
var at := _fielder_spot(i)
if (at.y < 300.0) != behind:
continue
var running := phase == "hit" and phase_t < hit_flight and (i == play_idx or i == cover_idx)
var aim := Vector2.ZERO
if phase == "hit" and i == play_idx and throw_base >= 0 and phase_t >= hit_flight and phase_t < hit_flight + 0.1 + throw_dur * 0.45:
aim = throw_to
var name := str(FIELDERS[i]["name"])
var infield := name != "LF" and name != "CF" and name != "RF"
_paint_fielder(at, running, aim, infield)
func _kit(home: bool) -> Dictionary:
if home:
return {
"jersey": Color(0.08, 0.08, 0.1),
"pants": Color(0.95, 0.78, 0.16),
"cap": Color(0.08, 0.08, 0.1),
"brim": Color(0.95, 0.78, 0.16),
"trim": Color(0.95, 0.78, 0.16),
"helmet": Color(0.08, 0.08, 0.1),
}
return {
"jersey": Color(0.96, 0.96, 0.97),
"pants": Color(0.92, 0.92, 0.94),
"cap": Color(0.72, 0.1, 0.14),
"brim": Color(0.72, 0.1, 0.14),
"trim": Color(0.72, 0.1, 0.14),
"helmet": Color(0.72, 0.1, 0.14),
}
func _fielding_kit() -> Dictionary:
return _kit(phase == "opponent")
func _batting_kit() -> Dictionary:
return _kit(phase != "opponent")
func _torso(hip: Vector2, shoulder: Vector2, hip_w: float, shoulder_w: float, kit: Dictionary, outline_w: float) -> void:
var pts := PackedVector2Array([
hip + Vector2(-hip_w, 0),
hip + Vector2(hip_w, 0),
shoulder + Vector2(shoulder_w, 0),
shoulder + Vector2(-shoulder_w, 0),
])
draw_colored_polygon(pts, kit["jersey"])
var closed := pts.duplicate()
closed.append(pts[0])
draw_polyline(closed, Color(0.08, 0.07, 0.1), outline_w, true)
draw_line(hip + Vector2(-hip_w, 0), hip + Vector2(hip_w, 0), kit["pants"], outline_w * 1.4, true)
func _head_front(center: Vector2, r: float, kit: Dictionary) -> void:
var skin := Color(0.93, 0.76, 0.58)
var ink := Color(0.08, 0.07, 0.1)
draw_circle(center, r, skin)
var cap := PackedVector2Array()
for i in 13:
var ang := PI + PI * float(i) / 12.0
cap.append(center + Vector2(0, -0.08 * r) + Vector2(cos(ang), sin(ang)) * r * 1.04)
draw_colored_polygon(cap, kit["cap"])
_fill_ellipse(center + Vector2(0, -0.04 * r), r * 1.2, r * 0.26, kit["brim"])
draw_circle(center + Vector2(-0.34 * r, 0.32 * r), maxf(r * 0.13, 0.9), ink)
draw_circle(center + Vector2(0.34 * r, 0.32 * r), maxf(r * 0.13, 0.9), ink)
func _head_back(center: Vector2, r: float, kit: Dictionary, side: float) -> void:
var skin := Color(0.93, 0.76, 0.58)
var shell: Color = kit["helmet"]
var ink := Color(0.08, 0.07, 0.1)
draw_circle(center + Vector2(0.3 * r * side, 0.2 * r), r * 0.78, skin)
draw_circle(center + Vector2(-0.08 * r * side, -0.04 * r), r, shell)
draw_arc(center + Vector2(-0.08 * r * side, -0.04 * r), r, 0, TAU, 18, ink, 1.6, true)
draw_circle(center + Vector2(0.62 * r * side, 0.42 * r), r * 0.4, shell)
draw_line(center + Vector2(0.45 * r * side, -0.5 * r), center + Vector2(1.2 * r * side, -0.12 * r), kit["brim"], r * 0.22, true)
func _head_side(center: Vector2, r: float, kit: Dictionary, look: float) -> void:
var skin := Color(0.93, 0.76, 0.58)
var shell: Color = kit["helmet"]
var ink := Color(0.08, 0.07, 0.1)
draw_circle(center + Vector2(0.22 * r * look, 0.18 * r), r * 0.82, skin)
draw_circle(center + Vector2(-0.12 * r * look, -0.1 * r), r, shell)
draw_arc(center + Vector2(-0.12 * r * look, -0.1 * r), r, 0, TAU, 16, ink, 1.4, true)
draw_circle(center + Vector2(-0.55 * r * look, 0.42 * r), r * 0.38, shell)
draw_line(center + Vector2(0.35 * r * look, -0.28 * r), center + Vector2(1.3 * r * look, 0.02 * r), kit["brim"], r * 0.22, true)
draw_circle(center + Vector2(0.62 * r * look, 0.22 * r), maxf(r * 0.12, 0.9), ink)
func _paint_fielder(feet: Vector2, running: bool, aim: Vector2, infield: bool) -> void:
var sc := _fielder_scale(feet.y)
var kit := _fielding_kit()
var stride := sin(Time.get_ticks_msec() * 0.016) * 8.0 * sc if running else 0.0
var set := infield and not running
var knee := 6.0 * sc if set else 0.0
var spread := (9.0 if set else 5.5) * sc
var skin := Color(0.93, 0.76, 0.58)
var pants: Color = kit["pants"]
var hip := feet + Vector2(0, -20.0 * sc + knee)
var shoulder := hip + Vector2(0, -15.0 * sc)
_fill_ellipse(feet + Vector2(0, 2), 12 * sc, 4.5 * sc, Color(0, 0, 0, 0.24))
draw_line(hip + Vector2(-4, 0) * sc, feet + Vector2(-spread - stride, 0), pants, maxf(4.4 * sc, 2.0), true)
draw_line(hip + Vector2(4, 0) * sc, feet + Vector2(spread + stride, 0), pants, maxf(4.4 * sc, 2.0), true)
_torso(hip, shoulder, 6.0 * sc, 8.5 * sc, kit, maxf(1.2 * sc, 1.0))
var throw_sh := shoulder + Vector2(7, 1) * sc
var hand := throw_sh + Vector2(5, 12) * sc
if aim != Vector2.ZERO:
var dir := aim - throw_sh
if dir.length() > 1.0:
hand = throw_sh + dir.normalized() * 15.0 * sc
draw_line(throw_sh, hand, skin, maxf(2.6 * sc, 1.6), true)
draw_circle(hand, maxf(2.0 * sc, 1.4), skin)
var glove_sh := shoulder + Vector2(-7, 1) * sc
var glove := hip + (Vector2(-6, 9) * sc if set else Vector2(-9, 4) * sc)
draw_line(glove_sh, glove, skin, maxf(2.6 * sc, 1.6), true)
_glove(glove, 6.5 * sc, 1.0, 0.25 if set else -0.15, true, false)
var head := shoulder + Vector2(0, -8.5 * sc)
draw_line(shoulder, head, skin, maxf(3.0 * sc, 1.6), true)
_head_front(head, 6.2 * sc, kit)
func _draw_pitcher() -> void:
var origin := RUBBER + Vector2(0, -28)
var sc := 0.8
var kit := _fielding_kit()
var arm := -0.4
if phase == "windup":
arm = lerpf(-0.35, -1.7, clampf(phase_t / 0.42, 0.0, 1.0))
elif phase == "flight":
arm = lerpf(1.2, 0.45, clampf(phase_t / pitch_dur, 0.0, 1.0))
elif phase == "whiff":
if whiff_wind > 0.0:
arm = lerpf(-0.35, -1.7, clampf((0.42 - whiff_wind) / 0.42, 0.0, 1.0))
else:
arm = lerpf(1.2, 0.45, clampf(ball_t / pitch_dur, 0.0, 1.0))
elif phase == "opponent" and opp_active:
if opp_t < 0.2:
arm = lerpf(-0.35, -1.7, clampf(opp_t / 0.2, 0.0, 1.0))
else:
arm = lerpf(1.2, 0.45, clampf((opp_t - 0.2) / 0.3, 0.0, 1.0))
var skin := Color(0.93, 0.76, 0.58)
var pants: Color = kit["pants"]
var hip := origin + Vector2(0, 8) * sc
var shoulder := origin + Vector2(0, -13) * sc
var throw_sh := shoulder + Vector2(-10, 2) * sc
hand_pos = throw_sh + Vector2(cos(arm), sin(arm)) * 34.0 * sc
_fill_ellipse(origin + Vector2(0, 38) * sc, 18 * sc, 6 * sc, Color(0, 0, 0, 0.28))
draw_line(hip + Vector2(-5, 0) * sc, origin + Vector2(-12, 34) * sc, pants, 7.0, true)
draw_line(hip + Vector2(5, 0) * sc, origin + Vector2(13, 34) * sc, pants, 7.0, true)
draw_line(origin + Vector2(-12, 34) * sc, origin + Vector2(-4, 36) * sc, Color(0.12, 0.1, 0.1), 3.5, true)
draw_line(origin + Vector2(13, 34) * sc, origin + Vector2(21, 35) * sc, Color(0.12, 0.1, 0.1), 3.5, true)
_torso(hip, shoulder, 9.0 * sc, 12.5 * sc, kit, 1.6)
draw_line(throw_sh, hand_pos, skin, 5.0, true)
draw_circle(hand_pos, 4.0, skin)
var glove := origin + Vector2(17, 6) * sc
draw_line(shoulder + Vector2(10, 2) * sc, glove, skin, 5.0, true)
_glove(glove, 8.0, -1.0, 0.2, true, false)
var head := shoulder + Vector2(0, -13) * sc
draw_line(shoulder, head, skin, 5.0, true)
_head_front(head, 9.5 * sc, kit)
func _draw_zone(frame: Color) -> void:
var ink := Color(0.1, 0.08, 0.12)
var arm := 14.0
var corners: Array[Vector2] = [zone.position, Vector2(zone.end.x, zone.position.y), zone.end, Vector2(zone.position.x, zone.end.y)]
var dirs: Array[Vector2] = [Vector2(1, 1), Vector2(-1, 1), Vector2(-1, -1), Vector2(1, -1)]
for i in 4:
var corner: Vector2 = corners[i]
var dir: Vector2 = dirs[i]
draw_line(corner, corner + Vector2(dir.x * arm, 0), ink, 8.0)
draw_line(corner, corner + Vector2(0, dir.y * arm), ink, 8.0)
draw_line(corner, corner + Vector2(dir.x * arm, 0), frame, 4.0)
draw_line(corner, corner + Vector2(0, dir.y * arm), frame, 4.0)
draw_circle(corner, 2.4, frame)
func _mirror_plate(point: Vector2, side: float) -> Vector2:
if side > 0.0:
return point
return Vector2(1280.0 - point.x, point.y)
func _draw_batter() -> void:
if phase == "opponent":
_draw_batter_figure(opp_lefty, opp_swing_p, _batting_kit(), false, Vector2.ZERO)
return
_draw_batter_figure(lefty[batter_i] == true, swing_p, _batting_kit(), meet, meet_pos)
func _draw_batter_figure(is_left: bool, p: float, kit: Dictionary, meet_on: bool, meet_at: Vector2) -> void:
var side := -1.0 if is_left else 1.0
var root := Vector2(760, 574) if is_left else Vector2(520, 574)
var slot_u := _smooth(clampf(p / 0.36, 0.0, 1.0))
var fire := _smooth(clampf((p - 0.28) / 0.72, 0.0, 1.0))
var through := _smooth(clampf((p - 0.9) / 0.28, 0.0, 1.0))
var finish := _smooth(clampf((p - 1.02) / 0.38, 0.0, 1.0))
var heel := clampf(fire * 1.15, 0.0, 1.0)
var ink := Color(0.08, 0.07, 0.1)
var skin := Color(0.93, 0.76, 0.58)
var jersey: Color = kit["jersey"]
var pants: Color = kit["pants"]
var cleat := Color(0.12, 0.1, 0.1)
var hand_r := Vector2(500, 472).lerp(Vector2(518, 504), slot_u)
hand_r = hand_r.lerp(Vector2(556, 512), fire)
hand_r = hand_r.lerp(Vector2(548, 446), finish)
var tip_r := Vector2(462, 388).lerp(Vector2(448, 486), slot_u)
tip_r = tip_r.lerp(Vector2(655, 502), fire)
tip_r = tip_r.lerp(Vector2(770, 460), through)
tip_r = tip_r.lerp(Vector2(590, 388), finish)
var grip := _mirror_plate(hand_r, side) + Vector2(0, 40)
var tip := _mirror_plate(tip_r, side) + Vector2(0, 40)
if meet_on and p > 0.72 and p < 1.22:
var on_ball := 1.0 - absf(p - 1.0) / 0.28
tip = tip.lerp(meet_at, clampf(on_ball, 0.0, 1.0))
var barrel := tip - grip
if barrel.length_squared() < 4.0:
barrel = Vector2(side, -0.2)
var stride := _smooth(clampf(p / 0.4, 0.0, 1.0))
var open := fire
var back_foot := root + Vector2(-16.0 * side, 48.0)
var toe := back_foot + Vector2(-6.0 * side, -8.0 * heel)
var lead_foot := root + Vector2(20.0 * side, lerpf(40.0, 22.0, stride))
var hip := root + Vector2(1.0 * side * open, 8.0)
var hip_back := hip + Vector2(-11.0 * side, 2.0)
var hip_lead := hip + Vector2(11.0 * side, 0.0)
var back_knee := hip_back.lerp(toe, 0.5) + Vector2(6.0 * side * (1.0 - heel * 0.4), -4.0)
var lead_knee := hip_lead.lerp(lead_foot, 0.5) + Vector2(4.0 * side, 3.0) * (1.0 - open)
var shoulder_y := hip + Vector2(2.0 * side * open, -40.0)
var lead_shoulder := shoulder_y + Vector2(lerpf(8.0, 15.0, open) * side, lerpf(-4.0, 0.0, open))
var back_shoulder := shoulder_y + Vector2(lerpf(-9.0, -14.0, open) * side, lerpf(5.0, 1.0, open))
var crown := (lead_shoulder + back_shoulder) * 0.5
var head := Vector2(root.x, crown.y - 20.0)
var back_elbow := back_shoulder.lerp(grip, 0.42) + Vector2(-6.0 * side, 10.0) * (1.0 - open)
var lead_elbow := lead_shoulder.lerp(grip, 0.48) + Vector2(0.0, 7.0) * (1.0 - open * 0.75)
_fill_ellipse((back_foot + lead_foot) * 0.5 + Vector2(0, 6), 24, 7, Color(0, 0, 0, 0.3))
draw_line(hip_back, back_knee, pants, 11.0, true)
draw_line(back_knee, toe, pants, 9.0, true)
draw_line(toe, toe + Vector2(-8.0 * side, 3.0), cleat, 4.0, true)
draw_line(hip_lead, lead_knee, pants, 11.0, true)
draw_line(lead_knee, lead_foot, pants, 9.0, true)
draw_line(lead_foot, lead_foot + Vector2(6.0 * side, -7.0), cleat, 4.0, true)
draw_line(back_shoulder, back_elbow, skin, 7.5, true)
draw_line(back_elbow, grip, skin, 6.5, true)
draw_colored_polygon(PackedVector2Array([hip_back, hip_lead, lead_shoulder, back_shoulder]), jersey)
draw_polyline(PackedVector2Array([hip_back, hip_lead, lead_shoulder, back_shoulder, hip_back]), ink, 2.0, true)
draw_line(hip_back, hip_lead, pants, 3.0, true)
var collar: Color = kit["trim"]
draw_line(back_shoulder.lerp(lead_shoulder, 0.22) + Vector2(0, 6), back_shoulder.lerp(lead_shoulder, 0.78) + Vector2(0, 6), collar, 3.0, true)
draw_circle(back_shoulder, 5.5, jersey.darkened(0.08))
draw_circle(lead_shoulder, 5.5, jersey.darkened(0.08))
draw_line(crown, head + Vector2(0, 11), skin, 6.5, true)
_head_back(head, 13.0, kit, side)
draw_line(lead_shoulder, lead_elbow, skin, 7.5, true)
draw_line(lead_elbow, grip, skin, 6.5, true)
var bat_scale := barrel.length() / (BAT_TEX.get_height() * 0.92)
draw_set_transform(grip, barrel.angle() + PI * 0.5, Vector2(bat_scale, bat_scale))
draw_texture(BAT_TEX, Vector2(-BAT_TEX.get_width() * 0.5, -BAT_TEX.get_height() * 0.92))
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
draw_circle(grip, 5.0, skin)
func _base_at(n: int) -> Vector2:
if n == 1:
return FIRST_BASE
if n == 2:
return SECOND_BASE
if n == 3:
return THIRD_BASE
return HOME_BASE
func _draw_baserunners() -> void:
if phase == "opponent":
for slot in [1, 0, 2]:
if opp_on[slot] != true:
continue
var bag: int = slot + 1
var off := Vector2(18, 12) if slot == 0 else (Vector2(-18, 12) if slot == 2 else Vector2(0, -20))
_paint_runner(_base_at(bag) + off, false, _faces_left(bag), false)
return
if phase == "hit" or phase == "trot":
var run_t := clampf((phase_t - 0.12) / 0.74, 0.0, float(trot_span))
for m in movers:
var start_b := int(m["start"])
var dest_b := int(m["dest"])
var span := dest_b - start_b
var traveled := clampf(run_t, 0.0, float(span))
var at := _along_bases(start_b, dest_b, traveled)
var step := mini(start_b + int(traveled), dest_b - 1)
_paint_runner(at, traveled < float(span), _faces_left(step), m["power"] == true)
else:
for slot in [1, 0, 2]:
if int(on_base[slot]) == -1:
continue
var bag: int = slot + 1
var off := Vector2(18, 12) if slot == 0 else (Vector2(-18, 12) if slot == 2 else Vector2(0, -20))
_paint_runner(_base_at(bag) + off, false, _faces_left(bag), base_power[slot] == true)
func _along_bases(start_b: int, dest_b: int, traveled: float) -> Vector2:
if dest_b <= start_b:
return _base_at(dest_b)
if traveled >= float(dest_b - start_b):
return _base_at(0 if dest_b >= 4 else dest_b)
var step := start_b + int(traveled)
var along := traveled - float(int(traveled))
along = along * along * (3.0 - 2.0 * along)
var nxt := 0 if step + 1 >= 4 else step + 1
return _base_at(step).lerp(_base_at(nxt), along)
func _faces_left(base_i: int) -> bool:
var nxt := 0 if base_i + 1 >= 4 else base_i + 1
return _base_at(nxt).x < _base_at(base_i).x
func _paint_runner(at: Vector2, running: bool, face_left: bool, _powered: bool) -> void:
var sc := clampf((at.y - 100.0) / 560.0, 0.5, 0.92)
var look := -1.0 if face_left else 1.0
var kit := _batting_kit()
var stride := sin(Time.get_ticks_msec() * 0.016) * 16.0 if running else 0.0
var pants: Color = kit["pants"]
var skin := Color(0.93, 0.76, 0.58)
draw_set_transform(at, 0.0, Vector2(sc, sc))
var hip := Vector2(look * 2.0, -30.0)
var shoulder := hip + Vector2(look * 2.0, -24.0)
_fill_ellipse(Vector2(0, -2), 16, 6, Color(0, 0, 0, 0.28))
draw_line(hip + Vector2(-5, 0), Vector2(-12 - stride, -4), pants, 8.0, true)
draw_line(hip + Vector2(5, 0), Vector2(12 + stride, -4), pants, 8.0, true)
draw_line(Vector2(-12 - stride, -4), Vector2(-4 - stride, -1), Color(0.1, 0.08, 0.08), 3.0, true)
draw_line(Vector2(12 + stride, -4), Vector2(20 + stride, -2), Color(0.1, 0.08, 0.08), 3.0, true)
var back_arm := shoulder + Vector2(-9.0 * look, 2.0)
draw_line(back_arm, back_arm + Vector2(-6.0 * look - stride * 0.5, 16.0), skin, 5.0, true)
_torso(hip, shoulder, 9.0, 12.0, kit, 1.8)
var front_arm := shoulder + Vector2(9.0 * look, 2.0)
draw_line(front_arm, front_arm + Vector2(8.0 * look + stride * 0.5, 12.0), skin, 5.0, true)
var head := shoulder + Vector2(look * 1.0, -14.0)
draw_line(shoulder, head, skin, 6.0, true)
_head_side(head, 11.0, kit, look)
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
func _catcher_glove(origin: Vector2) -> Vector2:
var rest := origin + Vector2(34, -30)
if phase == "opponent":
if not opp_active or opp_t < 0.2:
return rest
var u := clampf((opp_t - 0.2) / 0.3, 0.0, 1.0)
var opp_dest := opp_target
opp_dest.x = clampf(opp_dest.x, origin.x - 72.0, origin.x + 72.0)
opp_dest.y = clampf(opp_dest.y, origin.y - 156.0, origin.y - 16.0)
if opp_t > 0.7:
return opp_dest.lerp(rest, clampf((opp_t - 0.7) / 0.3, 0.0, 1.0))
return rest.lerp(opp_dest, _smooth(u))
if pitch.is_empty() or phase == "windup" or phase == "hit" or phase == "trot" or phase == "ready" or phase == "final":
return rest
if phase == "whiff" and whiff_wind > 0.0:
return rest
var travel := 1.0
if phase == "flight":
travel = clampf(phase_t / pitch_dur, 0.0, 1.0)
elif phase == "whiff":
travel = clampf(ball_t / pitch_dur, 0.0, 1.0)
var dest := target
if phase == "flight" or phase == "whiff":
dest = _ball_pos(minf(travel, 1.0))
dest.x = clampf(dest.x, origin.x - 72.0, origin.x + 72.0)
dest.y = clampf(dest.y, origin.y - 156.0, origin.y - 16.0)
if phase == "show" or phase == "gameover":
if phase_t <= 0.16:
return dest
return dest.lerp(rest, clampf((phase_t - 0.16) / 0.3, 0.0, 1.0))
var reach := _smooth(clampf((travel - 0.35) / 0.55, 0.0, 1.0))
return rest.lerp(dest, reach)
func _draw_catcher() -> void:
var origin := HOME_BASE + Vector2(0, 76)
var kit := _fielding_kit()
var ink := Color(0.08, 0.07, 0.1)
var skin := Color(0.93, 0.76, 0.58)
var pants: Color = kit["pants"]
var strap := Color(0.6, 0.62, 0.68)
var glove := _catcher_glove(origin)
var glove_side := 1.0 if glove.x >= origin.x else -1.0
var hip := origin + Vector2(0, 12)
var shoulder := origin + Vector2(0, -16)
var glove_sh := shoulder + Vector2(18.0 * glove_side, 2.0)
draw_line(glove_sh, glove, skin, 7.0, true)
_glove(glove, 19.0, -glove_side, 0.0, false, true)
_fill_ellipse(origin + Vector2(0, 30), 30, 9, Color(0, 0, 0, 0.32))
draw_line(hip + Vector2(-14, 0), origin + Vector2(-26, 30), pants, 12.0, true)
draw_line(hip + Vector2(14, 0), origin + Vector2(26, 30), pants, 12.0, true)
draw_line(hip + Vector2(-13, 2), origin + Vector2(-24, 26), strap, 3.0, true)
draw_line(hip + Vector2(13, 2), origin + Vector2(24, 26), strap, 3.0, true)
draw_line(origin + Vector2(-26, 30), origin + Vector2(-16, 34), Color(0.1, 0.08, 0.08), 4.0, true)
draw_line(origin + Vector2(26, 30), origin + Vector2(36, 32), Color(0.1, 0.08, 0.08), 4.0, true)
_torso(hip, shoulder, 22.0, 26.0, kit, 2.2)
draw_line(shoulder + Vector2(-16, 2), hip + Vector2(12, -4), strap, 3.0, true)
draw_line(shoulder + Vector2(16, 2), hip + Vector2(-12, -4), strap, 3.0, true)
var bare_sh := shoulder + Vector2(-18.0 * glove_side, 2.0)
draw_line(bare_sh, hip + Vector2(-20.0 * glove_side, 6.0), skin, 6.5, true)
var head := shoulder + Vector2(0, -20)
draw_line(shoulder, head, skin, 8.0, true)
draw_circle(head, 15, kit["helmet"])
draw_arc(head, 15, 0, TAU, 16, ink, 2.0, true)
draw_line(head + Vector2(-10, -3), head + Vector2(10, 7), strap, 2.4, true)
draw_line(head + Vector2(10, -3), head + Vector2(-10, 7), strap, 2.4, true)
draw_line(head + Vector2(-13, 3), head + Vector2(13, 3), strap, 2.0, true)
func _draw_ball() -> void:
if phase == "opponent":
_draw_opp_ball()
return
if pitch.is_empty() or phase == "ready" or phase == "final":
return
var travel := 0.0
var radius := 7.0
if phase == "windup":
ball = hand_pos
elif phase == "flight" or (phase == "whiff" and whiff_wind <= 0.0):
var clock := phase_t if phase == "flight" else ball_t
travel = clampf(clock / pitch_dur, 0.0, 1.0)
ball = _ball_pos(travel)
radius = lerpf(7.0, 24.0, travel)
elif phase == "whiff":
ball = hand_pos
elif phase == "hit":
var release := hit_flight + 0.1
var arrive := release + throw_dur
if throw_base == -2 and phase_t > hit_flight:
ball = hit_to
radius = 6.5
elif phase_t <= hit_flight or throw_base == -1:
var u := clampf(phase_t / maxf(hit_flight, 0.05), 0.0, 1.0)
var eased := 1.0 - (1.0 - u) * (1.0 - u)
ball = hit_from.lerp(hit_to, eased)
ball.y -= sin(minf(u, 1.0) * PI) * hit_arc
radius = lerpf(hit_radius, 6.0, eased)
if throw_base == -1 and phase_t > hit_flight + 0.2:
radius = 0.0
elif phase_t <= release:
ball = throw_from
radius = 7.0
elif phase_t <= arrive:
var u := clampf((phase_t - release) / maxf(throw_dur, 0.05), 0.0, 1.0)
ball = throw_from.lerp(throw_to, 1.0 - (1.0 - u) * (1.0 - u))
ball.y -= sin(u * PI) * lerpf(16.0, 36.0, clampf(throw_dur, 0.0, 1.0))
radius = lerpf(6.0, 12.0, clampf((ball.y - 150.0) / 460.0, 0.0, 1.0))
else:
ball = throw_to
radius = 8.0
else:
var hold := 0.16
var dur := 0.42
var from := target
var to := hand_pos
if phase_t <= hold:
ball = from
radius = 16.0
else:
var u := clampf((phase_t - hold) / dur, 0.0, 1.0)
var eased := _smooth(u)
ball = from.lerp(to, eased)
ball.y -= sin(u * PI) * 22.0
radius = lerpf(16.0, 7.0, eased)
if radius < 0.5:
return
var shadow_at := ball
var shadow_rx := radius * 0.62
var shadow_ry := maxf(radius * 0.24, 2.0)
var shadow_a := 0.3
if phase == "hit" and phase_t <= hit_flight:
var lift := sin(clampf(phase_t / maxf(hit_flight, 0.05), 0.0, 1.0) * PI)
shadow_at = Vector2(ball.x, ball.y + lift * hit_arc)
shadow_rx = lerpf(radius * 0.55, radius * 0.32, lift)
shadow_ry = maxf(shadow_rx * 0.36, 2.0)
shadow_a = lerpf(0.32, 0.12, lift)
_fill_ellipse(shadow_at + Vector2(0, 4), shadow_rx, shadow_ry, Color(0, 0, 0, shadow_a))
draw_texture_rect(BALL_TEX, Rect2(ball.x - radius, ball.y - radius, radius * 2.0, radius * 2.0), false)
var tint := _timing_tint()
if tint.a > 0.01:
draw_circle(ball, radius * 0.92, tint)
func _draw_opp_ball() -> void:
if not opp_active:
return
var radius := 7.0
var contact := 0.5
var kind := str(opp_result.get("kind", "out"))
if opp_t < 0.2:
ball = hand_pos
elif opp_t < contact:
var u := clampf((opp_t - 0.2) / 0.3, 0.0, 1.0)
ball = start.lerp(opp_target, u)
radius = lerpf(7.0, 22.0, u)
elif kind == "walk" or kind == "strikeout":
ball = opp_target
radius = 16.0
else:
var u := clampf((opp_t - contact) / 0.42, 0.0, 1.0)
var eased := 1.0 - (1.0 - u) * (1.0 - u)
ball = opp_target.lerp(opp_hit_to, eased)
ball.y -= sin(u * PI) * opp_hit_arc
radius = lerpf(16.0, 6.0, eased)
if kind == "home" and u >= 1.0:
return
_fill_ellipse(Vector2(ball.x, ball.y + sin(u * PI) * opp_hit_arc + 4.0), radius * 0.5, maxf(radius * 0.2, 2.0), Color(0, 0, 0, 0.25))
draw_texture_rect(BALL_TEX, Rect2(ball.x - radius, ball.y - radius, radius * 2.0, radius * 2.0), false)
func _timing_tint() -> Color:
var clock := -1.0
if phase == "flight":
clock = phase_t
elif phase == "whiff" and whiff_wind <= 0.0:
clock = ball_t
if clock < 0.0:
return Color(0, 0, 0, 0)
var edge := 0.17 if power_mode else 0.25
var ideal := pitch_dur - SWING_DELAY
if absf(clock - ideal) >= edge - 0.02:
return Color(0, 0, 0, 0)
if zone.has_point(target):
return Color(0.9, 0.08, 0.06, 0.5)
return Color(0.15, 0.45, 0.95, 0.5)
func _glove(center: Vector2, r: float, thumb: float, tilt: float, pocket: bool, mitt: bool) -> void:
r = maxf(r, 3.0)
var leather := Color(0.58, 0.36, 0.17)
var dark := Color(0.4, 0.23, 0.1)
var ink := Color(0.16, 0.1, 0.05)
var lace := Color(0.82, 0.66, 0.38)
var outline := maxf(r * 0.12, 1.0)
var finger_r := r * (0.42 if mitt else 0.36)
var finger_d := r * (0.84 if mitt else 0.86)
var fingers: Array[Vector2] = []
var count := 3 if mitt else 4
for i in count:
var span := 1.5 if mitt else 1.7
var ang := -PI * 0.5 + tilt + (float(i) - float(count - 1) * 0.5) * span / float(count)
fingers.append(center + Vector2(cos(ang), sin(ang)) * finger_d)
var thumb_at := center + Vector2(cos(tilt) * thumb, sin(tilt) * 0.25 + 0.15) * r * 0.95
var thumb_r := r * 0.44
# Outline pass, then fill pass, so the lobes read as one piece of leather.
for pass_i in 2:
var col := ink if pass_i == 0 else leather
var grow := outline if pass_i == 0 else 0.0
draw_circle(center, r + grow, col)
for f in fingers:
draw_circle(f, finger_r + grow, col)
draw_circle(thumb_at, thumb_r + grow, col)
if pass_i == 0:
continue
var near: Vector2 = fingers[count - 1] if thumb > 0.0 else fingers[0]
var web := PackedVector2Array([thumb_at, near, center + Vector2(thumb * r * 0.3, -r * 0.1)])
draw_colored_polygon(web, dark)
for i in fingers.size() - 1:
draw_line(fingers[i] + Vector2(0, finger_r * 0.2), fingers[i + 1] + Vector2(0, finger_r * 0.2), ink, maxf(r * 0.07, 0.8), true)
if pocket:
draw_circle(center + Vector2(0, r * 0.12), r * 0.55, dark)
draw_circle(center + Vector2(0, r * 0.12), r * 0.42, Color(0.47, 0.28, 0.12))
var lace_w := maxf(r * 0.08, 0.8)
for f in fingers:
var toward := (center - f).normalized()
draw_line(f - toward * finger_r * 0.6, f + toward * finger_r * 0.4, lace, lace_w, true)
else:
# Back of a padded mitt: lighter pad inside a darker rim, with a few laces.
draw_circle(center + Vector2(0, r * 0.05), r * 0.7, Color(0.64, 0.42, 0.21))
var lace_w := maxf(r * 0.08, 0.8)
draw_line(center + Vector2(-r * 0.45, r * 0.2), center + Vector2(r * 0.45, r * 0.2), lace, lace_w, true)
draw_line(center + Vector2(-r * 0.4, r * 0.48), center + Vector2(r * 0.4, r * 0.48), lace, lace_w, true)
for i in 5:
var ang := -PI * 0.5 + (float(i) - 2.0) * 0.36
var dir := Vector2(cos(ang), sin(ang))
draw_line(center + dir * r * 0.72, center + dir * r * 0.98, lace, lace_w, true)
draw_line(center + Vector2(-r * 0.6, r * 0.85), center + Vector2(r * 0.6, r * 0.85), ink, maxf(r * 0.1, 1.0), true)
func _draw_hud() -> void:
_draw_scoreboard()
draw_rect(Rect2(16, 12, 348, 126), Color(0.04, 0.05, 0.06, 0.84))
draw_rect(Rect2(16, 12, 4, 126), GOLD)
_text(Vector2(32, 40), "CARIBOOZE SIMULATOR", 20, GOLD)
var shown_outs := half_outs if phase == "opponent" else outs
_text(Vector2(32, 70), "OUTS", 16, Color(0.9, 0.9, 0.88))
for i in 3:
var center := Vector2(94.0 + float(i) * 26.0, 63.0)
draw_circle(center, 9.0, Color(0.85, 0.16, 0.14) if i < shown_outs else Color(0.2, 0.22, 0.24))
draw_arc(center, 9.0, 0, TAU, 16, Color(0.7, 0.72, 0.74), 1.5, true)
_text(Vector2(178, 70), "%d - %d" % [balls, strikes], 16, Color.WHITE)
var mode_line := "POWER" if power_mode else "CONTACT"
_text(Vector2(240, 70), mode_line, 16, GOLD if power_mode else Color(0.9, 0.9, 0.88))
_text(Vector2(318, 70), "H %d" % hits, 16, Color.WHITE)
if phase == "opponent":
_text(Vector2(32, 108), "%s %s" % [THEM_NAME, AWAY_LINEUP[opp_batter_i]], 20, Color.WHITE)
else:
_text(Vector2(32, 108), "%d %s" % [batter_i + 1, lineup[batter_i]], 20, Color.WHITE)
draw_rect(Rect2(16, 148, 300, 168), Color(0.04, 0.05, 0.06, 0.72))
for i in lineup.size():
var on_deck := i == batter_i and phase != "opponent"
var row_color := GOLD if on_deck else Color(0.78, 0.79, 0.8)
_text(Vector2(26, 160 + i * 16), "%d %s" % [i + 1, lineup[i]], 14, row_color)
var line := _stat_line(i)
if line != "":
var wide_s := font.get_string_size(line, HORIZONTAL_ALIGNMENT_LEFT, -1, 13).x
_text(Vector2(306.0 - wide_s, 160 + i * 16), line, 13, row_color)
if phase == "ready":
var title := "%s vs %s" % [US_NAME, THEM_NAME]
var wide_t := font.get_string_size(title, HORIZONTAL_ALIGNMENT_LEFT, -1, 30).x
draw_rect(Rect2(640.0 - wide_t * 0.5 - 24.0, 158, wide_t + 48.0, 66), Color(0.04, 0.05, 0.06, 0.82))
_text(Vector2(640, 186), title, 30, GOLD, true)
_text(Vector2(640, 212), "%d INNINGS TAP OR SPACE TO SWING" % INNINGS, 14, Color(0.9, 0.9, 0.88), true)
elif message != "":
var good := "HOME" in message or "SINGLE" in message or "DOUBLE" in message or "TRIPLE" in message or "WALK" in message
if phase == "opponent":
good = false
var banner := font.get_string_size(message, HORIZONTAL_ALIGNMENT_LEFT, -1, 26)
draw_rect(Rect2(640.0 - banner.x * 0.5 - 16.0, 158, banner.x + 32.0, 34), Color(0.04, 0.05, 0.06, 0.8))
_text(Vector2(640, 182), message, 26, GOLD if good else Color.WHITE, true)
if phase == "windup" or phase == "flight":
draw_rect(Rect2(430, 698, 420, 18), Color(0.04, 0.05, 0.06, 0.72))
_text(Vector2(640, 714), "TAP OR SPACE TO SWING", 14, Color(0.92, 0.92, 0.9), true)
if phase != "windup" and phase != "ready" and phase != "opponent" and not pitch.is_empty():
var radar := "%d MPH" % pitch_mph
var label := str(pitch["name"]).to_upper()
var wide := maxf(font.get_string_size(radar, HORIZONTAL_ALIGNMENT_LEFT, -1, 28).x, font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, 14).x)
draw_rect(Rect2(640.0 - wide * 0.5 - 18.0, 14, wide + 36.0, 58), Color(0.04, 0.05, 0.06, 0.82))
_text(Vector2(640, 40), radar, 28, Color.WHITE, true)
_text(Vector2(640, 62), label, 14, GOLD, true)
func _draw_scoreboard() -> void:
var cols := mini(maxi(inning, INNINGS), 9)
var first := maxi(inning - cols + 1, 1)
var cw := 30.0
var label_w := 112.0
var extra := 34.0
var width := label_w + float(cols) * cw + extra * 2.0 + 12.0
var x := 640.0 - width * 0.5
var y := 78.0
draw_rect(Rect2(x, y, width, 74), Color(0.04, 0.1, 0.07, 0.94))
draw_rect(Rect2(x, y, width, 3), GOLD)
for i in cols:
_text(Vector2(x + label_w + float(i) * cw + cw * 0.5, y + 16), str(first + i), 12, Color(0.72, 0.76, 0.7), true)
_text(Vector2(x + label_w + float(cols) * cw + extra * 0.5, y + 16), "R", 13, GOLD, true)
_text(Vector2(x + label_w + float(cols) * cw + extra * 1.5, y + 16), "H", 13, GOLD, true)
_score_row(x, y + 34, US_NAME, true, cols, first, label_w, cw, extra)
_score_row(x, y + 56, THEM_NAME, false, cols, first, label_w, cw, extra)
func _score_row(x: float, y: float, label: String, ours: bool, cols: int, first: int, label_w: float, cw: float, extra: float) -> void:
var idle := phase == "gameover" or phase == "final" or phase == "ready"
var active := (ours and phase != "opponent" and not idle) or (not ours and phase == "opponent")
_text(Vector2(x + 8, y), label, 14, GOLD if active else Color(0.82, 0.84, 0.8))
for i in cols:
_text(Vector2(x + label_w + float(i) * cw + cw * 0.5, y), _inning_cell(first + i, ours), 14, Color.WHITE, true)
_text(Vector2(x + label_w + float(cols) * cw + extra * 0.5, y), str(_run_total(ours)), 14, GOLD, true)
_text(Vector2(x + label_w + float(cols) * cw + extra * 1.5, y), str(_hit_total(ours)), 14, Color.WHITE, true)
func _inning_cell(n: int, ours: bool) -> String:
var line: Array = our_line if ours else their_line
var idx := n - 1
if idx < line.size():
return str(int(line[idx]))
if n == inning and ours and _our_half_live():
return str(inning_runs)
if n == inning and not ours and phase == "opponent":
return str(half_runs)
return "·"
func _our_half_live() -> bool:
return phase != "gameover" and phase != "final" and phase != "ready" and phase != "opponent"
func _run_total(ours: bool) -> int:
var line: Array = our_line if ours else their_line
var sum := _tally(line)
if ours and our_line.size() < inning and _our_half_live():
sum += inning_runs
if not ours and phase == "opponent":
sum += half_runs
return sum
func _hit_total(ours: bool) -> int:
if ours:
if our_line.size() < inning and _our_half_live():
return our_hits + hits
return our_hits
if phase == "opponent":
return their_hits + half_hits
return their_hits
func _text(pos: Vector2, text: String, size: int, color: Color, center := false) -> void:
var p := pos
if center:
p.x -= font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x * 0.5
draw_string(font, p, text, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)