212 lines
8.8 KiB
GDScript
212 lines
8.8 KiB
GDScript
@tool
|
|
extends Node2D
|
|
const SIZE := Vector2(256,231)
|
|
const FONT := {
|
|
"A":[14,17,17,31,17,17,17],"B":[30,17,17,30,17,17,30],"C":[14,17,16,16,16,17,14],"D":[30,17,17,17,17,17,30],"E":[31,16,16,30,16,16,31],"F":[31,16,16,30,16,16,16],"G":[14,17,16,23,17,17,15],"H":[17,17,17,31,17,17,17],"I":[31,4,4,4,4,4,31],"J":[7,2,2,2,18,18,12],"K":[17,18,20,24,20,18,17],"L":[16,16,16,16,16,16,31],"M":[17,27,21,21,17,17,17],"N":[17,25,21,19,17,17,17],"O":[14,17,17,17,17,17,14],"P":[30,17,17,30,16,16,16],"Q":[14,17,17,17,21,18,13],"R":[30,17,17,30,20,18,17],"S":[15,16,16,14,1,1,30],"T":[31,4,4,4,4,4,4],"U":[17,17,17,17,17,17,14],"V":[17,17,17,17,17,10,4],"W":[17,17,17,21,21,21,10],"X":[17,17,10,4,10,17,17],"Y":[17,17,10,4,4,4,4],"Z":[31,1,2,4,8,16,31],
|
|
"0":[14,17,19,21,25,17,14],"1":[4,12,4,4,4,4,14],"2":[14,17,1,2,4,8,31],"3":[30,1,1,14,1,1,30],"4":[2,6,10,18,31,2,2],"5":[31,16,16,30,1,1,30],"6":[14,16,16,30,17,17,14],"7":[31,1,2,4,8,8,8],"8":[14,17,17,14,17,17,14],"9":[14,17,17,15,1,1,14],"!":[4,4,4,4,4,0,4],"-":[0,0,0,31,0,0,0],".":[0,0,0,0,0,12,12],":":[0,12,12,0,12,12,0]
|
|
}
|
|
var game: Node
|
|
var mesh := ArrayMesh.new()
|
|
var vertices := PackedVector3Array()
|
|
var colors := PackedColorArray()
|
|
var indices := PackedInt32Array()
|
|
var logo_pixels: Array[Vector2i] = []
|
|
var title_craters: Array[Dictionary] = []
|
|
var title_rows: Dictionary = {}
|
|
|
|
func _ready() -> void:
|
|
game = get_parent().get_parent()
|
|
_build_logo()
|
|
|
|
func _build_logo() -> void:
|
|
for row in range(2):
|
|
var word := "MISSILE" if row == 0 else "COMMAND"
|
|
for letter in range(word.length()):
|
|
for y in range(7):
|
|
for x in range(5):
|
|
if FONT[word[letter]][y] & (1 << (4-x)):
|
|
for sy in range(4):
|
|
for sx in range(4):
|
|
logo_pixels.append(Vector2i(46+letter*24+x*4+sx,65+row*43+y*4+sy))
|
|
# Each crater has a deterministic impact time and persists as missing pixels.
|
|
for i in range(42):
|
|
title_craters.append({"pos":Vector2(42+fposmod(i*59.7,174),62+fposmod(i*37.3,79)),"time":1.0+i*.145,"radius":9.0+float(i%5)})
|
|
|
|
for row in range(2):
|
|
for letter in range(7):
|
|
title_craters.append({"pos":Vector2(56+letter*24,79+row*43),"time":6.2+letter*.1,"radius":20.0})
|
|
# Precompute erosion times once, not a pixel/crater search every frame.
|
|
for point in logo_pixels:
|
|
var erased_at := 7.7
|
|
for crater in title_craters:
|
|
var distance := Vector2(point).distance_to(crater.pos)
|
|
if distance < crater.radius:
|
|
erased_at = minf(erased_at,crater.time+distance/30.0)
|
|
if not title_rows.has(point.y): title_rows[point.y] = []
|
|
title_rows[point.y].append(Vector2(point.x,erased_at))
|
|
for y in title_rows:
|
|
title_rows[y].sort_custom(func(a: Vector2,b: Vector2) -> bool: return a.x < b.x)
|
|
|
|
func rect(position: Vector2, size: Vector2, color: Color) -> void:
|
|
var start := vertices.size()
|
|
for point in [position,position+Vector2(size.x,0),position+size,position+Vector2(0,size.y)]:
|
|
vertices.append(Vector3(floorf(point.x),floorf(point.y),0))
|
|
colors.append(color)
|
|
indices.append_array(PackedInt32Array([start,start+1,start+2,start,start+2,start+3]))
|
|
|
|
func segment(a: Vector2,b: Vector2,color: Color) -> void:
|
|
var count := maxi(1,int(maxf(absf(b.x-a.x),absf(b.y-a.y))))
|
|
for i in range(count+1): rect(a.lerp(b,float(i)/count).round(),Vector2.ONE,color)
|
|
|
|
func disk(center: Vector2,radius: float,color: Color) -> void:
|
|
var r := int(radius)
|
|
for y in range(-r,r+1):
|
|
var extent := int(sqrt(maxf(0,r*r-y*y)))
|
|
rect(center+Vector2(-extent,y),Vector2(extent*2+1,1),color)
|
|
|
|
func text_at(value: String,at: Vector2,color: Color,size: int = 1) -> void:
|
|
for i in range(value.length()):
|
|
if not FONT.has(value[i]): continue
|
|
for y in range(7):
|
|
var run := -1
|
|
for x in range(6):
|
|
var filled: bool = x < 5 and (FONT[value[i]][y] & (1 << (4-x))) != 0
|
|
if filled and run < 0: run = x
|
|
if not filled and run >= 0:
|
|
rect(at+Vector2((i*6+run)*size,y*size),Vector2((x-run)*size,size),color)
|
|
run = -1
|
|
|
|
func label(value: String,y: float,color: Color,size: int = 1) -> void:
|
|
text_at(value,Vector2((256-(value.length()*6-1)*size)/2.0,y),color,size)
|
|
|
|
func palette() -> Array:
|
|
return [[Color("ffff00"),Color("203cff"),Color("ff2525")],[Color("20e5ff"),Color("ff3030"),Color("ffffff")],[Color("ff3030"),Color("24ff35"),Color("ffff00")]][int((game.wave-1)/2)%3]
|
|
|
|
func landscape() -> void:
|
|
var p := palette()
|
|
rect(Vector2(0,214),Vector2(256,17),p[0])
|
|
for i in range(3):
|
|
var x: float = game.BASE_X[i]
|
|
for y in range(12): rect(Vector2(x-8-y,202+y),Vector2(16+y*2,1),p[0])
|
|
if game.bases[i]:
|
|
var slots := [Vector2(0,-9),Vector2(-2,-6),Vector2(2,-6),Vector2(-4,-3),Vector2(0,-3),Vector2(4,-3),Vector2(-6,0),Vector2(-2,0),Vector2(2,0),Vector2(6,0)]
|
|
for missile in range(game.ammo[i]):
|
|
var at: Vector2 = Vector2(x,213)+slots[missile]
|
|
rect(at-Vector2(0,2),Vector2(1,3),p[1])
|
|
rect(at-Vector2(1,0),Vector2(3,1),p[1])
|
|
else: rect(Vector2(x-7,207),Vector2(14,7),Color.BLACK)
|
|
for i in range(6):
|
|
var x: float = game.CITY_X[i]
|
|
if game.cities[i]:
|
|
for building in range(7):
|
|
var h := 2+(building*3+i)%7
|
|
rect(Vector2(x-9+building*3,214-h),Vector2(2,h),p[1])
|
|
rect(Vector2(x-9,212),Vector2(20,2),p[1])
|
|
else:
|
|
rect(Vector2(x-9,213),Vector2(19,2),Color.BLACK)
|
|
|
|
func hud() -> void:
|
|
var p := palette()
|
|
text_at(str(game.score),Vector2(14,7),p[2])
|
|
text_at(str(game.high_score),Vector2(106,7),p[2])
|
|
text_at("1UP",Vector2(17,18),p[1])
|
|
text_at("HIGH",Vector2(112,18),p[1])
|
|
text_at("WAVE %d" % game.wave,Vector2(204,7),p[2])
|
|
|
|
func title() -> void:
|
|
var t: float = game.attract_time
|
|
var color := Color.from_hsv(fposmod(floor(t*5)/7,1),1,1)
|
|
for y in title_rows:
|
|
var run_start := -1
|
|
var run_end := -1
|
|
for pixel in title_rows[y]:
|
|
var x := int(pixel.x)
|
|
var visible: bool = t < pixel.y
|
|
if run_start >= 0 and (not visible or x > run_end+1):
|
|
rect(Vector2(run_start,y),Vector2(run_end-run_start+1,1),color)
|
|
run_start = -1
|
|
if visible:
|
|
if run_start < 0: run_start = x
|
|
run_end = x
|
|
if run_start >= 0: rect(Vector2(run_start,y),Vector2(run_end-run_start+1,1),color)
|
|
for crater in title_craters:
|
|
var age: float = t-crater.time
|
|
if age > 0 and age < .65:
|
|
disk(crater.pos,minf(9,age*25)*(1-age/.65),Color.from_hsv(fposmod(t*.7+.33,1),1,1))
|
|
label("PUSH START",174,Color("24ff35"))
|
|
label("MISSILE COMMAND",193,Color("2040ff"))
|
|
label("FREE PLAY",222,Color("ffff00"))
|
|
|
|
func _draw() -> void:
|
|
if game == null: return
|
|
vertices.clear()
|
|
colors.clear()
|
|
indices.clear()
|
|
draw_rect(Rect2(Vector2.ZERO,SIZE),Color.BLACK)
|
|
if game.phase == "title":
|
|
title()
|
|
elif game.phase == "leaderboard":
|
|
landscape()
|
|
hud()
|
|
label("HIGH SCORES",37,Color("2040ff"))
|
|
for i in range(8):
|
|
var value := "%06d" % game.leaderboard[i] if i < game.leaderboard.size() else "------"
|
|
text_at("P1" if i < game.leaderboard.size() else "--",Vector2(70,56+i*12),Color.YELLOW)
|
|
text_at(value,Vector2(126,56+i*12),Color.YELLOW)
|
|
label("BONUS CITY EVERY",163,Color("2040ff"))
|
|
label("%d POINTS" % game.bonus_city_interval,176,Color.YELLOW)
|
|
label("PUSH START",195,Color("2040ff"))
|
|
else:
|
|
landscape()
|
|
hud()
|
|
var p := palette()
|
|
for missile in game.incoming:
|
|
segment(missile.origin,missile.pos,p[2])
|
|
if missile.kind == "smart":
|
|
var at: Vector2 = missile.pos
|
|
segment(at+Vector2(0,-3),at+Vector2(2,0),Color.WHITE)
|
|
segment(at+Vector2(2,0),at+Vector2(0,3),Color.WHITE)
|
|
segment(at+Vector2(0,3),at+Vector2(-2,0),Color.WHITE)
|
|
segment(at+Vector2(-2,0),at+Vector2(0,-3),Color.WHITE)
|
|
else: rect(missile.pos,Vector2(2,2),Color.WHITE)
|
|
for shot in game.interceptors:
|
|
segment(shot.origin,shot.pos,p[1])
|
|
rect(shot.pos,Vector2(2,2),Color.WHITE)
|
|
for plane in game.aircraft:
|
|
var at: Vector2 = plane.pos
|
|
segment(at-Vector2(6,0),at+Vector2(6,0),p[2])
|
|
segment(at-Vector2(2,4),at+Vector2(2,4),p[2])
|
|
segment(at-Vector2(5,2),at-Vector2(5,-2),p[2])
|
|
for blast in game.blasts:
|
|
var color: Color = p[2] if int(game.clock*12)%3 != 0 else Color.WHITE
|
|
disk(blast.pos,blast.radius,color)
|
|
if blast.age > 1.0: disk(blast.pos,blast.radius*.5,Color.BLACK)
|
|
if game.phase in ["play","ready"]:
|
|
var at: Vector2 = game.aim.round()
|
|
segment(at-Vector2(3,0),at+Vector2(3,0),p[1])
|
|
segment(at-Vector2(0,3),at+Vector2(0,3),p[1])
|
|
if game.phase == "ready": label("DEFEND CITIES",181,p[1])
|
|
if game.phase == "bonus":
|
|
label("BONUS POINTS",85,p[1])
|
|
label(str(game.wave_bonus),102,p[0],2)
|
|
label("%d CITIES SAVED" % game.cities.count(true),131,p[1])
|
|
if game.phase == "lost":
|
|
disk(Vector2(128,114),minf(75,(5-game.phase_time)*24),p[2])
|
|
label("THE END",110,Color.BLACK,2)
|
|
if game.phase == "won":
|
|
label("YOU WIN!",100,p[0],2)
|
|
label("CITIES DEFENDED",123,p[1])
|
|
label("PUSH START",178,p[1])
|
|
mesh.clear_surfaces()
|
|
if not vertices.is_empty():
|
|
var arrays := []
|
|
arrays.resize(Mesh.ARRAY_MAX)
|
|
arrays[Mesh.ARRAY_VERTEX] = vertices
|
|
arrays[Mesh.ARRAY_COLOR] = colors
|
|
arrays[Mesh.ARRAY_INDEX] = indices
|
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES,arrays)
|
|
draw_mesh(mesh,null)
|
|
|
|
|
|
|