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

+14
View File
@@ -0,0 +1,14 @@
[gd_scene format=3 uid="uid://bochyg70b5eig"]
[ext_resource type="Script" uid="uid://d00n1vs51ma48" path="res://games/tempest/tempest.gd" id="1"]
[ext_resource type="Script" uid="uid://b150rg646gh6x" path="res://games/tempest/vector_board.gd" id="2"]
[ext_resource type="Script" uid="uid://bd2kidcvk2qgy" path="res://games/tempest/vector_audio.gd" id="3"]
[node name="Tempest" type="Node" unique_id=125397583]
script = ExtResource("1")
[node name="VectorBoard" type="Node2D" parent="." unique_id=156840381]
script = ExtResource("2")
[node name="VectorAudio" type="Node" parent="." unique_id=536021095]
script = ExtResource("3")
+361
View File
@@ -0,0 +1,361 @@
@tool
extends ArcadeGame
@export_range(1, 9) var starting_lives: int = 3
## Number of tubes to clear from the chosen starting level; zero means endless.
@export_range(0, 99) var stages_to_win: int = 3
@export_range(0.5, 2.0) var difficulty: float = 1.0
@export_range(0.5, 2.5) var glow_strength: float = 1.35
@export var show_level_select: bool = true
const LANES := 16
const START_LEVELS := [1, 3, 5, 7, 9]
const START_BONUSES := [0, 6000, 16000, 32000, 54000]
var lane: int = 0
var stage: int = 1
var score: int = 0
var high_score: int = 0
var lives: int = 3
var phase: String = "title"
var phase_time: float = 0.0
var clock: float = 0.0
var attract_clock: float = 0.0
var selection: int = 0
var cleared: int = 0
var pending_bonus: int = 0
var awarded_bonus: int = 0
var next_extra_life: int = 20000
var enemies: Array[Dictionary] = []
var shots: Array[Dictionary] = []
var sparks: Array[Dictionary] = []
var spikes: Dictionary = {}
var remaining: int = 0
var spawn_timer: float = 0.0
var fire_timer: float = 0.0
var move_timer: float = 0.0
var zap_available: bool = true
var zap_uses: int = 0
var zap_flash: float = 0.0
var rng := RandomNumberGenerator.new()
@onready var view: Node2D = $VectorBoard
func _ready() -> void:
if not Engine.is_editor_hint():
rng.randomize()
lives = starting_lives
func start_game() -> bool:
if not super.start_game():
return false
stage = 1
score = 0
cleared = 0
pending_bonus = 0
awarded_bonus = 0
selection = 0
next_extra_life = 20000
lives = starting_lives
lane = 0
enemies.clear()
shots.clear()
sparks.clear()
spikes.clear()
zap_flash = 0.0
move_timer = 0.0
phase = "select" if show_level_select else "ready"
phase_time = 10.0
if not show_level_select:
_begin_stage()
return true
func confirm_level() -> void:
if phase != "select":
return
stage = START_LEVELS[selection]
pending_bonus = START_BONUSES[selection]
_begin_stage()
func reset_game() -> void:
super.reset_game()
$VectorAudio.stop()
phase = "title"
attract_clock = 0.0
enemies.clear()
shots.clear()
sparks.clear()
spikes.clear()
stage = 1
score = 0
lives = starting_lives
zap_uses = 0
zap_available = true
zap_flash = 0.0
pending_bonus = 0
awarded_bonus = 0
cleared = 0
func _begin_stage() -> void:
move_timer = 0.0
fire_timer = 0.15
zap_flash = 0.0
enemies.clear()
shots.clear()
spikes.clear()
remaining = 10 + stage * 4
spawn_timer = 0.5
zap_uses = 0
zap_available = true
phase = "ready"
phase_time = 1.4
func _pressed(action: String) -> bool:
return Input.is_action_pressed(action)
func _event_action(event: InputEvent, action: String) -> bool:
return event.is_action_pressed(action)
## Horizontal trackball travel per lane, measured at the game's logical width.
@export_range(1.0, 80.0) var trackball_pixels_per_lane: float = 12.0
var trackball_remainder := 0.0
func _unhandled_input(event: InputEvent) -> void:
if Engine.is_editor_hint() or (event is InputEventKey and event.echo):
return
if event is InputEventMouseMotion:
trackball_remainder += event.relative.x * 640.0 / maxf(1.0, get_viewport().get_visible_rect().size.x)
var steps := int(trackball_remainder / trackball_pixels_per_lane)
if steps != 0:
rotate_steps(steps)
trackball_remainder -= steps * trackball_pixels_per_lane
if _event_action(event, "arcade_start"):
if not is_running:
start_game()
elif phase == "select":
confirm_level()
elif _event_action(event, "arcade_action_1") and phase == "select":
confirm_level()
elif _event_action(event, "arcade_action_2"):
superzap()
if event.is_action_pressed("arcade_rotate_right"):
rotate_steps(1)
elif event.is_action_pressed("arcade_rotate_left"):
rotate_steps(-1)
func rotate_steps(steps: int) -> void:
if phase == "select":
selection = clampi(selection + steps, 0, START_LEVELS.size() - 1)
elif phase in ["play", "dive"]:
lane = bounded_lane(lane + steps)
func is_open_tube() -> bool:
return (stage - 1) % 9 == 8
func bounded_lane(value: int) -> int:
return clampi(value, 0, LANES - 1) if is_open_tube() else posmod(value, LANES)
func superzap() -> void:
if phase != "play" or zap_uses >= 2 or zap_flash > 0:
return
zap_flash = 0.35
$VectorAudio.play("zap")
if zap_uses == 0:
for enemy in enemies:
_destroy(enemy, false)
enemies.clear()
elif not enemies.is_empty():
var index := rng.randi_range(0, enemies.size() - 1)
_destroy(enemies[index], false)
enemies.remove_at(index)
zap_uses += 1
zap_available = zap_uses < 2
func _physics_process(delta: float) -> void:
clock += delta
if phase == "title":
attract_clock += delta
zap_flash = maxf(0.0, zap_flash - delta)
for spark in sparks:
spark.life -= delta
sparks = sparks.filter(func(s: Dictionary) -> bool: return s.life > 0.0)
if Engine.is_editor_hint():
view.queue_redraw()
return
if phase in ["play", "select", "dive"]:
move_timer -= delta
var direction := int(_pressed("arcade_right")) - int(_pressed("arcade_left"))
if direction == 0:
move_timer = 0.0
elif move_timer <= 0.0:
rotate_steps(direction)
move_timer = 0.18 if phase == "select" else 0.075
if phase == "play":
fire_timer -= delta
if _pressed("arcade_action_1"):
fire()
_step_play(delta)
elif phase in ["select", "ready", "death", "dive", "space", "arrive", "victory"]:
phase_time -= delta
if phase == "dive" and spikes.get(lane, 0.0) > 0.0 and spikes.get(lane, 0.0) >= dive_depth():
_die()
elif phase_time <= 0.0:
match phase:
"select": confirm_level()
"ready": phase = "play"
"death":
if lives <= 0:
phase = "lost"
lose_game()
else:
enemies.clear()
shots.clear()
spikes.clear()
phase = "ready"
phase_time = 1.4
"dive":
phase = "space"
phase_time = 1.2
"space":
if stages_to_win > 0 and cleared >= stages_to_win:
phase = "victory"
phase_time = 1.0
else:
stage += 1
phase = "arrive"
phase_time = 1.6
"arrive": _begin_stage()
"victory":
phase = "won"
win_game()
view.queue_redraw()
func dive_depth() -> float:
return clampf(phase_time / 1.6, 0.0, 1.0)
func fire() -> void:
if phase != "play" or fire_timer > 0.0:
return
shots.append({"lane": lane, "depth": 1.0})
fire_timer = 0.11
$VectorAudio.play("fire")
func make_enemy(kind: String, at_lane: int, depth: float = 0.0) -> Dictionary:
return {"lane": at_lane, "depth": depth, "kind": kind, "hp": 1, "flip": rng.randf_range(0.7, 1.5), "rim": 0.3, "roll": 0.0, "from_lane": at_lane, "direction": 1, "retreat": false}
func _step_play(delta: float) -> void:
spawn_timer -= delta
if remaining > 0 and spawn_timer <= 0:
var kind := "flipper"
if stage >= 3 and remaining % 5 == 0:
kind = "tanker"
elif stage >= 4 and remaining % 4 == 0:
kind = "spiker"
enemies.append(make_enemy(kind, rng.randi_range(0, LANES - 1)))
remaining -= 1
spawn_timer = maxf(0.28, 0.95 - stage * 0.045) / difficulty
var newborn: Array[Dictionary] = []
for shot in shots:
var old_depth: float = shot.depth
shot.depth -= delta * 2.3
var hit: Dictionary = {}
for enemy in enemies:
if enemy.hp > 0 and collision_lane(enemy) == shot.lane and enemy.depth <= old_depth + 0.055 and enemy.depth >= shot.depth - 0.055:
if hit.is_empty() or enemy.depth > hit.depth:
hit = enemy
var spike_tip: float = spikes.get(int(shot.lane), 0.0)
if spike_tip > 0 and old_depth >= spike_tip and shot.depth <= spike_tip and (hit.is_empty() or spike_tip > hit.depth):
spikes[int(shot.lane)] = maxf(0.0, spike_tip - 0.10)
_add_score(1)
shot.depth = -1.0
elif not hit.is_empty():
hit.hp -= 1
shot.depth = -1.0
if hit.hp <= 0:
_destroy(hit)
if hit.kind == "tanker":
for offset in [-1, 1]:
newborn.append(make_enemy("flipper", bounded_lane(int(hit.lane) + offset), hit.depth))
shots = shots.filter(func(s: Dictionary) -> bool: return s.depth >= 0.0)
enemies = enemies.filter(func(e: Dictionary) -> bool: return e.hp > 0)
enemies.append_array(newborn)
newborn.clear()
for enemy in enemies:
enemy.roll = maxf(0.0, float(enemy.get("roll", 0.0)) - delta)
var speed := (0.10 + stage * 0.012) * difficulty
if enemy.kind == "spiker":
if enemy.retreat:
enemy.depth -= delta * speed * 1.8
if enemy.depth <= 0:
enemy.hp = 0
else:
enemy.depth += delta * speed
spikes[int(enemy.lane)] = maxf(spikes.get(int(enemy.lane), 0.0), enemy.depth)
if enemy.depth > 0.65:
enemy.retreat = true
continue
enemy.depth = minf(1.0, enemy.depth + delta * speed)
if enemy.kind == "tanker" and enemy.depth > 0.75:
enemy.hp = 0
for offset in [-1, 1]:
newborn.append(make_enemy("flipper", bounded_lane(int(enemy.lane) + offset), enemy.depth))
continue
if enemy.depth >= 1.0:
enemy.rim -= delta
if enemy.rim <= 0 and enemy.roll <= 0:
var difference := lane - int(enemy.lane) if is_open_tube() else posmod(lane - int(enemy.lane), LANES)
if difference != 0:
_start_roll(enemy, (1 if difference > 0 else -1) if is_open_tube() else (1 if difference <= LANES / 2 else -1))
enemy.rim = 0.32
if collision_lane(enemy) == lane:
_die()
return
elif enemy.kind == "flipper" and enemy.depth > 0.28 and stage >= 2:
enemy.flip -= delta
if enemy.flip <= 0 and enemy.roll <= 0:
_start_roll(enemy, 1 if rng.randf() > 0.5 else -1)
enemy.flip = maxf(0.4, 1.4 - stage * 0.05)
enemies.append_array(newborn)
enemies = enemies.filter(func(e: Dictionary) -> bool: return e.hp > 0)
if remaining == 0 and enemies.is_empty():
cleared += 1
awarded_bonus = pending_bonus
_add_score(pending_bonus)
pending_bonus = 0
phase = "dive"
phase_time = 1.6
$VectorAudio.play("level")
shots.clear()
func _start_roll(enemy: Dictionary, direction: int) -> void:
enemy.from_lane = enemy.lane
enemy.direction = direction
if is_open_tube() and (int(enemy.lane) + direction < 0 or int(enemy.lane) + direction >= LANES):
return
enemy.lane = bounded_lane(int(enemy.lane) + direction)
enemy.roll = 0.24
func collision_lane(enemy: Dictionary) -> int:
return int(enemy.get("from_lane", enemy.lane)) if float(enemy.get("roll", 0.0)) > 0.12 else int(enemy.lane)
func _die() -> void:
lives -= 1
$VectorAudio.play("death")
sparks.append({"lane": lane, "depth": 1.0, "life": 0.7})
if phase == "dive":
# A failed warp resumes the cleared tube without counting it twice.
cleared -= 1
phase = "death"
phase_time = 1.2
func _add_score(points: int) -> void:
score += points
high_score = maxi(high_score, score)
while score >= next_extra_life:
lives += 1
next_extra_life += 20000
func _destroy(enemy: Dictionary, _split: bool = true) -> void:
if zap_flash <= 0.0:
$VectorAudio.play("hit")
_add_score({"tanker": 100, "spiker": 50}.get(enemy.kind, 150))
sparks.append({"lane": enemy.lane, "depth": enemy.depth, "life": 0.45})
+1
View File
@@ -0,0 +1 @@
uid://d00n1vs51ma48
+42
View File
@@ -0,0 +1,42 @@
extends Node
## Small synthesized arcade effects; no external audio assets required.
var voices: Array[AudioStreamPlayer] = []
var sounds: Dictionary = {}
var voice_index := 0
func _ready() -> void:
for i in range(6):
var voice := AudioStreamPlayer.new()
voice.volume_db = -17.0
add_child(voice)
voices.append(voice)
sounds["fire"] = tone(1300.0, 220.0, 0.09)
sounds["hit"] = tone(170.0, 40.0, 0.18)
sounds["zap"] = tone(1700.0, 70.0, 0.55)
sounds["death"] = tone(440.0, 35.0, 0.7)
sounds["level"] = tone(220.0, 880.0, 0.4)
func tone(start: float, finish: float, duration: float) -> AudioStreamWAV:
var samples := PackedByteArray()
var count := int(22050 * duration)
samples.resize(count * 2)
var phase := 0.0
for i in range(count):
var t := float(i) / count
phase += lerpf(start, finish, t) * TAU / 22050.0
var envelope := minf(t * 50.0, 1.0) * pow(1.0 - t, 1.5)
var value := int((sin(phase) + sin(phase * 2.0) * 0.25) * envelope * 18000)
samples.encode_s16(i * 2, value)
var stream := AudioStreamWAV.new()
stream.format = AudioStreamWAV.FORMAT_16_BITS
stream.mix_rate = 22050
stream.data = samples
return stream
func play(effect: String) -> void:
if not sounds.has(effect) or voices.is_empty():
return
var voice := voices[voice_index]
voice_index = (voice_index + 1) % voices.size()
voice.stream = sounds[effect]
voice.play()
func stop() -> void:
for voice in voices:
voice.stop()
+1
View File
@@ -0,0 +1 @@
uid://bd2kidcvk2qgy
+344
View File
@@ -0,0 +1,344 @@
@tool
extends Node2D
const SIZE := Vector2(640, 800)
const VANISH := Vector2(320, 535)
const GREEN := Color("30ff35")
const YELLOW := Color("fff633")
const BLUE := Color("3044ff")
const CYAN := Color("24edff")
# Stroke alphabet: XY pairs on a 4 x 6 grid. Every glyph is drawn as vectors.
const GLYPHS := {
"0":"00 40 46 06 00", "1":"10 20 26;06 46", "2":"00 40 43 03 06 46", "3":"00 40 46 06;03 43", "4":"00 03 43;40 46", "5":"40 00 03 43 46 06", "6":"40 00 06 46 43 03", "7":"00 40 16", "8":"00 40 46 06 00;03 43", "9":"46 40 00 03 43",
"A":"06 20 46;14 34", "B":"00 30 41 42 33 03;33 44 45 36 06 00", "C":"40 00 06 46", "D":"00 30 42 44 36 06 00", "E":"40 00 06 46;03 33", "F":"40 00 06;03 33", "G":"40 00 06 46 43 23", "H":"00 06;40 46;03 43", "I":"00 40;20 26;06 46", "J":"00 40 46 06 04", "K":"00 06;40 03 46", "L":"00 06 46", "M":"06 00 23 40 46", "N":"06 00 46 40", "O":"00 40 46 06 00", "P":"06 00 40 43 03", "Q":"00 40 46 06 00;24 46", "R":"06 00 40 43 03;23 46", "S":"40 00 03 43 46 06", "T":"00 40;20 26", "U":"00 06 46 40", "V":"00 26 40", "W":"00 06 23 46 40", "X":"00 46;40 06", "Y":"00 23 40;23 26", "Z":"00 40 06 46", "!":"20 24;25 26", "-":"03 43", "/":"06 40", ":":"21 22;24 25", ".":"25 26", "<":"40 03 46", ">":"00 43 06"
}
# Stroke coordinates traced from the supplied logo reference. Shared baselines
# and open strokes are intentional; these are not outlined block letters.
const LOGO_STROKES := [
[Vector2(87,61),Vector2(205,61)], [Vector2(147,61),Vector2(147,153)],
[Vector2(272,61),Vector2(216,61),Vector2(186,153),Vector2(305,153)],
[Vector2(201,107),Vector2(280,107)],
[Vector2(284,153),Vector2(320,61),Vector2(330,61),Vector2(354,121),Vector2(377,61),Vector2(389,61),Vector2(422,153)],
[Vector2(400,153),Vector2(436,153)],
[Vector2(428,153),Vector2(428,61),Vector2(491,61),Vector2(512,111),Vector2(428,111)],
[Vector2(606,61),Vector2(550,61),Vector2(520,153),Vector2(620,153)],
[Vector2(535,107),Vector2(615,107)],
[Vector2(632,125),Vector2(619,153),Vector2(724,153),Vector2(724,112),Vector2(644,89),Vector2(655,61),Vector2(728,61),Vector2(717,83)],
[Vector2(728,61),Vector2(846,61)], [Vector2(787,61),Vector2(787,153)]
]
var game: Node
var beam_texture: ImageTexture
var beam_mesh := ArrayMesh.new()
var beam_vertices := PackedVector3Array()
var beam_uvs := PackedVector2Array()
var beam_colors := PackedColorArray()
var beam_indices := PackedInt32Array()
var glyph_cache: Dictionary = {}
func _make_beam_texture() -> void:
# Build the phosphor falloff once. UVs stretch its central column along a
# segment and use its two halves as rounded end caps.
var image := Image.create(128,128,false,Image.FORMAT_RGBA8)
for y in range(128):
for x in range(128):
var r := Vector2((x-63.5)/63.5,(y-63.5)/63.5).length()
var alpha := (.07*exp(-r*r*3.0)+.16*exp(-r*r*15.0)+.75*exp(-r*r*200.0)) * (1.0-smoothstep(.85,1.0,r))
image.set_pixel(x,y,Color(1,1,1,alpha))
beam_texture = ImageTexture.create_from_image(image)
for character in GLYPHS:
var strokes: Array[PackedVector2Array] = []
for stroke in GLYPHS[character].split(";"):
var points := PackedVector2Array()
for pair in stroke.split(" "):
points.append(Vector2(float(pair[0]),float(pair[1])))
strokes.append(points)
glyph_cache[character] = strokes
func _beam_quad(a: Vector2, b: Vector2, normal: Vector2, u0: float, u1: float, tint: Color) -> void:
var base := beam_vertices.size()
for point in [a-normal,b-normal,b+normal,a+normal]:
beam_vertices.append(Vector3(point.x,point.y,0))
beam_colors.append(tint)
beam_uvs.append_array(PackedVector2Array([Vector2(u0,0),Vector2(u1,0),Vector2(u1,1),Vector2(u0,1)]))
beam_indices.append_array(PackedInt32Array([base,base+1,base+2,base,base+2,base+3]))
func _flush_beams() -> void:
beam_mesh.clear_surfaces()
if beam_vertices.is_empty():
return
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = beam_vertices
arrays[Mesh.ARRAY_TEX_UV] = beam_uvs
arrays[Mesh.ARRAY_COLOR] = beam_colors
arrays[Mesh.ARRAY_INDEX] = beam_indices
beam_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES,arrays)
draw_mesh(beam_mesh,beam_texture)
func _ready() -> void:
game = get_parent()
_make_beam_texture()
func _process(_delta: float) -> void:
if not Engine.is_editor_hint():
var available := get_viewport_rect().size
scale = Vector2.ONE * minf(available.x / SIZE.x, available.y / SIZE.y)
position = (available - SIZE * scale) * 0.5
queue_redraw()
func profile(index: float, level: int) -> Vector2:
# Hand-authored outlines; level nine is an open V with seventeen vertices.
if (level - 1) % 9 == 8:
# The arcade-like open V has a short flat bottom, not a collapsed point.
if index <= 7.0:
return Vector2(-1,-1).lerp(Vector2(-.14,1),index/7.0)
if index <= 9.0:
return Vector2(-.14,1).lerp(Vector2(.14,1),(index-7.0)/2.0)
return Vector2(.14,1).lerp(Vector2(1,-1),(index-9.0)/7.0)
var angle := index * TAU / 16.0 - PI / 2.0
var p := Vector2(cos(angle), sin(angle))
match (level - 1) % 9:
1: p /= maxf(absf(p.x), absf(p.y))
2:
var cross := [Vector2(0,-1),Vector2(.5,-1),Vector2(.5,-.5),Vector2(1,-.5),Vector2(1,0),Vector2(1,.5),Vector2(.5,.5),Vector2(.5,1),Vector2(0,1),Vector2(-.5,1),Vector2(-.5,.5),Vector2(-1,.5),Vector2(-1,0),Vector2(-1,-.5),Vector2(-.5,-.5),Vector2(-.5,-1)]
var i := int(floor(index))
p = cross[posmod(i,16)].lerp(cross[posmod(i+1,16)], fposmod(index,1.0))
3: p *= Vector2(1.0, 0.72)
4: p *= 0.76 + 0.24 * cos(angle * 4.0)
5: p /= absf(p.x) + absf(p.y)
6: p *= 0.73 + 0.27 * cos(angle * 4.0 + PI)
7: p *= Vector2(0.72, 1.0)
8: p *= 0.8 + 0.2 * cos(angle * 3.0)
return p
func perspective(depth: float) -> float:
return (1.0 / (1.0 + 4.0 * (1.0 - clampf(depth,0,1))) - 0.2) / 0.8
func tunnel_focus() -> Vector2:
# Open V lanes collapse visually if they use the low circular-tube focus.
return Vector2(320,345) if game.is_open_tube() else VANISH
func rim(index: float, depth: float) -> Vector2:
var p := profile(index, game.stage)
return (tunnel_focus() + p * Vector2(34,43 if game.is_open_tube() else 34)).lerp(Vector2(320,410) + p * 265.0, perspective(depth))
func glow(points: PackedVector2Array, color: Color, strength: float = 1.0) -> void:
if points.size() < 2:
return
var tint := Color(color,minf(1.0,strength*game.glow_strength))
var radius := 10.0 * sqrt(maxf(.1,game.glow_strength))
for i in range(points.size()-1):
var a := points[i]
var b := points[i+1]
if a.distance_squared_to(b) < .0001:
continue
var direction := (b-a).normalized()
var normal := direction.orthogonal()*radius
_beam_quad(a,b,normal,.5,.5,tint)
_beam_quad(a-direction*radius,a,normal,0,.5,tint)
_beam_quad(b,b+direction*radius,normal,.5,1,tint)
func line(a: Vector2, b: Vector2, color: Color, strength: float = 1.0) -> void:
glow(PackedVector2Array([a,b]),color,strength)
func text_at(value: String, origin: Vector2, height: float, color: Color, strength: float = 0.8) -> void:
var unit := height / 6.0
var x := origin.x
for character in value:
for stroke in glyph_cache.get(character,[]):
var points := PackedVector2Array()
for point in stroke:
points.append(Vector2(x,origin.y)+point*unit)
glow(points,color,strength)
x += unit*6.0
func label_at(value: String, y: float, height: float, color: Color) -> void:
text_at(value,Vector2((640 - value.length() * height + height / 3.0)/2,y),height,color)
func actor(which_lane: float, depth: float, kind: String, roll: float = 0.0, roll_direction: int = 1) -> void:
var center := rim(which_lane + .5,depth)
var tangent := (rim(which_lane+1,depth)-rim(which_lane,depth)).normalized()
var inward := (tunnel_focus()-center).normalized()
var s := lerpf(3.0,17.0,perspective(depth))
var color := Color("ff2929")
var shape := [Vector2(-1,-.6),Vector2(1,.6),Vector2(1,-.6),Vector2(-1,.6),Vector2(-1,-.6)]
if kind == "player":
color = YELLOW
s = lerpf(4.0,26.0,perspective(depth))
shape = [Vector2(-1,.9),Vector2(-.85,-.25),Vector2(0,-.7),Vector2(.85,-.25),Vector2(1,.9),Vector2(.45,.1),Vector2(0,-.15),Vector2(-.45,.1),Vector2(-1,.9)]
elif kind == "tanker":
color = Color("df36ff")
shape = [Vector2(-1,0),Vector2(-.45,-.75),Vector2(.45,-.75),Vector2(1,0),Vector2(.45,.75),Vector2(-.45,.75),Vector2(-1,0),Vector2(1,0)]
elif kind == "spiker":
color = GREEN
shape = []
for i in range(17):
shape.append(Vector2.from_angle(i * TAU / 8.0 + game.clock * 7.0) * (1.0 if i % 2 == 0 else .3))
var points := PackedVector2Array()
for point in shape:
var p: Vector2 = point
if roll > 0:
p = p.rotated((1.0-roll/.24)*PI*roll_direction)
points.append(center + (tangent*p.x+inward*p.y)*s)
glow(points,color)
func draw_tube(zoom: float = 1.0, strength: float = 1.0) -> void:
var pivot := tunnel_focus()
for i in range(17 if game.is_open_tube() else 16):
var selected: bool = i == game.lane or i == (game.lane+1 if game.is_open_tube() else (game.lane+1)%16)
line(pivot+(rim(i,0)-pivot)*zoom,pivot+(rim(i,1)-pivot)*zoom,YELLOW if selected else BLUE,strength)
for d in [0.0,1.0]:
var ring := PackedVector2Array()
for i in range(17):
ring.append(pivot+(rim(i,d)-pivot)*zoom)
glow(ring,BLUE,strength)
func stars(strength: float = 1.0) -> void:
for i in range(95):
var angle := i * 2.399963
var t := fposmod(sin(i * 127.1 + 17.3) * 43758.5453 + game.clock * .43,1.0)
var radius := pow(t,2.3)*700.0
var direction := Vector2.from_angle(angle)
line(tunnel_focus()+direction*radius,tunnel_focus()+direction*(radius+2+t*24),[CYAN,BLUE,Color.WHITE][i%3],strength*t*.75)
func hud() -> void:
text_at(str(game.score),Vector2(45,43),32,GREEN)
text_at("HIGH SCORE",Vector2(323,28),13,GREEN)
text_at(str(game.high_score),Vector2(342,52),19,GREEN)
text_at(str(game.stage),Vector2(342,83),16,BLUE)
for i in range(mini(game.lives,9)):
var x := 46.0 + i * 27
glow(PackedVector2Array([Vector2(x,104),Vector2(x+10,98),Vector2(x+20,104),Vector2(x+14,111),Vector2(x+10,105),Vector2(x+6,111),Vector2(x,104)]),YELLOW,.7)
func title_logo() -> void:
# The white leading copy grows from the distance. Each older copy is a
# smaller scale of exactly the same strokes, converging below the logo.
var time: float = fmod(game.attract_clock,28.0)
var front_scale := lerpf(.05,1.0,smoothstep(0.0,3.0,time))
var pivot := Vector2(320,408)
for layer in range(14,-1,-1):
var age := 0.0 if layer == 0 else float(layer)-fposmod(game.clock*6.0,1.0)
var copy_scale := front_scale * (1.0-age*.027)
var tint := Color.WHITE if layer == 0 else Color.from_hsv(fposmod(game.clock*.16-age*.055,1.0),1.0,1.0)
for stroke in LOGO_STROKES:
var points := PackedVector2Array()
for point in stroke:
var front: Vector2 = Vector2(320,286)+(point-Vector2(466.5,107))*.71
points.append(pivot+(front-pivot)*copy_scale)
glow(points,tint,.72 if layer > 0 else 1.0)
func level_select() -> void:
label_at("PLAYER 1",182,36,Color.WHITE)
label_at("RATE YOURSELF",245,19,GREEN)
label_at("TURN TO CHANGE",275,18,CYAN)
label_at("PRESS FIRE TO SELECT",304,18,YELLOW)
text_at("NOVICE",Vector2(102,368),14,Color.RED)
text_at("EXPERT",Vector2(463,368),14,Color.RED)
for i in range(5):
var x := 123.0+i*98
text_at(str(game.START_LEVELS[i]),Vector2(x,405),21,GREEN)
var shape := PackedVector2Array()
for j in range(17):
shape.append(Vector2(x+8,465)+profile(j,game.START_LEVELS[i])*24)
glow(shape,BLUE)
text_at(str(game.START_BONUSES[i]),Vector2(x-25,518),13,Color("ff3030"))
if i == game.selection:
glow(PackedVector2Array([Vector2(x-37,390),Vector2(x+48,390),Vector2(x+48,550),Vector2(x-37,550),Vector2(x-37,390)]),Color.WHITE,.7)
text_at("LEVEL",Vector2(14,410),13,GREEN)
text_at("HOLE",Vector2(14,465),13,GREEN)
text_at("BONUS",Vector2(14,520),13,GREEN)
label_at("TIME %d" % ceili(game.phase_time),651,20,GREEN)
func _draw() -> void:
beam_vertices.clear()
beam_uvs.clear()
beam_colors.clear()
beam_indices.clear()
_draw_board()
_flush_beams()
func _draw_board() -> void:
if game == null: return
if Engine.is_editor_hint():
draw_rect(Rect2(Vector2.ZERO,SIZE),Color.BLACK)
else:
draw_rect(Rect2(-position/scale,get_viewport_rect().size/scale),Color.BLACK)
if game.phase == "select":
level_select()
return
if game.phase == "title" and not Engine.is_editor_hint():
var page: float = fmod(game.attract_clock,28.0)
if page < 9.0:
title_logo()
elif page < 15.0:
label_at("HIGH SCORES",222,29,Color("ff3030"))
label_at("1 PLAYER %06d" % game.high_score,316,22,BLUE)
label_at("THIS SESSION",370,14,CYAN)
elif page < 23.0:
draw_tube()
actor(floor(game.clock*1.5),1.0,"player")
for i in range(6):
actor(i*3,fposmod(game.clock*.08+i*.16,1.0),"flipper")
label_at("GAME OVER",130,21,GREEN)
else:
label_at("DESTROY THE ENEMIES",270,23,Color.RED)
label_at("FIRE DOWN THE TUBE",330,20,YELLOW)
label_at("SUPERZAPPER",401,24,CYAN)
label_at("FIRST BLAST - ALL",451,18,GREEN)
label_at("SECOND BLAST - ONE",486,18,GREEN)
label_at("PRESS START",697,23,GREEN)
label_at("BONUS EVERY 20000",740,15,CYAN)
label_at("FREE PLAY",774,14,GREEN)
return
if game.phase in ["space","victory","won"]:
stars()
if game.phase in ["victory","won"]:
label_at("YOU WIN!",350,40,YELLOW)
elif game.awarded_bonus > 0:
label_at("BONUS %d" % game.awarded_bonus,310,25,YELLOW)
elif game.phase == "arrive":
var t := clampf(1.0-game.phase_time/1.6,0,1)
stars(1.0-t)
draw_tube(lerpf(.025,1.0,t*t*(3.0-2.0*t)))
elif game.phase == "dive":
var t: float = 1.0-game.dive_depth()
stars(t)
draw_tube(1.0+t*t*7,1.0-t)
for spike_lane in game.spikes:
line(rim(spike_lane+.5,0),rim(spike_lane+.5,game.spikes[spike_lane]),GREEN,1.0-t)
actor(game.lane,game.dive_depth(),"player")
if not game.spikes.is_empty(): label_at("AVOID SPIKES",720,18,Color.RED)
else:
draw_tube()
for spike_lane in game.spikes:
line(rim(spike_lane+.5,0),rim(spike_lane+.5,game.spikes[spike_lane]),GREEN)
for enemy in game.enemies:
var visible_lane: float = enemy.lane
var rolling: float = enemy.get("roll",0.0)
if rolling > 0:
visible_lane = float(enemy.from_lane)+(1.0-rolling/.24)*enemy.direction
actor(visible_lane,enemy.depth,enemy.kind,rolling,enemy.get("direction",1))
for shot in game.shots:
line(rim(shot.lane+.5,shot.depth),rim(shot.lane+.5,maxf(0,shot.depth-.08)),YELLOW)
if game.phase not in ["death","lost"]:
actor(game.lane,1,"player")
for spark in game.sparks:
var origin := rim(spark.lane+.5,spark.depth)
for i in range(8):
var direction := Vector2.from_angle(i*TAU/8+game.clock)
var r: float = (0.7-spark.life)*65
line(origin+direction*r*.5,origin+direction*r,YELLOW,minf(1,spark.life*3))
if game.phase == "ready": label_at("PLAYER 1",345,29,Color.WHITE)
if game.phase == "lost": label_at("GAME OVER",345,29,Color.RED)
if game.zap_flash > 0:
for i in range(16): line(rim(i+.5,0),rim(i+.5,1),Color.WHITE,game.zap_flash*3)
hud()
if game.phase in ["play","ready"]:
label_at("SUPERZAPPER %d" % (2-game.zap_uses),749,14,CYAN)
label_at("ARROWS MOVE SPACE FIRE Z ZAP",779,11,GREEN)
elif game.phase in ["won","lost"]:
label_at("PRESS START",749,22,GREEN)
+1
View File
@@ -0,0 +1 @@
uid://b150rg646gh6x