Added Glitch Room Source code to Gitty

This commit is contained in:
Jeff Luke committed 2026-10-04 11:31:17 -07:00
commit 1ac9eb5644
505 files changed
+28504

No files matched your search

+402
View File
@@ -0,0 +1,402 @@
@tool
extends ArcadeGame
@export_range(0, 99) var waves_to_win: int = 1
@export_range(1, 6) var starting_lives: int = 3
@export_range(0, 10000, 100) var bonus_life_score: int = 1500
@export var color_overlay: bool = true
@export var sound_enabled: bool = true
const SAUCER_SCORES := [100,50,50,100,150,100,100,50,300,100,100,100,50,150,100]
const WAVE_HEIGHTS := [96,104,112,112,112,120,120,120]
var phase := "attract"
var phase_time := 0.0
var clock := 0.0
var demo := false
var demo_time := 0.0
var score := 0
var high_score := 0
var last_score := 0
var lives := 3
var wave := 1
var extra_awarded := false
var player_x := 24.0
var player_shot := Vector2(-100,-100)
var shot_active := false
var shot_count := 0
var shot_delay := 0.0
var aliens: Array[Dictionary] = []
var bombs: Array[Dictionary] = []
var effects: Array[Dictionary] = []
var shields: Array[Dictionary] = []
var fleet_direction := 1
var fleet_cursor := 0
var fleet_delta := Vector2(2,0)
var fleet_ticks := 0.0
var march_note := 0
var march_sound_delay := 0.0
var hit_pause := 0.0
var bomb_timer := 1.0
var bomb_kind := 0
var bomb_column := 0
var saucer_x := -32.0
var saucer_direction := 1
var saucer_active := false
var saucer_timer := 20.0
var demo_target := 112.0
@onready var board: Node2D = $PixelScreen/Board
func _ready() -> void:
new_wave()
lives = starting_lives
func new_wave() -> void:
aliens.clear()
var top: int = 72 if wave == 1 else WAVE_HEIGHTS[(wave-2)%WAVE_HEIGHTS.size()]
# Bottom-to-top scan order gives the original rippling fleet movement.
for row in range(4,-1,-1):
for column in range(11):
aliens.append({"position":Vector2(24+column*16,top+row*16),"kind":0 if row==0 else (1 if row<3 else 2),"frame":0,"alive":true,"column":column})
fleet_direction = 1
fleet_cursor = 0
fleet_ticks = 0
march_sound_delay = 0
march_note = 0
fleet_delta = Vector2(2,0)
bombs.clear()
effects.clear()
shot_active = false
shot_delay = 0
bomb_timer = 1
saucer_active = false
saucer_timer = 20
hit_pause = 0
player_x = 24
build_shields()
func build_shields() -> void:
shields.clear()
var pattern: Image = board.masks["shield"]
for x in [32,77,122,167]:
var pixels := pattern.duplicate() as Image
shields.append({"position":Vector2i(x,192),"image":pixels,"texture":ImageTexture.create_from_image(pixels),"dirty":false})
func start_game() -> bool:
if is_running: return false
demo = false
$Audio.stop_all()
score = 0
lives = starting_lives
wave = 1
shot_count = 0
extra_awarded = false
clock = 0
new_wave()
phase = "ready"
phase_time = 2
return super.start_game()
func reset_game() -> void:
demo = false
$Audio.stop_all()
score = 0
last_score = 0
wave = 1
lives = starting_lives
new_wave()
phase = "attract"
phase_time = 0
clock = 0
super.reset_game()
func begin_demo() -> void:
demo = true
demo_time = 0
score = 0
lives = starting_lives
wave = 1
shot_count = 0
new_wave()
phase = "play"
func end_demo() -> void:
demo = false
score = last_score
phase = "attract"
phase_time = 0
$Audio.stop_all()
func _unhandled_input(event: InputEvent) -> void:
if Engine.is_editor_hint() or event.is_echo(): return
if event.is_action_pressed("arcade_start"):
start_game()
func held(action: StringName) -> bool:
return Input.is_action_pressed(action)
func _physics_process(delta: float) -> void:
clock += delta
if Engine.is_editor_hint():
phase_time = fmod(clock,22)
board.queue_redraw()
return
if phase == "attract":
phase_time += delta
if phase_time >= 28: begin_demo()
elif phase == "ready":
phase_time -= delta
if phase_time <= 0:
phase = "play"
if saucer_active: $Audio.ufo(true)
elif phase == "lost":
phase_time -= delta
if phase_time <= 0:
phase = "attract"
phase_time = 0
elif phase == "dead":
phase_time -= delta
if phase_time <= 0:
if lives <= 0: finish(false)
else:
player_x = 24
bombs.clear()
shot_active = false
phase = "ready"
phase_time = 1.0
elif phase == "clear":
phase_time -= delta
if phase_time <= 0:
if demo: end_demo()
elif waves_to_win > 0 and wave >= waves_to_win: finish(true)
else:
wave += 1
new_wave()
phase = "ready"
phase_time = 1.5
elif phase == "play":
var move := 0.0
if demo:
demo_target = 112+sin(demo_time*.7)*88
move = signf(demo_target-player_x) if absf(demo_target-player_x)>1 else 0.0
else:
move = float(held(&"arcade_right"))-float(held(&"arcade_left"))
player_x = clampf(player_x+move*60*delta,16,208)
shot_delay = maxf(0,shot_delay-delta)
if demo or held(&"arcade_action_1"): fire()
step_world(delta)
if demo:
demo_time += delta
if demo_time >= 25: end_demo()
for shield in shields:
if shield.dirty:
shield.texture.update(shield.image)
shield.dirty = false
board.queue_redraw()
func fire() -> bool:
if phase != "play" or shot_active or shot_delay > 0: return false
shot_active = true
player_shot = Vector2(roundf(player_x),216)
$Audio.effect("shot")
return true
func remove_shot() -> void:
if not shot_active: return
shot_active = false
shot_count += 1
shot_delay = .08
func alive_count() -> int:
var count := 0
for alien in aliens:
if alien.alive: count += 1
return count
func march() -> void:
var count := alive_count()
if count == 0: return
if fleet_cursor == 0:
var edge := false
for alien in aliens:
if alien.alive and (alien.position.x+fleet_direction*2 > 208 or alien.position.x+fleet_direction*2 < 16): edge = true
if edge:
fleet_direction *= -1
fleet_delta = Vector2(0,8)
else: fleet_delta = Vector2(fleet_direction*(3 if count==1 else 2),0)
if march_sound_delay <= 0:
$Audio.effect("march%d"%march_note)
march_note = (march_note+1)%4
march_sound_delay = .08
while fleet_cursor < aliens.size() and not aliens[fleet_cursor].alive: fleet_cursor += 1
if fleet_cursor < aliens.size():
var alien: Dictionary = aliens[fleet_cursor]
alien.position += fleet_delta
alien.frame = 1-alien.frame
var bounds := Rect2(alien.position-Vector2(8,4),Vector2(16,8))
for shield in shields:
if bounds.intersects(Rect2(Vector2(shield.position),Vector2(shield.image.get_size()))):
for y in range(shield.image.get_height()):
for x in range(shield.image.get_width()):
if bounds.has_point(Vector2(shield.position+Vector2i(x,y))): shield.image.set_pixel(x,y,Color.TRANSPARENT)
shield.dirty = true
if alien.position.y >= 216: finish(false)
fleet_cursor += 1
while fleet_cursor < aliens.size() and not aliens[fleet_cursor].alive: fleet_cursor += 1
if fleet_cursor >= aliens.size(): fleet_cursor = 0
func launch_bomb() -> void:
var kind := bomb_kind
bomb_kind = (bomb_kind+1)%3
for bomb in bombs:
if bomb.kind == kind: return
if kind == 1 and alive_count() == 1: return
var candidates: Array[Dictionary] = []
var columns: Dictionary = {}
for alien in aliens:
if alien.alive and not columns.has(alien.column):
columns[alien.column] = true
candidates.append(alien)
if candidates.is_empty(): return
var source: Dictionary = candidates[bomb_column%candidates.size()]
bomb_column += 3
if kind == 0:
for alien in candidates:
if absf(alien.position.x-player_x) < absf(source.position.x-player_x): source = alien
bombs.append({"position":source.position+Vector2(0,8),"kind":kind})
func pixel_hit(name: String, origin: Vector2, point: Vector2) -> bool:
var image: Image = board.masks[name]
var p := Vector2i(point.round()-origin.round())
return p.x>=0 and p.y>=0 and p.x<image.get_width() and p.y<image.get_height() and image.get_pixelv(p).a>.5
func shield_hit(point: Vector2, upward: bool) -> bool:
for shield in shields:
var p := Vector2i(point.round())-Vector2i(shield.position)
var image: Image = shield.image
if p.x<0 or p.y<0 or p.x>=image.get_width() or p.y>=image.get_height(): continue
if image.get_pixelv(p).a < .5: continue
for dy in range(-4,5):
for dx in range(-3,4):
var q := p+Vector2i(dx,dy)
if q.x<0 or q.y<0 or q.x>=image.get_width() or q.y>=image.get_height(): continue
var depth := -dy if upward else dy
if dx*dx+dy*dy<=12 and depth>=-1 and (absi(dx)<2 or (q.x+q.y)%3!=0): image.set_pixelv(q,Color.TRANSPARENT)
shield.dirty = true
return true
return false
func blast(position: Vector2, name: String = "burst", duration: float = .16, text: String = "") -> void:
effects.append({"position":position,"name":name,"time":duration,"text":text})
func step_world(delta: float) -> void:
march_sound_delay = maxf(0,march_sound_delay-delta)
for i in range(effects.size()-1,-1,-1):
effects[i].time -= delta
if effects[i].time <= 0: effects.remove_at(i)
hit_pause = maxf(0,hit_pause-delta)
if hit_pause == 0:
fleet_ticks += delta*60
while fleet_ticks >= 1 and phase == "play":
fleet_ticks -= 1
march()
if phase != "play": return
bomb_timer -= delta
if bomb_timer <= 0:
launch_bomb()
bomb_timer = maxf(.25,.75-score/20000.0)
saucer_timer -= delta
if saucer_timer <= 0 and not saucer_active:
saucer_active = true
saucer_direction = 1 if shot_count%2==0 else -1
saucer_x = -12 if saucer_direction==1 else 236
$Audio.ufo(true)
if saucer_active:
saucer_x += saucer_direction*30*delta
if saucer_x < -16 or saucer_x > 240:
saucer_active = false
saucer_timer = 23
$Audio.ufo(false)
# One-pixel substeps prevent fast projectiles tunneling through shields/sprites.
if shot_active:
var travel := delta*240
while travel > 0 and shot_active:
var amount := minf(1,travel)
player_shot.y -= amount
travel -= amount
if shield_hit(player_shot,true): remove_shot()
elif player_shot.y <= 32:
blast(player_shot,"shot_burst")
remove_shot()
elif saucer_active and pixel_hit("ufo",Vector2(saucer_x-8,40),player_shot):
var value: int = SAUCER_SCORES[shot_count%15]
add_score(value)
blast(Vector2(saucer_x,44),"ufo_burst",.65,str(value))
saucer_active = false
saucer_timer = 23
$Audio.ufo(false)
$Audio.effect("ufo_hit")
remove_shot()
else:
for alien in aliens:
if alien.alive and pixel_hit("alien%d_%d"%[alien.kind,alien.frame],alien.position-Vector2(8,4),player_shot):
alien.alive = false
add_score([30,20,10][alien.kind])
blast(alien.position)
hit_pause = .10
$Audio.effect("invader")
remove_shot()
break
for i in range(bombs.size()-1,-1,-1):
var bomb: Dictionary = bombs[i]
var destroyed := false
var travel := delta*(90 if alive_count()<9 else 60)
while travel>0 and not destroyed:
var amount := minf(1,travel)
bomb.position.y += amount
travel -= amount
if shield_hit(bomb.position+Vector2(0,3),false): destroyed = true
elif shot_active and absf(bomb.position.x-player_shot.x)<3 and absf(bomb.position.y-player_shot.y)<7:
destroyed = true
remove_shot()
blast(bomb.position,"shot_burst")
elif pixel_hit("player",Vector2(roundf(player_x)-8,216),bomb.position+Vector2(0,3)):
die()
destroyed = true
elif bomb.position.y>=238:
destroyed = true
blast(Vector2(bomb.position.x,237),"shot_burst")
if destroyed: bombs.remove_at(i)
if phase != "play": return
if alive_count()==0:
phase = "clear"
phase_time = 1.5
$Audio.stop_all()
func add_score(value: int) -> void:
score += value
if demo: return
high_score = maxi(high_score,score)
if bonus_life_score>0 and score>=bonus_life_score and not extra_awarded:
extra_awarded = true
lives += 1
$Audio.effect("bonus")
func die() -> void:
if phase != "play": return
lives -= 1
phase = "dead"
phase_time = 1.5
$Audio.stop_all()
$Audio.effect("death")
func finish(won: bool) -> void:
if demo:
end_demo()
return
if not is_running: return
last_score = score
phase = "won" if won else "lost"
phase_time = 5
$Audio.stop_all()
if won: win_game()
else: lose_game()