@tool extends ArcadeGame ## All positions use the 256 x 231 logical arcade screen. @export_range(0,99) var waves_to_win: int = 3 @export_range(.5,2.0) var difficulty: float = 1.0 @export_range(1000,50000,1000) var bonus_city_interval: int = 10000 @export var relative_mouse_aim: bool = false @export_range(.1,4.0) var trackball_sensitivity: float = 1.0 const CITY_X := [43,68,93,163,188,213] const BASE_X := [16,128,240] var phase := "title" var phase_time := 0.0 var clock := 0.0 var attract_time := 0.0 var wave := 1 var score := 0 var high_score := 0 var bonus_bank := 0 var next_bonus := 10000 var cities: Array[bool] = [true,true,true,true,true,true] var bases: Array[bool] = [true,true,true] var ammo: Array[int] = [10,10,10] var cooldown: Array[float] = [0.0,0.0,0.0] var aim := Vector2(128,100) var incoming: Array[Dictionary] = [] var interceptors: Array[Dictionary] = [] var blasts: Array[Dictionary] = [] var aircraft: Array[Dictionary] = [] var leaderboard: Array[int] = [] var remaining := 0 var spawn_time := 0.0 var aircraft_time := 0.0 var wave_bonus := 0 var recorded := false var rng := RandomNumberGenerator.new() @onready var board: Node2D = $PixelScreen/Board @onready var display: Node2D = $Display func _ready() -> void: if not Engine.is_editor_hint(): rng.randomize() func start_game() -> bool: if not super.start_game(): return false wave = 1 score = 0 bonus_bank = 0 next_bonus = bonus_city_interval recorded = false cities.fill(true) incoming.clear() interceptors.clear() blasts.clear() aircraft.clear() _begin_wave() return true func reset_game() -> void: super.reset_game() $Audio.stop() phase = "title" attract_time = 0 phase_time = 0 score = 0 wave = 1 cities.fill(true) bases.fill(true) ammo.fill(10) cooldown.fill(0) incoming.clear() interceptors.clear() blasts.clear() aircraft.clear() bonus_bank = 0 func _begin_wave() -> void: bases.fill(true) ammo.fill(10) cooldown.fill(0) remaining = 12 + mini(wave,20)*3 spawn_time = 1.0 aircraft_time = 9.0 phase = "ready" phase_time = 2.2 wave_bonus = 0 func add_trackball_motion(motion: Vector2) -> void: set_aim(aim+motion*trackball_sensitivity) func set_aim(point: Vector2) -> void: aim = point.clamp(Vector2(1,25),Vector2(254,199)) func _action(event: InputEvent, action: String) -> bool: return event.is_action_pressed(action) func _unhandled_input(event: InputEvent) -> void: if Engine.is_editor_hint(): return if event is InputEventMouseMotion: if relative_mouse_aim: add_trackball_motion(display.to_logical_delta(event.relative)) else: set_aim(display.to_logical(event.position)) if event is InputEventKey and event.echo: return if _action(event,"arcade_start"): if not is_running: start_game() return if phase != "play": return if event is InputEventMouseButton and not relative_mouse_aim: set_aim(display.to_logical(event.position)) for i in range(3): if _action(event,"arcade_action_%d" % (i+1)): fire(i) return func fire(base: int) -> bool: if phase != "play" or base < 0 or base > 2 or not bases[base] or ammo[base] <= 0 or cooldown[base] > 0: return false ammo[base] -= 1 cooldown[base] = .12 var origin := Vector2(BASE_X[base],202) interceptors.append({"origin":origin,"pos":origin,"target":aim,"speed":180.0 if base == 1 else 120.0}) $Audio.play("fire") return true func multiplier() -> int: return mini(6,1+int((wave-1)/2)) func add_score(amount: int) -> void: score += amount high_score = maxi(high_score,score) while score >= next_bonus: bonus_bank += 1 next_bonus += bonus_city_interval func _record_score() -> void: if recorded: return recorded = true leaderboard.append(score) leaderboard.sort() leaderboard.reverse() if leaderboard.size() > 8: leaderboard.resize(8) func _physics_process(delta: float) -> void: clock += delta if Engine.is_editor_hint(): board.queue_redraw() return for i in range(3): cooldown[i] = maxf(0,cooldown[i]-delta) match phase: "title","leaderboard": attract_time += delta if phase == "title" and attract_time >= 9.0: phase = "leaderboard" attract_time = 0.0 elif phase == "leaderboard" and attract_time >= 7.0: phase = "title" attract_time = 0.0 "ready": phase_time -= delta if phase_time <= 0: phase = "play" "play": _step(delta) "bonus": phase_time -= delta if phase_time <= 0: if waves_to_win > 0 and wave >= waves_to_win: phase = "won" _record_score() win_game() else: wave += 1 _begin_wave() "lost": phase_time -= delta if phase_time <= 0: phase = "title" attract_time = 0 board.queue_redraw() func target_point() -> Vector2: var candidates: Array[Vector2] = [] for i in range(6): if cities[i]: candidates.append(Vector2(CITY_X[i],211)) for i in range(3): if bases[i]: candidates.append(Vector2(BASE_X[i],204)) return candidates[rng.randi_range(0,candidates.size()-1)] if not candidates.is_empty() else Vector2(128,212) func spawn_missile(origin: Vector2, kind: String = "missile") -> void: incoming.append({"origin":origin,"pos":origin,"target":target_point(),"kind":kind,"speed":(11.0+mini(wave,25)*1.8)*difficulty,"split":kind == "splitter","alive":true}) func explosion(point: Vector2, hostile: bool = false, radius: float = 18.0) -> void: blasts.append({"pos":point,"age":0.0,"radius":0.0,"max":radius,"hostile":hostile}) if phase == "play": $Audio.play("hit") func _restore_cities() -> void: for i in range(6): if not cities[i] and bonus_bank > 0: cities[i] = true bonus_bank -= 1 func _impact(point: Vector2) -> void: for i in range(6): if absf(point.x-CITY_X[i]) < 9: cities[i] = false for i in range(3): if absf(point.x-BASE_X[i]) < 12: bases[i] = false ammo[i] = 0 explosion(point,true,14) func _step(delta: float) -> void: spawn_time -= delta if remaining > 0 and spawn_time <= 0: var kind := "missile" if wave >= 3 and remaining%5 == 0: kind = "splitter" if wave >= 5 and remaining%7 == 0: kind = "smart" spawn_missile(Vector2(rng.randf_range(4,252),24),kind) remaining -= 1 spawn_time = maxf(.24,1.1-wave*.045)/difficulty if wave >= 3 and remaining > 0: aircraft_time -= delta if aircraft_time <= 0: aircraft.append({"pos":Vector2(-10,55+rng.randf()*35),"drop":1.0,"alive":true}) aircraft_time = 15 for shot in interceptors: shot.pos = shot.pos.move_toward(shot.target,shot.speed*delta) if shot.pos == shot.target: explosion(shot.target) interceptors = interceptors.filter(func(s: Dictionary) -> bool: return s.pos != s.target) for blast in blasts: blast.age += delta var age: float = blast.age blast.radius = blast.max * (minf(1,age/.45) if age < 1.0 else maxf(0,(1.8-age)/.8)) blasts = blasts.filter(func(b: Dictionary) -> bool: return b.age < 1.8) var children: Array[Vector2] = [] for missile in incoming: var previous: Vector2 = missile.pos var destination: Vector2 = missile.target if missile.kind == "smart": for blast in blasts: if not blast.hostile and missile.pos.distance_to(blast.pos) < blast.radius+15: destination.x += 60 if missile.pos.x >= blast.pos.x else -60 missile.pos = missile.pos.move_toward(destination,missile.speed*delta).clamp(Vector2(0,24),Vector2(255,214)) for blast in blasts: if blast.hostile: continue var nearest := Geometry2D.get_closest_point_to_segment(blast.pos,previous,missile.pos) if nearest.distance_to(blast.pos) <= blast.radius+1: missile.alive = false add_score((125 if missile.kind == "smart" else 25)*multiplier()) break if not missile.alive: explosion(missile.pos,false,13) elif missile.pos.y >= 202 and missile.pos.distance_to(missile.target) < 12: missile.alive = false _impact(missile.target) elif missile.split and missile.pos.y > 90: missile.split = false children.append(missile.pos) incoming = incoming.filter(func(m: Dictionary) -> bool: return m.alive) for point in children: spawn_missile(point) spawn_missile(point) for plane in aircraft: plane.pos.x += delta*(18+wave) plane.drop -= delta if plane.drop <= 0: spawn_missile(plane.pos) plane.drop = 3 for blast in blasts: if not blast.hostile and plane.pos.distance_to(blast.pos) < blast.radius+4: plane.alive = false add_score(100*multiplier()) explosion(plane.pos) break aircraft = aircraft.filter(func(p: Dictionary) -> bool: return p.alive and p.pos.x < 268) if not cities.has(true) and bonus_bank == 0: phase = "lost" phase_time = 5 _record_score() lose_game() elif remaining == 0 and incoming.is_empty() and interceptors.is_empty() and blasts.is_empty() and aircraft.is_empty(): wave_bonus = (cities.count(true)*100+(ammo[0]+ammo[1]+ammo[2])*5)*multiplier() add_score(wave_bonus) _restore_cities() phase = "bonus" phase_time = 3.0 $Audio.play("level")