Files

284 lines
8.6 KiB
GDScript

@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")