@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)