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,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")
|
||||
@@ -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})
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://d00n1vs51ma48
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bd2kidcvk2qgy
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://b150rg646gh6x
|
||||
Reference in new issue
Block a user