@tool extends ArcadeGame @export_range(0,99) var boards_to_win: int = 1 @export_range(1,9) var starting_lives: int = 3 @export_range(15,120) var crossing_time: float = 60.0 @export_range(.5,2) var difficulty: float = 1.0 @export var music_enabled: bool = true const HOME_X := [16,64,112,160,208] var phase := "title" var phase_time := 0.0 var clock := 0.0 var score := 0 var high_score := 0 var lives := 3 var board_number := 1 var homes: Array[bool] = [false,false,false,false,false] var frog := Vector2(112,232) var from := Vector2(112,232) var destination := Vector2(112,232) var direction := 0 var hop_left := 0.0 var repeat_left := 0.0 var time_left := 60.0 var furthest_row := 0 var warned := false var extra_awarded := false var death_kind := "squash" var lanes: Array[Dictionary] = [] var leaderboard: Array[int] = [] var attract_time := 0.0 var demo_active := false var demo_time := 0.0 var idle_score := 0 @onready var view: Node2D = $PixelScreen/Board func _ready() -> void: make_lanes() lives = starting_lives func make_lanes() -> void: lanes.clear() var definitions := [ [1,"car",-24.0,16.0,80.0,20.0,1], [2,"car",28.0,16.0,82.0,48.0,3], [3,"car",-32.0,16.0,78.0,8.0,0], [4,"car",45.0,16.0,110.0,70.0,2], [5,"car",-22.0,32.0,100.0,30.0,4], [7,"turtle",-24.0,48.0,72.0,4.0,0], [8,"log",25.0,48.0,98.0,30.0,0], [9,"log",36.0,96.0,144.0,5.0,0], [10,"turtle",-30.0,32.0,70.0,14.0,1], [11,"log",22.0,64.0,100.0,40.0,0]] for item in definitions: lanes.append({"row":item[0],"kind":item[1],"speed":item[2]*(1+(board_number-1)*.10)*difficulty,"width":item[3],"spacing":item[4],"offset":item[5],"sprite":item[6]}) func start_game() -> bool: if not super.start_game(): return false demo_active = false attract_time = 0 $Audio.stop_all() score = 0 lives = starting_lives board_number = 1 extra_awarded = false homes.fill(false) make_lanes() respawn() phase = "ready" phase_time = 1.5 $Audio.effect("coin-in") return true func reset_game() -> void: super.reset_game() demo_active = false attract_time = 0 $Audio.stop_all() phase = "title" clock = 0 score = 0 board_number = 1 lives = starting_lives homes.fill(false) make_lanes() respawn() func respawn() -> void: frog = Vector2(112,232) from = frog destination = frog direction = 0 hop_left = 0 repeat_left = 0 time_left = crossing_time furthest_row = 0 warned = false func _unhandled_input(event: InputEvent) -> void: if Engine.is_editor_hint() or (event is InputEventKey and event.echo): return if event.is_action_pressed("arcade_start"): if not is_running: start_game() func pressed(action: String) -> bool: return Input.is_action_pressed(action) func hop(step: Vector2i) -> bool: if phase != "play" or hop_left > 0 or absi(step.x)+absi(step.y) != 1: return false var target := frog+Vector2(step)*16 if target.x < 8 or target.x > 216 or target.y > 232 or target.y < 40: return false direction = 0 if step.y < 0 else (2 if step.y > 0 else (1 if step.x < 0 else 3)) from = frog destination = target hop_left = .12 $Audio.effect("hop") return true func _physics_process(delta: float) -> void: clock += delta if Engine.is_editor_hint(): attract_time = fmod(clock,44.0) view.queue_redraw() return if phase in ["title","scores"]: attract_time += delta if phase == "title" and attract_time >= 44.0: begin_demo() view.queue_redraw() return elif phase == "scores" and attract_time >= 7.0: phase = "title" attract_time = 0 elif phase in ["ready","dead","home","clear"]: phase_time -= delta if phase_time <= 0: match phase: "ready": phase = "play" if music_enabled and not demo_active: $Audio.music() "dead": if lives <= 0 and not demo_active: finish(false) else: respawn() phase = "play" if music_enabled and not demo_active: $Audio.music() "home": if not homes.has(false): add_score(1000) phase = "clear" phase_time = 2.0 else: respawn() phase = "play" "clear": if demo_active: end_demo() elif boards_to_win > 0 and board_number >= boards_to_win: finish(true) else: board_number += 1 homes.fill(false) make_lanes() respawn() phase = "ready" phase_time = 1.5 elif phase == "play": repeat_left -= delta if hop_left <= 0 and repeat_left <= 0: var step := Vector2i.ZERO if demo_active: step = demo_step() elif pressed("arcade_up"): step = Vector2i.UP elif pressed("arcade_down"): step = Vector2i.DOWN elif pressed("arcade_left"): step = Vector2i.LEFT elif pressed("arcade_right"): step = Vector2i.RIGHT if step != Vector2i.ZERO and hop(step): repeat_left = .16 step_world(delta) if demo_active: demo_time += delta if demo_time >= 32.0: end_demo() view.queue_redraw() func begin_demo() -> void: idle_score = score demo_active = true demo_time = 0 score = 0 lives = starting_lives board_number = 1 homes.fill(false) clock = 0 make_lanes() respawn() phase = "play" $Audio.stop_all() func end_demo() -> void: demo_active = false score = idle_score phase = "scores" attract_time = 0 $Audio.stop_all() # Predict the same lane positions and hitboxes used by live gameplay. # The demo supplies ordinary hops; it has no invulnerability or scripted teleports. func demo_safe(step: Vector2i) -> bool: var target := frog + Vector2(step)*16 if target.x < 8 or target.x > 216 or target.y < 40 or target.y > 232: return false if target.y == 40: for i in range(5): if absf(target.x-HOME_X[i]) <= 7 and not homes[i]: return true return false for tick in range(1,13): var t := tick * .01 var point := frog.lerp(target,t/.12) var row := roundi((232-point.y)/16) for lane in lanes: if lane.row != row: continue var supported := false var predicted := lane.duplicate() predicted.offset += lane.speed*t for x in lane_objects(predicted): if lane.kind == "car" and point.x+6 > x+2 and point.x-6 < x+lane.width-2: return false if lane.kind != "car" and point.x-5 >= x and point.x+5 <= x+lane.width: supported = true if tick == 12 and lane.kind != "car": if not supported: return false if lane.kind == "turtle" and lane.sprite == 1 and fposmod(clock+t,7.0) > 5.3: return false return true func demo_step() -> Vector2i: if demo_safe(Vector2i.UP): return Vector2i.UP var target_x := 112.0 var distance := INF for i in range(5): if not homes[i] and absf(frog.x-HOME_X[i]) < distance: distance = absf(frog.x-HOME_X[i]) target_x = HOME_X[i] var side := Vector2i.RIGHT if target_x > frog.x else Vector2i.LEFT if absf(target_x-frog.x)>7 and demo_safe(side): return side return Vector2i.ZERO func lane_objects(lane: Dictionary) -> Array[float]: var result: Array[float] = [] var start: float = fposmod(lane.offset,lane.spacing)-lane.spacing while start < 224: result.append(start) start += lane.spacing return result func submerged(lane: Dictionary) -> bool: return lane.kind == "turtle" and lane.sprite == 1 and fposmod(clock,7.0) > 5.7 func step_world(delta: float) -> void: time_left -= delta if time_left <= 0: die("time") return if time_left <= 10 and not warned: warned = true $Audio.effect("time") for lane in lanes: lane.offset = fposmod(lane.offset+lane.speed*delta,lane.spacing) if hop_left > 0: hop_left = maxf(0,hop_left-delta) frog = from.lerp(destination,1.0-hop_left/.12) if hop_left <= 0: var row := roundi((232-frog.y)/16) if row > furthest_row: add_score((row-furthest_row)*10) furthest_row = row # Evaluate the current rendered location, including the hop trajectory. var row := roundi((232-frog.y)/16) if row >= 12 and hop_left <= 0: var index := -1 for i in range(5): if absf(frog.x-HOME_X[i]) <= 7: index = i if index < 0 or homes[index]: die("squash") else: homes[index] = true add_score(50+int(time_left)*10) phase = "home" phase_time = .8 $Audio.effect("extra") return for lane in lanes: if lane.row != row: continue var supported := false for x in lane_objects(lane): if lane.kind == "car": if frog.x+5 > x+2 and frog.x-5 < x+lane.width-2: die("squash") return elif frog.x-4 >= x and frog.x+4 <= x+lane.width and not submerged(lane): supported = true if lane.kind != "car": # Support is checked on landing; hops may cross a platform edge in flight. if hop_left <= 0: if not supported: die("plunk") return frog.x += lane.speed*delta if frog.x < 5 or frog.x > 219: die("plunk") break func die(kind: String) -> void: if phase != "play": return lives -= 1 death_kind = kind phase = "dead" phase_time = 1.1 hop_left = 0 $Audio.stop_music() $Audio.effect(kind) func add_score(value: int) -> void: score += value if not demo_active: high_score = maxi(high_score,score) if score >= 20000 and not extra_awarded: extra_awarded = true lives += 1 $Audio.effect("extra") func finish(won: bool) -> void: if demo_active: end_demo() return $Audio.stop_all() leaderboard.append(score) leaderboard.sort() leaderboard.reverse() if leaderboard.size() > 5: leaderboard.resize(5) phase = "won" if won else "lost" if won: win_game() else: lose_game()