330 lines
9.0 KiB
GDScript
330 lines
9.0 KiB
GDScript
@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()
|