Added Glitch Room Source code to Gitty
This commit is contained in:
commit
1ac9eb5644
505 files changed
+28504
No files matched your search
@@ -0,0 +1,523 @@
|
||||
@tool
|
||||
extends ArcadeGame
|
||||
const ACTOR = preload("res://games/galaga/actors/actor.gd")
|
||||
const FIGHTER = preload("res://games/galaga/actors/Fighter.tscn")
|
||||
const ENEMY = preload("res://games/galaga/actors/Enemy.tscn")
|
||||
const PROJECTILE = preload("res://games/galaga/actors/Projectile.tscn")
|
||||
const EXPLOSION = preload("res://games/galaga/actors/Explosion.tscn")
|
||||
@export_group("Room Objective")
|
||||
@export_range(0,100,1) var stages_to_win := 1
|
||||
@export_range(0,1000000,100) var score_to_win := 0
|
||||
@export_range(1,9,1) var starting_lives := 3
|
||||
@export var extra_life_score := 20000
|
||||
@export var second_extra_life_score := 70000
|
||||
@export var repeat_extra_life_interval := 70000
|
||||
var next_extra_life := 20000
|
||||
var stage := 1
|
||||
var score := 0
|
||||
var lives := 3
|
||||
var shots_fired := 0
|
||||
var hits := 0
|
||||
var state := "idle"
|
||||
var timer := 0.0
|
||||
var clock := 0.0
|
||||
var cooldown := 0.0
|
||||
var dive_timer := 2.0
|
||||
var invulnerable := 0.0
|
||||
var awarded := false
|
||||
var player: Node2D
|
||||
var enemies: Array[Dictionary] = []
|
||||
var bullets: Array[Dictionary] = []
|
||||
var effects: Array[Dictionary] = []
|
||||
var voices: Array[AudioStreamPlayer] = []
|
||||
var streams := {}
|
||||
var field: Node2D
|
||||
var hud: Node2D
|
||||
var rng := RandomNumberGenerator.new()
|
||||
@export_group("Opening Timing")
|
||||
@export var intro_seconds := 6.0
|
||||
@export var stage_card_seconds := 1.0
|
||||
@export var player_card_seconds := 1.0
|
||||
@export_group("Cabinet")
|
||||
@export var credits := 1
|
||||
var wingman: Node2D
|
||||
var captive: Node2D
|
||||
var captor: Dictionary = {}
|
||||
var dual := false
|
||||
var capture_origin := Vector2.ZERO
|
||||
var capture_cooldown := 8.0
|
||||
var beam: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
pass
|
||||
field = preload("res://games/galaga/galaga_view.gd").new()
|
||||
add_child(field)
|
||||
player = FIGHTER.instantiate()
|
||||
field.add_child(player)
|
||||
player.position = Vector2(112,256)
|
||||
wingman = FIGHTER.instantiate()
|
||||
field.add_child(wingman)
|
||||
wingman.visible = false
|
||||
captive = FIGHTER.instantiate()
|
||||
field.add_child(captive)
|
||||
captive.visible = false
|
||||
captive.spinning = false
|
||||
captive.modulate = Color(1,0.4,0.4)
|
||||
_spawn_wave(true)
|
||||
if not Engine.is_editor_hint():
|
||||
for i in range(8):
|
||||
var voice := AudioStreamPlayer.new()
|
||||
add_child(voice)
|
||||
voices.append(voice)
|
||||
for name in ["start","fire","dive","explosion","boss_damage","boss_destroy","goei_destroy","zako_destroy","extend","stage_count","tractor_beam","capturing","fighter_captured","fighter_rescued","captured_ship_destroy"]:
|
||||
streams[name] = load("res://games/galaga/audio/%s.wav" % name)
|
||||
|
||||
func start_game() -> bool:
|
||||
if is_running or Engine.is_editor_hint(): return false
|
||||
_clear_actors()
|
||||
score = 0
|
||||
stage = 1
|
||||
lives = clampi(int(run_config.get("starting_lives",starting_lives)),1,9)
|
||||
shots_fired = 0
|
||||
hits = 0
|
||||
awarded = false
|
||||
next_extra_life = extra_life_score
|
||||
clock = 0
|
||||
credits = maxi(0,credits-1)
|
||||
cooldown = 0
|
||||
invulnerable = 0
|
||||
player.position = Vector2(112,256)
|
||||
player.visible = false
|
||||
state = "intro"
|
||||
timer = intro_seconds
|
||||
dual = false
|
||||
wingman.visible = false
|
||||
captive.visible = false
|
||||
captive.spinning = false
|
||||
captor = {}
|
||||
beam = {}
|
||||
capture_cooldown = 8.0
|
||||
_spawn_wave()
|
||||
_sound("start")
|
||||
return super.start_game()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
for voice in voices:
|
||||
voice.stop()
|
||||
voice.stream = null
|
||||
|
||||
func reset_game() -> void:
|
||||
_clear_actors()
|
||||
for voice in voices: voice.stop()
|
||||
state = "idle"
|
||||
dual = false
|
||||
beam = {}
|
||||
captor = {}
|
||||
captive.visible = false
|
||||
wingman.visible = false
|
||||
player.visible = true
|
||||
player.position = Vector2(112,256)
|
||||
_spawn_wave(true)
|
||||
super.reset_game()
|
||||
|
||||
func win_game() -> void:
|
||||
if not is_running or state=="victory": return
|
||||
for voice in voices: voice.stop()
|
||||
state = "victory"
|
||||
timer = 1.5
|
||||
beam = {}
|
||||
|
||||
func lose_game() -> void:
|
||||
if not is_running: return
|
||||
for voice in voices: voice.stop()
|
||||
state = "lost"
|
||||
super.lose_game()
|
||||
|
||||
func _clear_actors() -> void:
|
||||
for collection in [enemies,bullets,effects]:
|
||||
for item in collection: item.node.queue_free()
|
||||
collection.clear()
|
||||
|
||||
func _spawn_wave(preview := false) -> void:
|
||||
dive_timer = 2.0
|
||||
for row in range(5):
|
||||
var count := 4 if row == 0 else (8 if row < 3 else 10)
|
||||
for col in range(count):
|
||||
var node = ENEMY.instantiate()
|
||||
node.kind = "boss" if row == 0 else ("butterfly" if row < 3 else "bee")
|
||||
field.add_child(node)
|
||||
var home := Vector2(112 + (col - (count-1)/2.0)*18,48+row*18)
|
||||
node.position = home if preview else Vector2(-24,-24)
|
||||
node.visible = preview
|
||||
var index := enemies.size()
|
||||
enemies.append({"node":node,"home":home,"mode":"formation" if preview else "enter","time":-(index/8)*1.1-(index%8)*0.075,"side":-1.0 if (index/8)%2==0 else 1.0,"hp":2 if row==0 else 1,"origin":Vector2.ZERO,"target":112.0,"fired":false})
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if Engine.is_editor_hint(): return
|
||||
if event.is_action_pressed("arcade_start"):
|
||||
start_game()
|
||||
|
||||
func _held(action: String) -> bool:
|
||||
return Input.is_action_pressed(action)
|
||||
|
||||
func _physics_process(dt: float) -> void:
|
||||
if Engine.is_editor_hint(): return
|
||||
if state=="won":
|
||||
clock += dt
|
||||
return
|
||||
if not is_running: return
|
||||
_tick(dt)
|
||||
|
||||
func _tick(dt: float) -> void:
|
||||
clock += dt
|
||||
for i in range(effects.size()-1,-1,-1):
|
||||
var effect: Dictionary = effects[i]
|
||||
effect.node.animation_time += dt
|
||||
if effect.node.animation_time >= 0.34:
|
||||
effect.node.queue_free()
|
||||
effects.remove_at(i)
|
||||
if state != "playing":
|
||||
timer -= dt
|
||||
if state=="victory":
|
||||
_update_peaceful_formation()
|
||||
for enemy in enemies:
|
||||
if enemy.mode!="formation" and enemy.node.visible:
|
||||
enemy.node.position += enemy.node.heading*70*dt
|
||||
for i in range(bullets.size()-1,-1,-1):
|
||||
var bullet: Dictionary = bullets[i]
|
||||
bullet.node.position += bullet.velocity*dt
|
||||
if not Rect2(-16,-16,256,320).has_point(bullet.node.position):
|
||||
bullet.node.queue_free()
|
||||
bullets.remove_at(i)
|
||||
if timer<=0:
|
||||
_clear_bullets()
|
||||
state="won"
|
||||
super.win_game()
|
||||
return
|
||||
if state == "capturing":
|
||||
var t := clampf(1-timer/1.5,0,1)
|
||||
player.position = capture_origin.lerp(captor.node.position-Vector2(0,16),t)
|
||||
if timer<=0: _finish_capture()
|
||||
return
|
||||
if state in ["captured_return", "capture_ready", "rescue_spin", "rescue_center", "rescue_align", "rescue_descend"]:
|
||||
_update_peaceful_formation()
|
||||
match state:
|
||||
"captured_return":
|
||||
captor.node.position = captor.node.position.move_toward(_formation_home(captor),100*dt)
|
||||
captive.position = captor.node.position-Vector2(0,16)
|
||||
if timer<=0 and captor.node.position.distance_to(_formation_home(captor))<1:
|
||||
captor.mode="formation"
|
||||
if lives<=0:
|
||||
lose_game()
|
||||
else:
|
||||
player.position=Vector2(112,256)
|
||||
player.visible=true
|
||||
state="capture_ready"
|
||||
timer=1.5
|
||||
"capture_ready":
|
||||
if timer<=0: _resume_action()
|
||||
"rescue_spin":
|
||||
captive.spin_time += dt
|
||||
if timer<=0:
|
||||
captive.spinning=false
|
||||
state="rescue_center"
|
||||
"rescue_center":
|
||||
player.position = player.position.move_toward(Vector2(104,256),100*dt)
|
||||
if player.position.distance_to(Vector2(104,256))<0.1: state="rescue_align"
|
||||
"rescue_align":
|
||||
captive.position.x = move_toward(captive.position.x,120,100*dt)
|
||||
if absf(captive.position.x-120)<0.1: state="rescue_descend"
|
||||
"rescue_descend":
|
||||
captive.position = captive.position.move_toward(Vector2(120,256),110*dt)
|
||||
if captive.position.distance_to(Vector2(120,256))<0.1:
|
||||
dual=true
|
||||
captive.visible=false
|
||||
wingman.position=Vector2(120,256)
|
||||
wingman.visible=true
|
||||
_resume_action()
|
||||
return
|
||||
if timer <= 0:
|
||||
if state == "intro":
|
||||
state = "stage_card"
|
||||
timer = stage_card_seconds
|
||||
_sound("stage_count")
|
||||
elif state == "stage_card":
|
||||
state = "ready"
|
||||
timer = player_card_seconds
|
||||
player.visible = true
|
||||
elif state == "ready": state = "playing"
|
||||
elif state == "death":
|
||||
if lives <= 0: lose_game()
|
||||
else:
|
||||
player.position = Vector2(112,256)
|
||||
player.visible = true
|
||||
invulnerable = 2.5
|
||||
state = "playing"
|
||||
elif state == "clear":
|
||||
stage += 1
|
||||
_spawn_wave()
|
||||
state = "stage_card"
|
||||
player.visible = false
|
||||
wingman.visible = false
|
||||
timer = stage_card_seconds
|
||||
_sound("stage_count")
|
||||
return
|
||||
cooldown = maxf(0,cooldown-dt)
|
||||
invulnerable = maxf(0,invulnerable-dt)
|
||||
player.visible = invulnerable == 0 or int(clock*12)%2==0
|
||||
var movement := float(_held("arcade_right"))-float(_held("arcade_left"))
|
||||
player.position.x = clampf(player.position.x+movement*110*dt,8,216)
|
||||
if dual: player.position.x = minf(player.position.x,200)
|
||||
wingman.position = player.position+Vector2(16,0)
|
||||
wingman.visible = dual and player.visible
|
||||
if _held("arcade_action_1"): _fire()
|
||||
_update_enemies(dt)
|
||||
if state != "playing": return
|
||||
_update_bullets(dt)
|
||||
if state != "playing": return
|
||||
for enemy in enemies:
|
||||
if (enemy.node.position.distance_to(player.position) < 11 or (dual and enemy.node.position.distance_to(wingman.position)<11)) and invulnerable <= 0:
|
||||
_die()
|
||||
return
|
||||
if enemies.is_empty():
|
||||
var target := int(run_config.get("stages_to_win", stages_to_win))
|
||||
if target > 0 and stage >= target: win_game()
|
||||
else:
|
||||
state = "clear"
|
||||
timer = 2
|
||||
_clear_bullets()
|
||||
|
||||
func _update_enemies(dt: float) -> void:
|
||||
dive_timer -= dt
|
||||
capture_cooldown -= dt
|
||||
var entering := false
|
||||
for e in enemies:
|
||||
if e.mode=="enter": entering=true
|
||||
for enemy in enemies:
|
||||
var previous_position: Vector2 = enemy.node.position
|
||||
enemy.time += dt
|
||||
var expansion := 1.0+0.10*(0.5+0.5*sin(clock*1.7))
|
||||
var home: Vector2 = _formation_home(enemy)
|
||||
match enemy.mode:
|
||||
"enter":
|
||||
if enemy.time < 0:
|
||||
enemy.node.visible = false
|
||||
continue
|
||||
enemy.node.visible = true
|
||||
var t := clampf(enemy.time/2.0,0,1)
|
||||
var start := Vector2(112+enemy.side*140,160)
|
||||
if t<0.8:
|
||||
# Every member samples one shared route; its slot affects only docking.
|
||||
var u := t/0.8
|
||||
enemy.node.position = start.lerp(Vector2(112,90),u) + Vector2(-enemy.side*sin(u*TAU)*72,-sin(u*PI)*105)
|
||||
else:
|
||||
enemy.node.position = Vector2(112,90).lerp(home,(t-0.8)/0.2)
|
||||
if t>=1: enemy.mode = "formation"
|
||||
"formation":
|
||||
enemy.node.position = home
|
||||
if not entering and capture_cooldown<=0 and enemy.node.kind=="boss" and captor.is_empty() and beam.is_empty() and not dual:
|
||||
enemy.mode="beam_approach"
|
||||
enemy.origin=home
|
||||
enemy.target=player.position.x
|
||||
enemy.time=0
|
||||
beam=enemy
|
||||
capture_cooldown=15
|
||||
elif dive_timer <= 0 and not entering:
|
||||
enemy.mode = "dive"
|
||||
enemy.time = 0
|
||||
enemy.origin = home
|
||||
enemy.target = player.position.x
|
||||
enemy.fired = false
|
||||
dive_timer = maxf(0.45,2.0-stage*0.12)
|
||||
_sound("dive")
|
||||
"dive":
|
||||
var t: float = enemy.time/(3.0/maxf(1,1+stage*0.04))
|
||||
enemy.node.position = Vector2(lerpf(enemy.origin.x,enemy.target,minf(t,1))+sin(t*TAU)*25,enemy.origin.y+t*250)
|
||||
if t>0.35 and not enemy.fired:
|
||||
enemy.fired = true
|
||||
_bullet(enemy.node.position,(player.position-enemy.node.position).normalized()*95,false)
|
||||
if enemy.node.position.y>310:
|
||||
enemy.mode = "return"
|
||||
enemy.time = 0
|
||||
"return":
|
||||
var t := minf(1,enemy.time/1.0)
|
||||
enemy.node.position = Vector2(home.x,lerpf(-20,home.y,t))
|
||||
if t>=1: enemy.mode = "formation"
|
||||
"beam_approach":
|
||||
var t := minf(enemy.time/1.3,1)
|
||||
enemy.node.position=enemy.origin.lerp(Vector2(enemy.target,172),t)
|
||||
if t>=1:
|
||||
enemy.mode="beam"
|
||||
enemy.time=0
|
||||
_sound("tractor_beam")
|
||||
"beam":
|
||||
if invulnerable<=0 and absf(player.position.x-enemy.node.position.x)<18:
|
||||
captor=enemy
|
||||
capture_origin=player.position
|
||||
state="capturing"
|
||||
timer=1.5
|
||||
_clear_bullets()
|
||||
_stop_sounds()
|
||||
_sound("capturing")
|
||||
return
|
||||
if enemy.time>=3:
|
||||
enemy.mode="return"
|
||||
enemy.time=0
|
||||
beam={}
|
||||
enemy.node.animation_time += dt
|
||||
if enemy.mode in ["dive","enter","return"]:
|
||||
var travel: Vector2 = enemy.node.position-previous_position
|
||||
if travel.length_squared()>0.01: enemy.node.heading = travel.normalized()
|
||||
else: enemy.node.heading = Vector2.DOWN
|
||||
if not captor.is_empty() and captive.visible:
|
||||
captive.position=captor.node.position-Vector2(0,16)
|
||||
|
||||
func _fire() -> void:
|
||||
if cooldown>0: return
|
||||
var count := 0
|
||||
for bullet in bullets:
|
||||
if bullet.friendly: count += 1
|
||||
if count>=(4 if dual else 2): return
|
||||
cooldown = 0.18
|
||||
shots_fired += 1
|
||||
_bullet(player.position-Vector2(0,9),Vector2(0,-230),true)
|
||||
if dual: _bullet(wingman.position-Vector2(0,9),Vector2(0,-230),true)
|
||||
_sound("fire")
|
||||
|
||||
func _bullet(at: Vector2, velocity: Vector2, friendly: bool) -> void:
|
||||
var node = PROJECTILE.instantiate()
|
||||
node.kind = "shot" if friendly else "enemy_shot"
|
||||
node.position = at
|
||||
field.add_child(node)
|
||||
bullets.append({"node":node,"velocity":velocity,"friendly":friendly})
|
||||
|
||||
func _update_bullets(dt: float) -> void:
|
||||
for i in range(bullets.size()-1,-1,-1):
|
||||
var bullet: Dictionary = bullets[i]
|
||||
bullet.node.position += bullet.velocity*dt
|
||||
var remove: bool = bullet.node.position.y < -8 or bullet.node.position.y>296
|
||||
if bullet.friendly:
|
||||
if captive.visible and state=="playing" and bullet.node.position.distance_to(captive.position)<8:
|
||||
captive.visible=false
|
||||
captor={}
|
||||
_sound("captured_ship_destroy")
|
||||
bullet.node.queue_free()
|
||||
bullets.remove_at(i)
|
||||
continue
|
||||
for j in range(enemies.size()-1,-1,-1):
|
||||
var enemy: Dictionary = enemies[j]
|
||||
if bullet.node.position.distance_to(enemy.node.position)>9: continue
|
||||
hits += 1
|
||||
enemy.hp -= 1
|
||||
remove = true
|
||||
if enemy.hp <= 0:
|
||||
var kind: String = enemy.node.kind
|
||||
var points: int = {"boss":150,"butterfly":80,"bee":50}[kind]
|
||||
if enemy.mode == "dive": points *= 2
|
||||
_explode(enemy.node.position,true)
|
||||
if enemy==beam: beam={}
|
||||
if enemy==captor:
|
||||
captor={}
|
||||
state="rescue_spin"
|
||||
timer=1.5
|
||||
captive.spinning=true
|
||||
captive.spin_time=0
|
||||
_stop_sounds()
|
||||
_sound("fighter_rescued")
|
||||
enemy.node.queue_free()
|
||||
enemies.remove_at(j)
|
||||
_sound({"boss":"boss_destroy","butterfly":"goei_destroy","bee":"zako_destroy"}[kind])
|
||||
_add_score(points)
|
||||
else:
|
||||
enemy.node.damaged = true
|
||||
_sound("boss_damage")
|
||||
break
|
||||
elif invulnerable<=0 and (bullet.node.position.distance_to(player.position)<8 or (dual and bullet.node.position.distance_to(wingman.position)<8)):
|
||||
_die()
|
||||
return
|
||||
if remove:
|
||||
bullet.node.queue_free()
|
||||
bullets.remove_at(i)
|
||||
if state != "playing": return
|
||||
|
||||
func _add_score(points: int) -> void:
|
||||
score += points
|
||||
while next_extra_life>0 and score>=next_extra_life:
|
||||
var first := not awarded
|
||||
awarded = true
|
||||
lives += 1
|
||||
_sound("extend")
|
||||
if first and second_extra_life_score>next_extra_life:
|
||||
next_extra_life = second_extra_life_score
|
||||
elif repeat_extra_life_interval>0:
|
||||
next_extra_life += repeat_extra_life_interval
|
||||
else: next_extra_life = 0
|
||||
var target := int(run_config.get("score_to_win",score_to_win))
|
||||
if target>0 and score>=target: win_game()
|
||||
|
||||
func _clear_bullets() -> void:
|
||||
for bullet in bullets: bullet.node.queue_free()
|
||||
bullets.clear()
|
||||
|
||||
func _die() -> void:
|
||||
if dual:
|
||||
dual=false
|
||||
_explode(wingman.position)
|
||||
wingman.visible=false
|
||||
invulnerable=2.5
|
||||
_sound("explosion")
|
||||
return
|
||||
_explode(player.position)
|
||||
player.visible = false
|
||||
lives -= 1
|
||||
state = "death"
|
||||
timer = 1.5
|
||||
_clear_bullets()
|
||||
_sound("explosion")
|
||||
|
||||
func _explode(at: Vector2, enemy := false) -> void:
|
||||
var node = EXPLOSION.instantiate()
|
||||
if enemy: node.kind="enemy_explosion"
|
||||
node.position = at
|
||||
field.add_child(node)
|
||||
effects.append({"node":node})
|
||||
|
||||
func _finish_capture() -> void:
|
||||
beam={}
|
||||
lives-=1
|
||||
captive.visible=true
|
||||
captive.position=player.position
|
||||
player.visible=false
|
||||
captor.mode="captured_return"
|
||||
captor.time=0
|
||||
_stop_sounds()
|
||||
state="captured_return"
|
||||
_sound("fighter_captured")
|
||||
timer=1.5
|
||||
|
||||
func _sound(name: String) -> void:
|
||||
if state=="capturing" and name!="capturing": return
|
||||
if state.begins_with("rescue_") and name!="fighter_rescued": return
|
||||
if not streams.has(name): return
|
||||
for voice in voices:
|
||||
if not voice.playing:
|
||||
voice.stream = streams[name]
|
||||
voice.play()
|
||||
return
|
||||
|
||||
func _stop_sounds() -> void:
|
||||
for voice in voices: voice.stop()
|
||||
|
||||
func _formation_home(enemy: Dictionary) -> Vector2:
|
||||
var expansion := 1.0+0.10*(0.5+0.5*sin(clock*1.7))
|
||||
return Vector2(112,48)+(enemy.home-Vector2(112,48))*expansion
|
||||
|
||||
func _update_peaceful_formation() -> void:
|
||||
for enemy in enemies:
|
||||
if enemy.mode=="formation": enemy.node.position=_formation_home(enemy)
|
||||
|
||||
func _resume_action() -> void:
|
||||
state="playing"
|
||||
invulnerable=2.5
|
||||
dive_timer=2.0
|
||||
_clear_bullets()
|
||||
Reference in new issue
Block a user