284 lines
8.6 KiB
GDScript
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")
|
|
|