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)