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,25 @@
|
||||
[gd_scene format=3 uid="uid://da7lj6k47rotp"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://fqff3d88cr1j" path="res://games/frogger/frogger.gd" id="1"]
|
||||
[ext_resource type="Script" uid="uid://df5twn4utxg0n" path="res://games/frogger/frogger_view.gd" id="2"]
|
||||
[ext_resource type="Script" uid="uid://blkjvi1sb7hmb" path="res://games/frogger/display.gd" id="3"]
|
||||
[ext_resource type="Script" uid="uid://dh0mu01mar7nl" path="res://games/frogger/frogger_audio.gd" id="4"]
|
||||
|
||||
[node name="Frogger" type="Node" unique_id=1686543748]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="PixelScreen" type="SubViewport" parent="." unique_id=865659315]
|
||||
disable_3d = true
|
||||
size = Vector2i(224, 256)
|
||||
render_target_update_mode = 4
|
||||
|
||||
[node name="Board" type="Node2D" parent="PixelScreen" unique_id=184374191]
|
||||
texture_filter = 1
|
||||
script = ExtResource("2")
|
||||
|
||||
[node name="Display" type="Node2D" parent="." unique_id=333917260]
|
||||
texture_filter = 1
|
||||
script = ExtResource("3")
|
||||
|
||||
[node name="Audio" type="Node" parent="." unique_id=712591344]
|
||||
script = ExtResource("4")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b242bxbbuaj5o"
|
||||
path="res://.godot/imported/spr_frogger.png-7ce0f59b8e564b24229faf1647204a8e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/frogger/art/spr_frogger.png"
|
||||
dest_files=["res://.godot/imported/spr_frogger.png-7ce0f59b8e564b24229faf1647204a8e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="mp3"
|
||||
type="AudioStreamMP3"
|
||||
uid="uid://sp8rad7erusb"
|
||||
path="res://.godot/imported/frogger-music.mp3-83a0af3950edf8e2ce0550d65f173319.mp3str"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/frogger/audio/frogger-music.mp3"
|
||||
dest_files=["res://.godot/imported/frogger-music.mp3-83a0af3950edf8e2ce0550d65f173319.mp3str"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://smtgenseeuju"
|
||||
path="res://.godot/imported/sound-frogger-coin-in.wav-8633ca7444f8a7846bf6405524ad9b37.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/frogger/audio/sound-frogger-coin-in.wav"
|
||||
dest_files=["res://.godot/imported/sound-frogger-coin-in.wav-8633ca7444f8a7846bf6405524ad9b37.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://jdgf84jxnjus"
|
||||
path="res://.godot/imported/sound-frogger-extra.wav-158c017030f00ec8317ac46cfefa90a8.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/frogger/audio/sound-frogger-extra.wav"
|
||||
dest_files=["res://.godot/imported/sound-frogger-extra.wav-158c017030f00ec8317ac46cfefa90a8.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dbeeno1vhd72o"
|
||||
path="res://.godot/imported/sound-frogger-hop.wav-cf3b2064d4fb4c61b75fee957fe06e77.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/frogger/audio/sound-frogger-hop.wav"
|
||||
dest_files=["res://.godot/imported/sound-frogger-hop.wav-cf3b2064d4fb4c61b75fee957fe06e77.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://c40g5wcvnh1a1"
|
||||
path="res://.godot/imported/sound-frogger-plunk.wav-73a8475dfe1089b682fa96175c511864.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/frogger/audio/sound-frogger-plunk.wav"
|
||||
dest_files=["res://.godot/imported/sound-frogger-plunk.wav-73a8475dfe1089b682fa96175c511864.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://cgy6c1rwyev5e"
|
||||
path="res://.godot/imported/sound-frogger-squash.wav-1b686c87ff414930a009a0fcd27650d0.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/frogger/audio/sound-frogger-squash.wav"
|
||||
dest_files=["res://.godot/imported/sound-frogger-squash.wav-1b686c87ff414930a009a0fcd27650d0.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://clqop5ekc2wkt"
|
||||
path="res://.godot/imported/sound-frogger-time.wav-18d22f8e1a9342d25829a0bf05ddae73.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/frogger/audio/sound-frogger-time.wav"
|
||||
dest_files=["res://.godot/imported/sound-frogger-time.wav-18d22f8e1a9342d25829a0bf05ddae73.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
@@ -0,0 +1,19 @@
|
||||
@tool
|
||||
extends Node2D
|
||||
const LOGICAL := Vector2(224,256)
|
||||
var destination := Rect2(0,0,672,768)
|
||||
func _process(_delta: float) -> void:
|
||||
var available := get_viewport_rect().size if not Engine.is_editor_hint() else Vector2(672,768)
|
||||
var factor := minf(available.x/7.0,available.y/8.0)
|
||||
var size := Vector2(7,8)*factor
|
||||
destination = Rect2((available-size)*.5,size)
|
||||
queue_redraw()
|
||||
func to_logical(point: Vector2) -> Vector2:
|
||||
return (point-destination.position)/destination.size*LOGICAL
|
||||
func to_logical_delta(delta: Vector2) -> Vector2:
|
||||
return delta/destination.size*LOGICAL
|
||||
func _draw() -> void:
|
||||
draw_rect(Rect2(Vector2.ZERO,get_viewport_rect().size),Color.BLACK)
|
||||
if has_node("../PixelScreen"):
|
||||
draw_texture_rect(get_node("../PixelScreen").get_texture(),destination,false)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://blkjvi1sb7hmb
|
||||
@@ -0,0 +1,329 @@
|
||||
@tool
|
||||
extends ArcadeGame
|
||||
@export_range(0,99) var boards_to_win: int = 1
|
||||
@export_range(1,9) var starting_lives: int = 3
|
||||
@export_range(15,120) var crossing_time: float = 60.0
|
||||
@export_range(.5,2) var difficulty: float = 1.0
|
||||
@export var music_enabled: bool = true
|
||||
const HOME_X := [16,64,112,160,208]
|
||||
var phase := "title"
|
||||
var phase_time := 0.0
|
||||
var clock := 0.0
|
||||
var score := 0
|
||||
var high_score := 0
|
||||
var lives := 3
|
||||
var board_number := 1
|
||||
var homes: Array[bool] = [false,false,false,false,false]
|
||||
var frog := Vector2(112,232)
|
||||
var from := Vector2(112,232)
|
||||
var destination := Vector2(112,232)
|
||||
var direction := 0
|
||||
var hop_left := 0.0
|
||||
var repeat_left := 0.0
|
||||
var time_left := 60.0
|
||||
var furthest_row := 0
|
||||
var warned := false
|
||||
var extra_awarded := false
|
||||
var death_kind := "squash"
|
||||
var lanes: Array[Dictionary] = []
|
||||
var leaderboard: Array[int] = []
|
||||
var attract_time := 0.0
|
||||
var demo_active := false
|
||||
var demo_time := 0.0
|
||||
var idle_score := 0
|
||||
@onready var view: Node2D = $PixelScreen/Board
|
||||
|
||||
func _ready() -> void:
|
||||
make_lanes()
|
||||
lives = starting_lives
|
||||
|
||||
func make_lanes() -> void:
|
||||
lanes.clear()
|
||||
var definitions := [
|
||||
[1,"car",-24.0,16.0,80.0,20.0,1], [2,"car",28.0,16.0,82.0,48.0,3],
|
||||
[3,"car",-32.0,16.0,78.0,8.0,0], [4,"car",45.0,16.0,110.0,70.0,2],
|
||||
[5,"car",-22.0,32.0,100.0,30.0,4], [7,"turtle",-24.0,48.0,72.0,4.0,0],
|
||||
[8,"log",25.0,48.0,98.0,30.0,0], [9,"log",36.0,96.0,144.0,5.0,0],
|
||||
[10,"turtle",-30.0,32.0,70.0,14.0,1], [11,"log",22.0,64.0,100.0,40.0,0]]
|
||||
for item in definitions:
|
||||
lanes.append({"row":item[0],"kind":item[1],"speed":item[2]*(1+(board_number-1)*.10)*difficulty,"width":item[3],"spacing":item[4],"offset":item[5],"sprite":item[6]})
|
||||
|
||||
func start_game() -> bool:
|
||||
if not super.start_game(): return false
|
||||
demo_active = false
|
||||
attract_time = 0
|
||||
$Audio.stop_all()
|
||||
score = 0
|
||||
lives = starting_lives
|
||||
board_number = 1
|
||||
extra_awarded = false
|
||||
homes.fill(false)
|
||||
make_lanes()
|
||||
respawn()
|
||||
phase = "ready"
|
||||
phase_time = 1.5
|
||||
$Audio.effect("coin-in")
|
||||
return true
|
||||
|
||||
func reset_game() -> void:
|
||||
super.reset_game()
|
||||
demo_active = false
|
||||
attract_time = 0
|
||||
$Audio.stop_all()
|
||||
phase = "title"
|
||||
clock = 0
|
||||
score = 0
|
||||
board_number = 1
|
||||
lives = starting_lives
|
||||
homes.fill(false)
|
||||
make_lanes()
|
||||
respawn()
|
||||
|
||||
func respawn() -> void:
|
||||
frog = Vector2(112,232)
|
||||
from = frog
|
||||
destination = frog
|
||||
direction = 0
|
||||
hop_left = 0
|
||||
repeat_left = 0
|
||||
time_left = crossing_time
|
||||
furthest_row = 0
|
||||
warned = false
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if Engine.is_editor_hint() or (event is InputEventKey and event.echo): return
|
||||
if event.is_action_pressed("arcade_start"):
|
||||
if not is_running: start_game()
|
||||
|
||||
func pressed(action: String) -> bool:
|
||||
return Input.is_action_pressed(action)
|
||||
|
||||
func hop(step: Vector2i) -> bool:
|
||||
if phase != "play" or hop_left > 0 or absi(step.x)+absi(step.y) != 1: return false
|
||||
var target := frog+Vector2(step)*16
|
||||
if target.x < 8 or target.x > 216 or target.y > 232 or target.y < 40: return false
|
||||
direction = 0 if step.y < 0 else (2 if step.y > 0 else (1 if step.x < 0 else 3))
|
||||
from = frog
|
||||
destination = target
|
||||
hop_left = .12
|
||||
$Audio.effect("hop")
|
||||
return true
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
clock += delta
|
||||
if Engine.is_editor_hint():
|
||||
attract_time = fmod(clock,44.0)
|
||||
view.queue_redraw()
|
||||
return
|
||||
if phase in ["title","scores"]:
|
||||
attract_time += delta
|
||||
if phase == "title" and attract_time >= 44.0:
|
||||
begin_demo()
|
||||
view.queue_redraw()
|
||||
return
|
||||
elif phase == "scores" and attract_time >= 7.0:
|
||||
phase = "title"
|
||||
attract_time = 0
|
||||
elif phase in ["ready","dead","home","clear"]:
|
||||
phase_time -= delta
|
||||
if phase_time <= 0:
|
||||
match phase:
|
||||
"ready":
|
||||
phase = "play"
|
||||
if music_enabled and not demo_active: $Audio.music()
|
||||
"dead":
|
||||
if lives <= 0 and not demo_active:
|
||||
finish(false)
|
||||
else:
|
||||
respawn()
|
||||
phase = "play"
|
||||
if music_enabled and not demo_active: $Audio.music()
|
||||
"home":
|
||||
if not homes.has(false):
|
||||
add_score(1000)
|
||||
phase = "clear"
|
||||
phase_time = 2.0
|
||||
else:
|
||||
respawn()
|
||||
phase = "play"
|
||||
"clear":
|
||||
if demo_active:
|
||||
end_demo()
|
||||
elif boards_to_win > 0 and board_number >= boards_to_win:
|
||||
finish(true)
|
||||
else:
|
||||
board_number += 1
|
||||
homes.fill(false)
|
||||
make_lanes()
|
||||
respawn()
|
||||
phase = "ready"
|
||||
phase_time = 1.5
|
||||
elif phase == "play":
|
||||
repeat_left -= delta
|
||||
if hop_left <= 0 and repeat_left <= 0:
|
||||
var step := Vector2i.ZERO
|
||||
if demo_active: step = demo_step()
|
||||
elif pressed("arcade_up"): step = Vector2i.UP
|
||||
elif pressed("arcade_down"): step = Vector2i.DOWN
|
||||
elif pressed("arcade_left"): step = Vector2i.LEFT
|
||||
elif pressed("arcade_right"): step = Vector2i.RIGHT
|
||||
if step != Vector2i.ZERO and hop(step): repeat_left = .16
|
||||
step_world(delta)
|
||||
if demo_active:
|
||||
demo_time += delta
|
||||
if demo_time >= 32.0: end_demo()
|
||||
view.queue_redraw()
|
||||
|
||||
func begin_demo() -> void:
|
||||
idle_score = score
|
||||
demo_active = true
|
||||
demo_time = 0
|
||||
score = 0
|
||||
lives = starting_lives
|
||||
board_number = 1
|
||||
homes.fill(false)
|
||||
clock = 0
|
||||
make_lanes()
|
||||
respawn()
|
||||
phase = "play"
|
||||
$Audio.stop_all()
|
||||
|
||||
func end_demo() -> void:
|
||||
demo_active = false
|
||||
score = idle_score
|
||||
phase = "scores"
|
||||
attract_time = 0
|
||||
$Audio.stop_all()
|
||||
|
||||
# Predict the same lane positions and hitboxes used by live gameplay.
|
||||
# The demo supplies ordinary hops; it has no invulnerability or scripted teleports.
|
||||
func demo_safe(step: Vector2i) -> bool:
|
||||
var target := frog + Vector2(step)*16
|
||||
if target.x < 8 or target.x > 216 or target.y < 40 or target.y > 232: return false
|
||||
if target.y == 40:
|
||||
for i in range(5):
|
||||
if absf(target.x-HOME_X[i]) <= 7 and not homes[i]: return true
|
||||
return false
|
||||
for tick in range(1,13):
|
||||
var t := tick * .01
|
||||
var point := frog.lerp(target,t/.12)
|
||||
var row := roundi((232-point.y)/16)
|
||||
for lane in lanes:
|
||||
if lane.row != row: continue
|
||||
var supported := false
|
||||
var predicted := lane.duplicate()
|
||||
predicted.offset += lane.speed*t
|
||||
for x in lane_objects(predicted):
|
||||
if lane.kind == "car" and point.x+6 > x+2 and point.x-6 < x+lane.width-2: return false
|
||||
if lane.kind != "car" and point.x-5 >= x and point.x+5 <= x+lane.width:
|
||||
supported = true
|
||||
if tick == 12 and lane.kind != "car":
|
||||
if not supported: return false
|
||||
if lane.kind == "turtle" and lane.sprite == 1 and fposmod(clock+t,7.0) > 5.3: return false
|
||||
return true
|
||||
|
||||
func demo_step() -> Vector2i:
|
||||
if demo_safe(Vector2i.UP): return Vector2i.UP
|
||||
var target_x := 112.0
|
||||
var distance := INF
|
||||
for i in range(5):
|
||||
if not homes[i] and absf(frog.x-HOME_X[i]) < distance:
|
||||
distance = absf(frog.x-HOME_X[i])
|
||||
target_x = HOME_X[i]
|
||||
var side := Vector2i.RIGHT if target_x > frog.x else Vector2i.LEFT
|
||||
if absf(target_x-frog.x)>7 and demo_safe(side): return side
|
||||
return Vector2i.ZERO
|
||||
|
||||
func lane_objects(lane: Dictionary) -> Array[float]:
|
||||
var result: Array[float] = []
|
||||
var start: float = fposmod(lane.offset,lane.spacing)-lane.spacing
|
||||
while start < 224:
|
||||
result.append(start)
|
||||
start += lane.spacing
|
||||
return result
|
||||
|
||||
func submerged(lane: Dictionary) -> bool:
|
||||
return lane.kind == "turtle" and lane.sprite == 1 and fposmod(clock,7.0) > 5.7
|
||||
|
||||
func step_world(delta: float) -> void:
|
||||
time_left -= delta
|
||||
if time_left <= 0:
|
||||
die("time")
|
||||
return
|
||||
if time_left <= 10 and not warned:
|
||||
warned = true
|
||||
$Audio.effect("time")
|
||||
for lane in lanes: lane.offset = fposmod(lane.offset+lane.speed*delta,lane.spacing)
|
||||
if hop_left > 0:
|
||||
hop_left = maxf(0,hop_left-delta)
|
||||
frog = from.lerp(destination,1.0-hop_left/.12)
|
||||
if hop_left <= 0:
|
||||
var row := roundi((232-frog.y)/16)
|
||||
if row > furthest_row:
|
||||
add_score((row-furthest_row)*10)
|
||||
furthest_row = row
|
||||
# Evaluate the current rendered location, including the hop trajectory.
|
||||
var row := roundi((232-frog.y)/16)
|
||||
if row >= 12 and hop_left <= 0:
|
||||
var index := -1
|
||||
for i in range(5):
|
||||
if absf(frog.x-HOME_X[i]) <= 7: index = i
|
||||
if index < 0 or homes[index]:
|
||||
die("squash")
|
||||
else:
|
||||
homes[index] = true
|
||||
add_score(50+int(time_left)*10)
|
||||
phase = "home"
|
||||
phase_time = .8
|
||||
$Audio.effect("extra")
|
||||
return
|
||||
for lane in lanes:
|
||||
if lane.row != row: continue
|
||||
var supported := false
|
||||
for x in lane_objects(lane):
|
||||
if lane.kind == "car":
|
||||
if frog.x+5 > x+2 and frog.x-5 < x+lane.width-2:
|
||||
die("squash")
|
||||
return
|
||||
elif frog.x-4 >= x and frog.x+4 <= x+lane.width and not submerged(lane):
|
||||
supported = true
|
||||
if lane.kind != "car":
|
||||
# Support is checked on landing; hops may cross a platform edge in flight.
|
||||
if hop_left <= 0:
|
||||
if not supported:
|
||||
die("plunk")
|
||||
return
|
||||
frog.x += lane.speed*delta
|
||||
if frog.x < 5 or frog.x > 219: die("plunk")
|
||||
break
|
||||
|
||||
func die(kind: String) -> void:
|
||||
if phase != "play": return
|
||||
lives -= 1
|
||||
death_kind = kind
|
||||
phase = "dead"
|
||||
phase_time = 1.1
|
||||
hop_left = 0
|
||||
$Audio.stop_music()
|
||||
$Audio.effect(kind)
|
||||
|
||||
func add_score(value: int) -> void:
|
||||
score += value
|
||||
if not demo_active: high_score = maxi(high_score,score)
|
||||
if score >= 20000 and not extra_awarded:
|
||||
extra_awarded = true
|
||||
lives += 1
|
||||
$Audio.effect("extra")
|
||||
|
||||
func finish(won: bool) -> void:
|
||||
if demo_active:
|
||||
end_demo()
|
||||
return
|
||||
$Audio.stop_all()
|
||||
leaderboard.append(score)
|
||||
leaderboard.sort()
|
||||
leaderboard.reverse()
|
||||
if leaderboard.size() > 5: leaderboard.resize(5)
|
||||
phase = "won" if won else "lost"
|
||||
if won: win_game()
|
||||
else: lose_game()
|
||||
@@ -0,0 +1 @@
|
||||
uid://fqff3d88cr1j
|
||||
@@ -0,0 +1,35 @@
|
||||
extends Node
|
||||
var voices: Array[AudioStreamPlayer] = []
|
||||
var tracks: Dictionary = {}
|
||||
var tune: AudioStreamPlayer
|
||||
var current := 0
|
||||
func _ready() -> void:
|
||||
for name in ["hop","plunk","squash","time","extra","coin-in"]:
|
||||
tracks[name] = load("res://games/frogger/audio/sound-frogger-"+name+".wav")
|
||||
for i in range(6):
|
||||
var voice := AudioStreamPlayer.new()
|
||||
voice.volume_db = -8
|
||||
add_child(voice)
|
||||
voices.append(voice)
|
||||
tune = AudioStreamPlayer.new()
|
||||
tune.stream = load("res://games/frogger/audio/frogger-music.mp3")
|
||||
tune.volume_db = -12
|
||||
add_child(tune)
|
||||
tune.finished.connect(_music_finished)
|
||||
func effect(name: String) -> void:
|
||||
if get_parent().demo_active: return
|
||||
if not tracks.has(name): return
|
||||
var voice := voices[current]
|
||||
current = (current+1)%voices.size()
|
||||
voice.stream = tracks[name]
|
||||
voice.play()
|
||||
func music() -> void:
|
||||
if get_parent().demo_active: return
|
||||
if not tune.playing: tune.play()
|
||||
func _music_finished() -> void:
|
||||
if get_parent().phase in ["play","home"] and get_parent().music_enabled: tune.play()
|
||||
func stop_music() -> void:
|
||||
if tune: tune.stop()
|
||||
func stop_all() -> void:
|
||||
stop_music()
|
||||
for voice in voices: voice.stop()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dh0mu01mar7nl
|
||||
@@ -0,0 +1,138 @@
|
||||
@tool
|
||||
extends Node2D
|
||||
const SHEET = preload("res://games/frogger/art/spr_frogger.png")
|
||||
const CHARACTERS := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-"
|
||||
var game: Node
|
||||
var atlas: ImageTexture
|
||||
var regions: Dictionary = {}
|
||||
var source: Image
|
||||
func _ready() -> void:
|
||||
game = get_parent().get_parent()
|
||||
source = SHEET.get_image()
|
||||
source.convert(Image.FORMAT_RGBA8)
|
||||
for i in range(8): register("frog%d"%i,Rect2i(1+i*18,1,16,16))
|
||||
for i in range(2): register("attract%d"%i,Rect2i(1+i*18,98,16,16))
|
||||
for i in range(7): register("death%d"%i,Rect2i(1+i*18,80,16,16))
|
||||
for i in range(5): register("car%d"%i,Rect2i(1+i*18,116,32 if i==4 else 16,16))
|
||||
for i in range(3): register("log%d"%i,Rect2i(1+i*18,134,16,16))
|
||||
for i in range(5): register("turtle%d"%i,Rect2i(1+i*18,152,16,16))
|
||||
for i in range(5): register("letter%d"%i,Rect2i(1+i*18,232,16,16))
|
||||
register("home",Rect2i(1,188,32,24))
|
||||
register("hedge",Rect2i(35,188,8,24))
|
||||
register("saved",Rect2i(45,196,16,16))
|
||||
register("bank",Rect2i(135,196,16,16))
|
||||
for color in range(5):
|
||||
for i in range(CHARACTERS.length()):
|
||||
register("font%d_%s"%[color,CHARACTERS[i]],Rect2i(1+(i%17)*9,250+color*28+int(i/17)*9,8,8))
|
||||
atlas = ImageTexture.create_from_image(source)
|
||||
source = null
|
||||
|
||||
func register(name: String,area: Rect2i) -> void:
|
||||
regions[name] = Rect2(area)
|
||||
# Flood only border-connected black/gray padding; preserve enclosed dark detail.
|
||||
var pending: Array[Vector2i] = []
|
||||
for x in range(area.size.x):
|
||||
pending.append(Vector2i(x,0))
|
||||
pending.append(Vector2i(x,area.size.y-1))
|
||||
for y in range(area.size.y):
|
||||
pending.append(Vector2i(0,y))
|
||||
pending.append(Vector2i(area.size.x-1,y))
|
||||
var seen: Dictionary = {}
|
||||
while not pending.is_empty():
|
||||
var p: Vector2i = pending.pop_back()
|
||||
if p.x<0 or p.y<0 or p.x>=area.size.x or p.y>=area.size.y or seen.has(p): continue
|
||||
seen[p] = true
|
||||
var pixel := source.get_pixelv(area.position+p)
|
||||
var key := pixel.is_equal_approx(Color.BLACK) or pixel.is_equal_approx(Color("404040")) or pixel.a == 0
|
||||
if not key: continue
|
||||
source.set_pixelv(area.position+p,Color.TRANSPARENT)
|
||||
for offset in [Vector2i.LEFT,Vector2i.RIGHT,Vector2i.UP,Vector2i.DOWN]: pending.append(p+offset)
|
||||
|
||||
func sprite(name: String,point: Vector2) -> void:
|
||||
var region: Rect2 = regions[name]
|
||||
draw_texture_rect_region(atlas,Rect2(point.round(),region.size),region)
|
||||
func text_at(value: String,point: Vector2,color: int = 0) -> void:
|
||||
for i in range(value.length()):
|
||||
var key := "font%d_%s"%[color,value[i]]
|
||||
if regions.has(key): sprite(key,point+Vector2(i*8,0))
|
||||
func label(value: String,y: float,color: int = 0) -> void:
|
||||
text_at(value,Vector2((224-value.length()*8)/2.0,y),color)
|
||||
func title() -> void:
|
||||
var t: float = game.attract_time
|
||||
for i in range(7):
|
||||
var age: float = t - .5 - i*4
|
||||
if age < 0: continue
|
||||
var x: float = 28+i*24
|
||||
var y := 136.0
|
||||
if age < 3.5:
|
||||
x = lerpf(232,x,floorf(age*12)/42.0)
|
||||
sprite("attract1",Vector2(x,y))
|
||||
elif t < 29:
|
||||
sprite("attract0",Vector2(x,y))
|
||||
elif t < 33:
|
||||
y = lerpf(136,55,floorf((t-29)*12)/48.0)
|
||||
sprite("frog%d"%(int(t*6)%2),Vector2(x,y))
|
||||
elif t < 33+i*.45:
|
||||
sprite("attract0",Vector2(x,55))
|
||||
else:
|
||||
sprite("letter%d"%[0,1,2,3,3,4,1][i],Vector2(x,55))
|
||||
if t >= 36: label("-POINT TABLE-",89)
|
||||
if t >= 37: label("10 PTS FOR EACH STEP",115,1)
|
||||
if t >= 38:
|
||||
label("50 PTS FOR EVERY FROG",139,1)
|
||||
label("ARRIVED HOME SAFELY",151,2)
|
||||
if t >= 39:
|
||||
label("1000 PTS FOR FIVE",177,1)
|
||||
label("FROGS HOME",189,1)
|
||||
if int(t*2)%2 == 0: label("PUSH START",219,4)
|
||||
label("FREE PLAY",247,4)
|
||||
func board() -> void:
|
||||
for x in range(0,224,8): sprite("hedge",Vector2(x,24))
|
||||
for i in range(5):
|
||||
draw_rect(Rect2(game.HOME_X[i]-16,24,32,24),Color("000047"))
|
||||
sprite("home",Vector2(game.HOME_X[i]-16,24))
|
||||
if game.homes[i]: sprite("saved",Vector2(game.HOME_X[i]-8,32))
|
||||
for y in [128,224]:
|
||||
for x in range(0,224,16): sprite("bank",Vector2(x,y))
|
||||
for lane in game.lanes:
|
||||
var y: float = 232-lane.row*16-8
|
||||
for x in game.lane_objects(lane):
|
||||
match lane.kind:
|
||||
"car": sprite("car%d"%lane.sprite,Vector2(x,y))
|
||||
"log":
|
||||
var tiles := int(lane.width/16)
|
||||
for i in range(tiles): sprite("log%d"%(0 if i==0 else (2 if i==tiles-1 else 1)),Vector2(x+i*16,y))
|
||||
"turtle":
|
||||
if game.submerged(lane): continue
|
||||
var frame := int(game.clock*5)%3
|
||||
if lane.sprite == 1 and fposmod(game.clock,7.0)>5:
|
||||
frame = 3 if fposmod(game.clock,7.0)<5.4 else 4
|
||||
for i in range(int(lane.width/16)): sprite("turtle%d"%frame,Vector2(x+i*16,y))
|
||||
if game.phase == "dead":
|
||||
var frame := clampi(int((1.1-game.phase_time)*7),0,6)
|
||||
sprite("death%d"%frame,game.frog-Vector2(8,8))
|
||||
elif game.phase not in ["home","clear","won","lost"]:
|
||||
sprite("frog%d"%(game.direction*2+(1 if game.hop_left>0 else 0)),game.frog-Vector2(8,8))
|
||||
for i in range(mini(game.lives,6)): sprite("frog0",Vector2(i*12,239))
|
||||
var fraction: float = clampf(game.time_left/game.crossing_time,0,1)
|
||||
draw_rect(Rect2(88,248,104*fraction,8),Color("e00000") if fraction<.167 else Color("00c000"))
|
||||
text_at("TIME",Vector2(192,248),1)
|
||||
if game.phase == "ready": label("PLAYER 1",135,2)
|
||||
if game.phase == "clear": label("FIVE FROGS HOME",135,1)
|
||||
if game.phase == "won": label("YOU WIN",135,1)
|
||||
if game.phase == "lost": label("GAME OVER",135,2)
|
||||
if game.demo_active and int(game.demo_time*2)%2 == 0: label("GAME OVER",135,2)
|
||||
func _draw() -> void:
|
||||
if game == null or atlas == null: return
|
||||
draw_rect(Rect2(0,0,224,256),Color.BLACK)
|
||||
draw_rect(Rect2(0,0,224,128),Color("000047"))
|
||||
text_at("1-UP",Vector2(24,0))
|
||||
text_at("HI-SCORE",Vector2(104,0))
|
||||
text_at("%05d"%game.score,Vector2(16,8),2)
|
||||
text_at("%05d"%game.high_score,Vector2(112,8),2)
|
||||
if game.phase == "title": title()
|
||||
elif game.phase == "scores":
|
||||
label("HIGH SCORES",55,1)
|
||||
for i in range(5): label("%d %05d"%[i+1,game.leaderboard[i]] if i<game.leaderboard.size() else "%d -----"%(i+1),88+i*20)
|
||||
label("PUSH START",218,4)
|
||||
else: board()
|
||||
@@ -0,0 +1 @@
|
||||
uid://df5twn4utxg0n
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene format=3 uid="uid://byorfa7yxhhrv"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b6ui86lx5kkpa" path="res://games/galaga/galaga.gd" id="1"]
|
||||
|
||||
[node name="Galaga" type="Node2D" unique_id=633520072]
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://games/galaga/actors/actor.gd" id="1"]
|
||||
[node name="Enemy" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
kind = "boss"
|
||||
radius = 6.0
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://games/galaga/actors/actor.gd" id="1"]
|
||||
[node name="Explosion" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
kind = "explosion"
|
||||
radius = 0.0
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://games/galaga/actors/actor.gd" id="1"]
|
||||
[node name="Fighter" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
kind = "fighter"
|
||||
radius = 6.0
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://games/galaga/actors/actor.gd" id="1"]
|
||||
[node name="Projectile" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
kind = "shot"
|
||||
radius = 2.0
|
||||
@@ -0,0 +1,61 @@
|
||||
@tool
|
||||
extends Node2D
|
||||
## Visual and collision bounds share the node origin, in native game pixels.
|
||||
@export_enum("fighter", "boss", "butterfly", "bee", "shot", "enemy_shot", "explosion", "enemy_explosion") var kind := "fighter":
|
||||
set(value):
|
||||
kind = value
|
||||
queue_redraw()
|
||||
@export var damaged := false
|
||||
@export var radius := 6.0
|
||||
var animation_time := 0.0
|
||||
var spinning := false
|
||||
var spin_time := 0.0
|
||||
@export var heading := Vector2.DOWN
|
||||
const SHEET = preload("res://games/galaga/art/spr_galaga.png")
|
||||
const DIRECTION_ROWS := {"boss":36, "boss_damaged":54, "butterfly":72, "bee":108}
|
||||
|
||||
func _ready() -> void:
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
if kind not in ["shot", "enemy_shot"]:
|
||||
var sprite_material := ShaderMaterial.new()
|
||||
sprite_material.shader = preload("res://games/galaga/actors/sprite.gdshader")
|
||||
material = sprite_material
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
# Quantize the rendered center in native 224x288 coordinates, before the
|
||||
# view's integer enlargement. Keep simulation positions precise.
|
||||
draw_set_transform(position.round()-position)
|
||||
if kind in ["shot", "enemy_shot"]:
|
||||
draw_rect(Rect2(-1, -3, 2, 6), Color.CYAN if kind == "shot" else Color.SALMON)
|
||||
return
|
||||
if kind == "explosion":
|
||||
var frame := mini(3, int(animation_time * 12))
|
||||
draw_texture_rect_region(SHEET, Rect2(-16,-16,32,32), Rect2(145 + frame * 34, 1, 32,32))
|
||||
return
|
||||
if kind == "enemy_explosion":
|
||||
var frame := mini(4, int(animation_time * 15))
|
||||
draw_texture_rect_region(SHEET, Rect2(-16,-16,32,32), Rect2(289+frame*34,1,32,32))
|
||||
return
|
||||
var source := Vector2(108,0)
|
||||
var destination := Rect2(-8,-8,16,16)
|
||||
if kind=="fighter" and spinning:
|
||||
var facing := Vector2.UP.rotated(spin_time*TAU*2)
|
||||
var column := 6-clampi(roundi(atan2(absf(facing.x),absf(facing.y))/(PI/12)),0,6)
|
||||
source=Vector2(column*18,0)
|
||||
if facing.x>0: destination.size.x=-16
|
||||
if facing.y>0: destination.size.y=-16
|
||||
if kind in ["boss", "butterfly", "bee"]:
|
||||
# Seven authored angles from right to down, at 15-degree intervals.
|
||||
# Mirror these pixels into the other quadrants; never rotate the texture.
|
||||
var column := 6 - clampi(roundi(atan2(absf(heading.x),absf(heading.y))/(PI/12)),0,6)
|
||||
# Adjacent rows are different sprites, not interchangeable wing frames.
|
||||
var row: int = DIRECTION_ROWS["boss_damaged" if kind == "boss" and damaged else kind]
|
||||
source = Vector2(column*18,row)
|
||||
if heading.x < 0: destination.size.x = -16
|
||||
if heading.y < 0: destination.size.y = -16
|
||||
draw_texture_rect_region(SHEET, destination, Rect2(source+Vector2.ONE,Vector2(16,16)))
|
||||
if Engine.is_editor_hint():
|
||||
draw_arc(Vector2.ZERO, radius, 0, TAU, 24, Color(0,1,0,0.5))
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8jf7ijduugag
|
||||
@@ -0,0 +1,7 @@
|
||||
shader_type canvas_item;
|
||||
render_mode unshaded;
|
||||
void fragment() {
|
||||
vec4 pixel = texture(TEXTURE, UV);
|
||||
if (max(pixel.r, max(pixel.g, pixel.b)) < 0.02) { discard; }
|
||||
COLOR *= pixel;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://m8sm8co7m5hs
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c8tq40qfjbj0j"
|
||||
path="res://.godot/imported/spr_galaga.png-14ddb3514e589ecbada54d6f3d3d9c6a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/art/spr_galaga.png"
|
||||
dest_files=["res://.godot/imported/spr_galaga.png-14ddb3514e589ecbada54d6f3d3d9c6a.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b73fvvkpj1n0m"
|
||||
path="res://.godot/imported/spr_galaga_screens_fonts.png-903d74438a2f25937f64b812fa39ec5f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/art/spr_galaga_screens_fonts.png"
|
||||
dest_files=["res://.godot/imported/spr_galaga_screens_fonts.png-903d74438a2f25937f64b812fa39ec5f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dncbkjieopw2j"
|
||||
path="res://.godot/imported/boss_damage.wav-78e99d1a33486a6035ee91fa662ba934.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/boss_damage.wav"
|
||||
dest_files=["res://.godot/imported/boss_damage.wav-78e99d1a33486a6035ee91fa662ba934.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://daxutfef332d0"
|
||||
path="res://.godot/imported/boss_destroy.wav-e354d8227cc7537a700b34f5fe73f561.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/boss_destroy.wav"
|
||||
dest_files=["res://.godot/imported/boss_destroy.wav-e354d8227cc7537a700b34f5fe73f561.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://cx3ne7dset50h"
|
||||
path="res://.godot/imported/captured_ship_destroy.wav-00ca325f65364f942d197e896286ff9e.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/captured_ship_destroy.wav"
|
||||
dest_files=["res://.godot/imported/captured_ship_destroy.wav-00ca325f65364f942d197e896286ff9e.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dj5w28f8vfe11"
|
||||
path="res://.godot/imported/capturing.wav-31e55e3bb216de5d977b1eb2a70fa55e.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/capturing.wav"
|
||||
dest_files=["res://.godot/imported/capturing.wav-31e55e3bb216de5d977b1eb2a70fa55e.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://hkxrmuwhnjm8"
|
||||
path="res://.godot/imported/challenging_stage_perfect.wav-108af1f9129534a9994485cfa520c75c.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/challenging_stage_perfect.wav"
|
||||
dest_files=["res://.godot/imported/challenging_stage_perfect.wav-108af1f9129534a9994485cfa520c75c.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://14q74uylektm"
|
||||
path="res://.godot/imported/challenging_stage_results.wav-dab64a66c50b7f1993c4c2c3acd961b7.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/challenging_stage_results.wav"
|
||||
dest_files=["res://.godot/imported/challenging_stage_results.wav-dab64a66c50b7f1993c4c2c3acd961b7.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://bdhprhh6occ5v"
|
||||
path="res://.godot/imported/challenging_stage_start.wav-73d928e3ae25ec568dc7063663edd538.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/challenging_stage_start.wav"
|
||||
dest_files=["res://.godot/imported/challenging_stage_start.wav-73d928e3ae25ec568dc7063663edd538.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dxrhspw3c16qk"
|
||||
path="res://.godot/imported/credit.wav-700cc7936bb34b0b15042241f97b45ed.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/credit.wav"
|
||||
dest_files=["res://.godot/imported/credit.wav-700cc7936bb34b0b15042241f97b45ed.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://cs3pmp26xbwhk"
|
||||
path="res://.godot/imported/dive.wav-5a74a6bfe1592a6aaea72d344d9ca9e3.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/dive.wav"
|
||||
dest_files=["res://.godot/imported/dive.wav-5a74a6bfe1592a6aaea72d344d9ca9e3.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://cpyt1s7qanl8q"
|
||||
path="res://.godot/imported/explosion.wav-6c4c21ff43ef8c0e71bcf43356fc5b5a.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/explosion.wav"
|
||||
dest_files=["res://.godot/imported/explosion.wav-6c4c21ff43ef8c0e71bcf43356fc5b5a.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://bvquw0oosnxqh"
|
||||
path="res://.godot/imported/extend.wav-6f07b0f7aff14e3922701a3466431816.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/extend.wav"
|
||||
dest_files=["res://.godot/imported/extend.wav-6f07b0f7aff14e3922701a3466431816.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://ddwn4qvfxdksy"
|
||||
path="res://.godot/imported/fighter_captured.wav-de09c8e5ce22fdac89b516e365c9b080.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/fighter_captured.wav"
|
||||
dest_files=["res://.godot/imported/fighter_captured.wav-de09c8e5ce22fdac89b516e365c9b080.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://33yiax8gm27x"
|
||||
path="res://.godot/imported/fighter_rescued.wav-104f1a0d3458b6b0c5fe2b4988a05239.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/fighter_rescued.wav"
|
||||
dest_files=["res://.godot/imported/fighter_rescued.wav-104f1a0d3458b6b0c5fe2b4988a05239.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://be1e74l1loh7n"
|
||||
path="res://.godot/imported/fire.wav-5f3244d6c331a676322539de15b7dd8a.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/fire.wav"
|
||||
dest_files=["res://.godot/imported/fire.wav-5f3244d6c331a676322539de15b7dd8a.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://btk1xrnbeclk5"
|
||||
path="res://.godot/imported/goei_destroy.wav-8823f818c7ebeab88cbc5bbce5e497a6.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/goei_destroy.wav"
|
||||
dest_files=["res://.godot/imported/goei_destroy.wav-8823f818c7ebeab88cbc5bbce5e497a6.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://caafa28sxwha2"
|
||||
path="res://.godot/imported/name_entry.wav-ac9a4ac5de170d13f1cf215b46ecf4be.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/name_entry.wav"
|
||||
dest_files=["res://.godot/imported/name_entry.wav-ac9a4ac5de170d13f1cf215b46ecf4be.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://bwveio4bx3bcg"
|
||||
path="res://.godot/imported/name_entry_top_end.wav-746289f541bb0b67aa212f88d14d1e29.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/name_entry_top_end.wav"
|
||||
dest_files=["res://.godot/imported/name_entry_top_end.wav-746289f541bb0b67aa212f88d14d1e29.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://b2pbd5dqivcjh"
|
||||
path="res://.godot/imported/name_entry_top_firstloop.wav-9dd53a38e65854032fde87dfd496ebf4.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/name_entry_top_firstloop.wav"
|
||||
dest_files=["res://.godot/imported/name_entry_top_firstloop.wav-9dd53a38e65854032fde87dfd496ebf4.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://7vox07aqyd22"
|
||||
path="res://.godot/imported/name_entry_top_loop.wav-ec1fa871accf7a1b3d2bb99e5301f70a.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/name_entry_top_loop.wav"
|
||||
dest_files=["res://.godot/imported/name_entry_top_loop.wav-ec1fa871accf7a1b3d2bb99e5301f70a.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://8tpl8twpsre2"
|
||||
path="res://.godot/imported/stage_count.wav-5dd59d6a7f3b9370e4221a44921f54ff.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/stage_count.wav"
|
||||
dest_files=["res://.godot/imported/stage_count.wav-5dd59d6a7f3b9370e4221a44921f54ff.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dhwcq3grnk6ex"
|
||||
path="res://.godot/imported/start.wav-b157900079964c24f391d1f6cfcfba00.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/start.wav"
|
||||
dest_files=["res://.godot/imported/start.wav-b157900079964c24f391d1f6cfcfba00.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://7iue027ylkmd"
|
||||
path="res://.godot/imported/tractor_beam.wav-81aae0e2d4adf83ca0a76ed52762592a.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/tractor_beam.wav"
|
||||
dest_files=["res://.godot/imported/tractor_beam.wav-81aae0e2d4adf83ca0a76ed52762592a.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://bvh5e81wnestw"
|
||||
path="res://.godot/imported/transform.wav-20248f23471e9e0a259acc1e86aba9be.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/transform.wav"
|
||||
dest_files=["res://.godot/imported/transform.wav-20248f23471e9e0a259acc1e86aba9be.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://c8lhi71x0bxk"
|
||||
path="res://.godot/imported/zako_destroy.wav-d927ad0cf990543c87414951c2478a97.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/galaga/audio/zako_destroy.wav"
|
||||
dest_files=["res://.godot/imported/zako_destroy.wav-d927ad0cf990543c87414951c2478a97.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
@@ -0,0 +1,523 @@
|
||||
@tool
|
||||
extends ArcadeGame
|
||||
const ACTOR = preload("res://games/galaga/actors/actor.gd")
|
||||
const FIGHTER = preload("res://games/galaga/actors/Fighter.tscn")
|
||||
const ENEMY = preload("res://games/galaga/actors/Enemy.tscn")
|
||||
const PROJECTILE = preload("res://games/galaga/actors/Projectile.tscn")
|
||||
const EXPLOSION = preload("res://games/galaga/actors/Explosion.tscn")
|
||||
@export_group("Room Objective")
|
||||
@export_range(0,100,1) var stages_to_win := 1
|
||||
@export_range(0,1000000,100) var score_to_win := 0
|
||||
@export_range(1,9,1) var starting_lives := 3
|
||||
@export var extra_life_score := 20000
|
||||
@export var second_extra_life_score := 70000
|
||||
@export var repeat_extra_life_interval := 70000
|
||||
var next_extra_life := 20000
|
||||
var stage := 1
|
||||
var score := 0
|
||||
var lives := 3
|
||||
var shots_fired := 0
|
||||
var hits := 0
|
||||
var state := "idle"
|
||||
var timer := 0.0
|
||||
var clock := 0.0
|
||||
var cooldown := 0.0
|
||||
var dive_timer := 2.0
|
||||
var invulnerable := 0.0
|
||||
var awarded := false
|
||||
var player: Node2D
|
||||
var enemies: Array[Dictionary] = []
|
||||
var bullets: Array[Dictionary] = []
|
||||
var effects: Array[Dictionary] = []
|
||||
var voices: Array[AudioStreamPlayer] = []
|
||||
var streams := {}
|
||||
var field: Node2D
|
||||
var hud: Node2D
|
||||
var rng := RandomNumberGenerator.new()
|
||||
@export_group("Opening Timing")
|
||||
@export var intro_seconds := 6.0
|
||||
@export var stage_card_seconds := 1.0
|
||||
@export var player_card_seconds := 1.0
|
||||
@export_group("Cabinet")
|
||||
@export var credits := 1
|
||||
var wingman: Node2D
|
||||
var captive: Node2D
|
||||
var captor: Dictionary = {}
|
||||
var dual := false
|
||||
var capture_origin := Vector2.ZERO
|
||||
var capture_cooldown := 8.0
|
||||
var beam: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
pass
|
||||
field = preload("res://games/galaga/galaga_view.gd").new()
|
||||
add_child(field)
|
||||
player = FIGHTER.instantiate()
|
||||
field.add_child(player)
|
||||
player.position = Vector2(112,256)
|
||||
wingman = FIGHTER.instantiate()
|
||||
field.add_child(wingman)
|
||||
wingman.visible = false
|
||||
captive = FIGHTER.instantiate()
|
||||
field.add_child(captive)
|
||||
captive.visible = false
|
||||
captive.spinning = false
|
||||
captive.modulate = Color(1,0.4,0.4)
|
||||
_spawn_wave(true)
|
||||
if not Engine.is_editor_hint():
|
||||
for i in range(8):
|
||||
var voice := AudioStreamPlayer.new()
|
||||
add_child(voice)
|
||||
voices.append(voice)
|
||||
for name in ["start","fire","dive","explosion","boss_damage","boss_destroy","goei_destroy","zako_destroy","extend","stage_count","tractor_beam","capturing","fighter_captured","fighter_rescued","captured_ship_destroy"]:
|
||||
streams[name] = load("res://games/galaga/audio/%s.wav" % name)
|
||||
|
||||
func start_game() -> bool:
|
||||
if is_running or Engine.is_editor_hint(): return false
|
||||
_clear_actors()
|
||||
score = 0
|
||||
stage = 1
|
||||
lives = clampi(int(run_config.get("starting_lives",starting_lives)),1,9)
|
||||
shots_fired = 0
|
||||
hits = 0
|
||||
awarded = false
|
||||
next_extra_life = extra_life_score
|
||||
clock = 0
|
||||
credits = maxi(0,credits-1)
|
||||
cooldown = 0
|
||||
invulnerable = 0
|
||||
player.position = Vector2(112,256)
|
||||
player.visible = false
|
||||
state = "intro"
|
||||
timer = intro_seconds
|
||||
dual = false
|
||||
wingman.visible = false
|
||||
captive.visible = false
|
||||
captive.spinning = false
|
||||
captor = {}
|
||||
beam = {}
|
||||
capture_cooldown = 8.0
|
||||
_spawn_wave()
|
||||
_sound("start")
|
||||
return super.start_game()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
for voice in voices:
|
||||
voice.stop()
|
||||
voice.stream = null
|
||||
|
||||
func reset_game() -> void:
|
||||
_clear_actors()
|
||||
for voice in voices: voice.stop()
|
||||
state = "idle"
|
||||
dual = false
|
||||
beam = {}
|
||||
captor = {}
|
||||
captive.visible = false
|
||||
wingman.visible = false
|
||||
player.visible = true
|
||||
player.position = Vector2(112,256)
|
||||
_spawn_wave(true)
|
||||
super.reset_game()
|
||||
|
||||
func win_game() -> void:
|
||||
if not is_running or state=="victory": return
|
||||
for voice in voices: voice.stop()
|
||||
state = "victory"
|
||||
timer = 1.5
|
||||
beam = {}
|
||||
|
||||
func lose_game() -> void:
|
||||
if not is_running: return
|
||||
for voice in voices: voice.stop()
|
||||
state = "lost"
|
||||
super.lose_game()
|
||||
|
||||
func _clear_actors() -> void:
|
||||
for collection in [enemies,bullets,effects]:
|
||||
for item in collection: item.node.queue_free()
|
||||
collection.clear()
|
||||
|
||||
func _spawn_wave(preview := false) -> void:
|
||||
dive_timer = 2.0
|
||||
for row in range(5):
|
||||
var count := 4 if row == 0 else (8 if row < 3 else 10)
|
||||
for col in range(count):
|
||||
var node = ENEMY.instantiate()
|
||||
node.kind = "boss" if row == 0 else ("butterfly" if row < 3 else "bee")
|
||||
field.add_child(node)
|
||||
var home := Vector2(112 + (col - (count-1)/2.0)*18,48+row*18)
|
||||
node.position = home if preview else Vector2(-24,-24)
|
||||
node.visible = preview
|
||||
var index := enemies.size()
|
||||
enemies.append({"node":node,"home":home,"mode":"formation" if preview else "enter","time":-(index/8)*1.1-(index%8)*0.075,"side":-1.0 if (index/8)%2==0 else 1.0,"hp":2 if row==0 else 1,"origin":Vector2.ZERO,"target":112.0,"fired":false})
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if Engine.is_editor_hint(): return
|
||||
if event.is_action_pressed("arcade_start"):
|
||||
start_game()
|
||||
|
||||
func _held(action: String) -> bool:
|
||||
return Input.is_action_pressed(action)
|
||||
|
||||
func _physics_process(dt: float) -> void:
|
||||
if Engine.is_editor_hint(): return
|
||||
if state=="won":
|
||||
clock += dt
|
||||
return
|
||||
if not is_running: return
|
||||
_tick(dt)
|
||||
|
||||
func _tick(dt: float) -> void:
|
||||
clock += dt
|
||||
for i in range(effects.size()-1,-1,-1):
|
||||
var effect: Dictionary = effects[i]
|
||||
effect.node.animation_time += dt
|
||||
if effect.node.animation_time >= 0.34:
|
||||
effect.node.queue_free()
|
||||
effects.remove_at(i)
|
||||
if state != "playing":
|
||||
timer -= dt
|
||||
if state=="victory":
|
||||
_update_peaceful_formation()
|
||||
for enemy in enemies:
|
||||
if enemy.mode!="formation" and enemy.node.visible:
|
||||
enemy.node.position += enemy.node.heading*70*dt
|
||||
for i in range(bullets.size()-1,-1,-1):
|
||||
var bullet: Dictionary = bullets[i]
|
||||
bullet.node.position += bullet.velocity*dt
|
||||
if not Rect2(-16,-16,256,320).has_point(bullet.node.position):
|
||||
bullet.node.queue_free()
|
||||
bullets.remove_at(i)
|
||||
if timer<=0:
|
||||
_clear_bullets()
|
||||
state="won"
|
||||
super.win_game()
|
||||
return
|
||||
if state == "capturing":
|
||||
var t := clampf(1-timer/1.5,0,1)
|
||||
player.position = capture_origin.lerp(captor.node.position-Vector2(0,16),t)
|
||||
if timer<=0: _finish_capture()
|
||||
return
|
||||
if state in ["captured_return", "capture_ready", "rescue_spin", "rescue_center", "rescue_align", "rescue_descend"]:
|
||||
_update_peaceful_formation()
|
||||
match state:
|
||||
"captured_return":
|
||||
captor.node.position = captor.node.position.move_toward(_formation_home(captor),100*dt)
|
||||
captive.position = captor.node.position-Vector2(0,16)
|
||||
if timer<=0 and captor.node.position.distance_to(_formation_home(captor))<1:
|
||||
captor.mode="formation"
|
||||
if lives<=0:
|
||||
lose_game()
|
||||
else:
|
||||
player.position=Vector2(112,256)
|
||||
player.visible=true
|
||||
state="capture_ready"
|
||||
timer=1.5
|
||||
"capture_ready":
|
||||
if timer<=0: _resume_action()
|
||||
"rescue_spin":
|
||||
captive.spin_time += dt
|
||||
if timer<=0:
|
||||
captive.spinning=false
|
||||
state="rescue_center"
|
||||
"rescue_center":
|
||||
player.position = player.position.move_toward(Vector2(104,256),100*dt)
|
||||
if player.position.distance_to(Vector2(104,256))<0.1: state="rescue_align"
|
||||
"rescue_align":
|
||||
captive.position.x = move_toward(captive.position.x,120,100*dt)
|
||||
if absf(captive.position.x-120)<0.1: state="rescue_descend"
|
||||
"rescue_descend":
|
||||
captive.position = captive.position.move_toward(Vector2(120,256),110*dt)
|
||||
if captive.position.distance_to(Vector2(120,256))<0.1:
|
||||
dual=true
|
||||
captive.visible=false
|
||||
wingman.position=Vector2(120,256)
|
||||
wingman.visible=true
|
||||
_resume_action()
|
||||
return
|
||||
if timer <= 0:
|
||||
if state == "intro":
|
||||
state = "stage_card"
|
||||
timer = stage_card_seconds
|
||||
_sound("stage_count")
|
||||
elif state == "stage_card":
|
||||
state = "ready"
|
||||
timer = player_card_seconds
|
||||
player.visible = true
|
||||
elif state == "ready": state = "playing"
|
||||
elif state == "death":
|
||||
if lives <= 0: lose_game()
|
||||
else:
|
||||
player.position = Vector2(112,256)
|
||||
player.visible = true
|
||||
invulnerable = 2.5
|
||||
state = "playing"
|
||||
elif state == "clear":
|
||||
stage += 1
|
||||
_spawn_wave()
|
||||
state = "stage_card"
|
||||
player.visible = false
|
||||
wingman.visible = false
|
||||
timer = stage_card_seconds
|
||||
_sound("stage_count")
|
||||
return
|
||||
cooldown = maxf(0,cooldown-dt)
|
||||
invulnerable = maxf(0,invulnerable-dt)
|
||||
player.visible = invulnerable == 0 or int(clock*12)%2==0
|
||||
var movement := float(_held("arcade_right"))-float(_held("arcade_left"))
|
||||
player.position.x = clampf(player.position.x+movement*110*dt,8,216)
|
||||
if dual: player.position.x = minf(player.position.x,200)
|
||||
wingman.position = player.position+Vector2(16,0)
|
||||
wingman.visible = dual and player.visible
|
||||
if _held("arcade_action_1"): _fire()
|
||||
_update_enemies(dt)
|
||||
if state != "playing": return
|
||||
_update_bullets(dt)
|
||||
if state != "playing": return
|
||||
for enemy in enemies:
|
||||
if (enemy.node.position.distance_to(player.position) < 11 or (dual and enemy.node.position.distance_to(wingman.position)<11)) and invulnerable <= 0:
|
||||
_die()
|
||||
return
|
||||
if enemies.is_empty():
|
||||
var target := int(run_config.get("stages_to_win", stages_to_win))
|
||||
if target > 0 and stage >= target: win_game()
|
||||
else:
|
||||
state = "clear"
|
||||
timer = 2
|
||||
_clear_bullets()
|
||||
|
||||
func _update_enemies(dt: float) -> void:
|
||||
dive_timer -= dt
|
||||
capture_cooldown -= dt
|
||||
var entering := false
|
||||
for e in enemies:
|
||||
if e.mode=="enter": entering=true
|
||||
for enemy in enemies:
|
||||
var previous_position: Vector2 = enemy.node.position
|
||||
enemy.time += dt
|
||||
var expansion := 1.0+0.10*(0.5+0.5*sin(clock*1.7))
|
||||
var home: Vector2 = _formation_home(enemy)
|
||||
match enemy.mode:
|
||||
"enter":
|
||||
if enemy.time < 0:
|
||||
enemy.node.visible = false
|
||||
continue
|
||||
enemy.node.visible = true
|
||||
var t := clampf(enemy.time/2.0,0,1)
|
||||
var start := Vector2(112+enemy.side*140,160)
|
||||
if t<0.8:
|
||||
# Every member samples one shared route; its slot affects only docking.
|
||||
var u := t/0.8
|
||||
enemy.node.position = start.lerp(Vector2(112,90),u) + Vector2(-enemy.side*sin(u*TAU)*72,-sin(u*PI)*105)
|
||||
else:
|
||||
enemy.node.position = Vector2(112,90).lerp(home,(t-0.8)/0.2)
|
||||
if t>=1: enemy.mode = "formation"
|
||||
"formation":
|
||||
enemy.node.position = home
|
||||
if not entering and capture_cooldown<=0 and enemy.node.kind=="boss" and captor.is_empty() and beam.is_empty() and not dual:
|
||||
enemy.mode="beam_approach"
|
||||
enemy.origin=home
|
||||
enemy.target=player.position.x
|
||||
enemy.time=0
|
||||
beam=enemy
|
||||
capture_cooldown=15
|
||||
elif dive_timer <= 0 and not entering:
|
||||
enemy.mode = "dive"
|
||||
enemy.time = 0
|
||||
enemy.origin = home
|
||||
enemy.target = player.position.x
|
||||
enemy.fired = false
|
||||
dive_timer = maxf(0.45,2.0-stage*0.12)
|
||||
_sound("dive")
|
||||
"dive":
|
||||
var t: float = enemy.time/(3.0/maxf(1,1+stage*0.04))
|
||||
enemy.node.position = Vector2(lerpf(enemy.origin.x,enemy.target,minf(t,1))+sin(t*TAU)*25,enemy.origin.y+t*250)
|
||||
if t>0.35 and not enemy.fired:
|
||||
enemy.fired = true
|
||||
_bullet(enemy.node.position,(player.position-enemy.node.position).normalized()*95,false)
|
||||
if enemy.node.position.y>310:
|
||||
enemy.mode = "return"
|
||||
enemy.time = 0
|
||||
"return":
|
||||
var t := minf(1,enemy.time/1.0)
|
||||
enemy.node.position = Vector2(home.x,lerpf(-20,home.y,t))
|
||||
if t>=1: enemy.mode = "formation"
|
||||
"beam_approach":
|
||||
var t := minf(enemy.time/1.3,1)
|
||||
enemy.node.position=enemy.origin.lerp(Vector2(enemy.target,172),t)
|
||||
if t>=1:
|
||||
enemy.mode="beam"
|
||||
enemy.time=0
|
||||
_sound("tractor_beam")
|
||||
"beam":
|
||||
if invulnerable<=0 and absf(player.position.x-enemy.node.position.x)<18:
|
||||
captor=enemy
|
||||
capture_origin=player.position
|
||||
state="capturing"
|
||||
timer=1.5
|
||||
_clear_bullets()
|
||||
_stop_sounds()
|
||||
_sound("capturing")
|
||||
return
|
||||
if enemy.time>=3:
|
||||
enemy.mode="return"
|
||||
enemy.time=0
|
||||
beam={}
|
||||
enemy.node.animation_time += dt
|
||||
if enemy.mode in ["dive","enter","return"]:
|
||||
var travel: Vector2 = enemy.node.position-previous_position
|
||||
if travel.length_squared()>0.01: enemy.node.heading = travel.normalized()
|
||||
else: enemy.node.heading = Vector2.DOWN
|
||||
if not captor.is_empty() and captive.visible:
|
||||
captive.position=captor.node.position-Vector2(0,16)
|
||||
|
||||
func _fire() -> void:
|
||||
if cooldown>0: return
|
||||
var count := 0
|
||||
for bullet in bullets:
|
||||
if bullet.friendly: count += 1
|
||||
if count>=(4 if dual else 2): return
|
||||
cooldown = 0.18
|
||||
shots_fired += 1
|
||||
_bullet(player.position-Vector2(0,9),Vector2(0,-230),true)
|
||||
if dual: _bullet(wingman.position-Vector2(0,9),Vector2(0,-230),true)
|
||||
_sound("fire")
|
||||
|
||||
func _bullet(at: Vector2, velocity: Vector2, friendly: bool) -> void:
|
||||
var node = PROJECTILE.instantiate()
|
||||
node.kind = "shot" if friendly else "enemy_shot"
|
||||
node.position = at
|
||||
field.add_child(node)
|
||||
bullets.append({"node":node,"velocity":velocity,"friendly":friendly})
|
||||
|
||||
func _update_bullets(dt: float) -> void:
|
||||
for i in range(bullets.size()-1,-1,-1):
|
||||
var bullet: Dictionary = bullets[i]
|
||||
bullet.node.position += bullet.velocity*dt
|
||||
var remove: bool = bullet.node.position.y < -8 or bullet.node.position.y>296
|
||||
if bullet.friendly:
|
||||
if captive.visible and state=="playing" and bullet.node.position.distance_to(captive.position)<8:
|
||||
captive.visible=false
|
||||
captor={}
|
||||
_sound("captured_ship_destroy")
|
||||
bullet.node.queue_free()
|
||||
bullets.remove_at(i)
|
||||
continue
|
||||
for j in range(enemies.size()-1,-1,-1):
|
||||
var enemy: Dictionary = enemies[j]
|
||||
if bullet.node.position.distance_to(enemy.node.position)>9: continue
|
||||
hits += 1
|
||||
enemy.hp -= 1
|
||||
remove = true
|
||||
if enemy.hp <= 0:
|
||||
var kind: String = enemy.node.kind
|
||||
var points: int = {"boss":150,"butterfly":80,"bee":50}[kind]
|
||||
if enemy.mode == "dive": points *= 2
|
||||
_explode(enemy.node.position,true)
|
||||
if enemy==beam: beam={}
|
||||
if enemy==captor:
|
||||
captor={}
|
||||
state="rescue_spin"
|
||||
timer=1.5
|
||||
captive.spinning=true
|
||||
captive.spin_time=0
|
||||
_stop_sounds()
|
||||
_sound("fighter_rescued")
|
||||
enemy.node.queue_free()
|
||||
enemies.remove_at(j)
|
||||
_sound({"boss":"boss_destroy","butterfly":"goei_destroy","bee":"zako_destroy"}[kind])
|
||||
_add_score(points)
|
||||
else:
|
||||
enemy.node.damaged = true
|
||||
_sound("boss_damage")
|
||||
break
|
||||
elif invulnerable<=0 and (bullet.node.position.distance_to(player.position)<8 or (dual and bullet.node.position.distance_to(wingman.position)<8)):
|
||||
_die()
|
||||
return
|
||||
if remove:
|
||||
bullet.node.queue_free()
|
||||
bullets.remove_at(i)
|
||||
if state != "playing": return
|
||||
|
||||
func _add_score(points: int) -> void:
|
||||
score += points
|
||||
while next_extra_life>0 and score>=next_extra_life:
|
||||
var first := not awarded
|
||||
awarded = true
|
||||
lives += 1
|
||||
_sound("extend")
|
||||
if first and second_extra_life_score>next_extra_life:
|
||||
next_extra_life = second_extra_life_score
|
||||
elif repeat_extra_life_interval>0:
|
||||
next_extra_life += repeat_extra_life_interval
|
||||
else: next_extra_life = 0
|
||||
var target := int(run_config.get("score_to_win",score_to_win))
|
||||
if target>0 and score>=target: win_game()
|
||||
|
||||
func _clear_bullets() -> void:
|
||||
for bullet in bullets: bullet.node.queue_free()
|
||||
bullets.clear()
|
||||
|
||||
func _die() -> void:
|
||||
if dual:
|
||||
dual=false
|
||||
_explode(wingman.position)
|
||||
wingman.visible=false
|
||||
invulnerable=2.5
|
||||
_sound("explosion")
|
||||
return
|
||||
_explode(player.position)
|
||||
player.visible = false
|
||||
lives -= 1
|
||||
state = "death"
|
||||
timer = 1.5
|
||||
_clear_bullets()
|
||||
_sound("explosion")
|
||||
|
||||
func _explode(at: Vector2, enemy := false) -> void:
|
||||
var node = EXPLOSION.instantiate()
|
||||
if enemy: node.kind="enemy_explosion"
|
||||
node.position = at
|
||||
field.add_child(node)
|
||||
effects.append({"node":node})
|
||||
|
||||
func _finish_capture() -> void:
|
||||
beam={}
|
||||
lives-=1
|
||||
captive.visible=true
|
||||
captive.position=player.position
|
||||
player.visible=false
|
||||
captor.mode="captured_return"
|
||||
captor.time=0
|
||||
_stop_sounds()
|
||||
state="captured_return"
|
||||
_sound("fighter_captured")
|
||||
timer=1.5
|
||||
|
||||
func _sound(name: String) -> void:
|
||||
if state=="capturing" and name!="capturing": return
|
||||
if state.begins_with("rescue_") and name!="fighter_rescued": return
|
||||
if not streams.has(name): return
|
||||
for voice in voices:
|
||||
if not voice.playing:
|
||||
voice.stream = streams[name]
|
||||
voice.play()
|
||||
return
|
||||
|
||||
func _stop_sounds() -> void:
|
||||
for voice in voices: voice.stop()
|
||||
|
||||
func _formation_home(enemy: Dictionary) -> Vector2:
|
||||
var expansion := 1.0+0.10*(0.5+0.5*sin(clock*1.7))
|
||||
return Vector2(112,48)+(enemy.home-Vector2(112,48))*expansion
|
||||
|
||||
func _update_peaceful_formation() -> void:
|
||||
for enemy in enemies:
|
||||
if enemy.mode=="formation": enemy.node.position=_formation_home(enemy)
|
||||
|
||||
func _resume_action() -> void:
|
||||
state="playing"
|
||||
invulnerable=2.5
|
||||
dive_timer=2.0
|
||||
_clear_bullets()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b6ui86lx5kkpa
|
||||
@@ -0,0 +1,14 @@
|
||||
@tool
|
||||
extends Node2D
|
||||
## Draw above actors in the same native-pixel coordinate system.
|
||||
const FRAME := Rect2(0, 0, 224, 288)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
var outer := (get_global_transform_with_canvas().affine_inverse() * get_viewport_rect()).merge(FRAME)
|
||||
draw_rect(Rect2(outer.position, Vector2(FRAME.position.x-outer.position.x, outer.size.y)), Color.BLACK)
|
||||
draw_rect(Rect2(Vector2(FRAME.end.x,outer.position.y),Vector2(outer.end.x-FRAME.end.x,outer.size.y)), Color.BLACK)
|
||||
draw_rect(Rect2(Vector2(0,outer.position.y),Vector2(FRAME.size.x,-outer.position.y)), Color.BLACK)
|
||||
draw_rect(Rect2(Vector2(0,FRAME.end.y),Vector2(FRAME.size.x,outer.end.y-FRAME.end.y)), Color.BLACK)
|
||||
@@ -0,0 +1 @@
|
||||
uid://clnm0uio7etas
|
||||
@@ -0,0 +1,91 @@
|
||||
@tool
|
||||
extends Node2D
|
||||
const FONT = preload("res://games/galaga/art/spr_galaga_screens_fonts.png")
|
||||
const SHEET = preload("res://games/galaga/art/spr_galaga.png")
|
||||
var attract_time := 0.0
|
||||
var previous_state := ""
|
||||
var high_score := 20000
|
||||
var edge_mask: Node2D
|
||||
|
||||
func _ready() -> void:
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
edge_mask = preload("res://games/galaga/galaga_edge_mask.gd").new()
|
||||
edge_mask.z_index = 100
|
||||
add_child(edge_mask)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var game = get_parent()
|
||||
if game.state == "idle":
|
||||
if previous_state != "idle": attract_time = 0.0
|
||||
attract_time += delta
|
||||
for actor in get_children():
|
||||
if actor == edge_mask: continue
|
||||
if game.state == "idle" and not Engine.is_editor_hint(): actor.visible = false
|
||||
previous_state = game.state
|
||||
high_score = maxi(high_score,game.score)
|
||||
var size := get_viewport_rect().size
|
||||
var factor := maxf(0.1,minf(size.x/224.0,size.y/288.0))
|
||||
if factor >= 1: factor = floorf(factor)
|
||||
scale = Vector2.ONE*factor
|
||||
position = ((size-Vector2(224,288)*factor)/2).floor()
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
draw_rect(Rect2(-1000,-1000,2224,2288),Color.BLACK)
|
||||
var star_clock: float = attract_time if get_parent().state=="idle" else get_parent().clock
|
||||
for i in range(90):
|
||||
var x := fposmod(i*73.0,224)
|
||||
var y := fposmod(i*113.0+star_clock*(12+i%3*8),288)
|
||||
if (int(star_clock*2)+i)%7!=0:
|
||||
draw_rect(Rect2(Vector2(x,y).round(),Vector2.ONE),[Color.CYAN,Color.RED,Color.YELLOW,Color.BLUE,Color.MAGENTA,Color.WHITE][i%6]*0.7)
|
||||
if get_parent().state == "idle" and not Engine.is_editor_hint():
|
||||
_attract()
|
||||
return
|
||||
_text("1UP %06d" % get_parent().score,Vector2(8,4))
|
||||
if get_parent().state != "intro":
|
||||
for i in range(mini(5,maxi(0,get_parent().lives-1))):
|
||||
draw_texture_rect_region(SHEET,Rect2(8+i*16,270,16,16),Rect2(109,1,16,16))
|
||||
_text("STAGE %d" % get_parent().stage,Vector2(144,278))
|
||||
var text: String = {"intro":"PLAYER 1","stage_card":"STAGE %d" % get_parent().stage,"ready":"PLAYER 1 STAGE %d" % get_parent().stage,"won":"YOU WIN!","lost":"GAME OVER","clear":"STAGE CLEAR"}.get(get_parent().state,"")
|
||||
if text != "": _text(text,Vector2((224-text.length()*8)/2,180),Color.CYAN)
|
||||
if get_parent().state=="captured_return": _center("FIGHTER CAPTURED",180,Color.CYAN)
|
||||
elif get_parent().state=="capture_ready": _center("READY",180,Color.CYAN)
|
||||
var game = get_parent()
|
||||
if not game.beam.is_empty() and game.beam.mode=="beam":
|
||||
var source_x := 289+int(game.clock*8)%3*50
|
||||
draw_texture_rect_region(SHEET,Rect2(game.beam.node.position.round()+Vector2(-24,8),Vector2(48,79)),Rect2(source_x,36,48,79))
|
||||
|
||||
func _text(message: String, at: Vector2, tint := Color.WHITE) -> void:
|
||||
var chars := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
for i in range(message.length()):
|
||||
var index := chars.find(message[i])
|
||||
if index<0: continue
|
||||
var row := 0 if index<25 else 1
|
||||
var col := index if row==0 else index-25
|
||||
draw_texture_rect_region(FONT,Rect2(at+Vector2(i*8,0),Vector2(7,7)),Rect2(454+col*9,443+row*9,7,7),tint)
|
||||
|
||||
func _center(message: String, y: float, tint := Color.WHITE) -> void:
|
||||
_text(message,Vector2((224-message.length()*8)/2,y),tint)
|
||||
|
||||
func _attract() -> void:
|
||||
_text("1UP",Vector2(16,0),Color.RED)
|
||||
_text("HIGH SCORE",Vector2(72,0),Color.RED)
|
||||
_text("2UP",Vector2(184,0),Color.RED)
|
||||
_center(str(high_score).pad_zeros(6),10)
|
||||
if int(attract_time / 8.0) % 2 == 0:
|
||||
_center("PUSH START BUTTON",104,Color.CYAN)
|
||||
var bonus: int = get_parent().extra_life_score
|
||||
var messages := ["1ST BONUS FOR %d PTS" % bonus,"2ND BONUS FOR %d PTS" % get_parent().second_extra_life_score,"AND FOR EVERY %d PTS" % get_parent().repeat_extra_life_interval]
|
||||
for i in range(3):
|
||||
_text(messages[i],Vector2(24,136+i*24),Color.YELLOW)
|
||||
draw_texture_rect_region(SHEET,Rect2(8,132+i*24,16,16),Rect2(109,1,16,16))
|
||||
else:
|
||||
_center("THE GALACTIC HEROES",56,Color.BLUE)
|
||||
_center("BEST 5",112,Color.RED)
|
||||
_text("SCORE NAME",Vector2(64,152),Color.CYAN)
|
||||
for i in range(5):
|
||||
_text(["1ST","2ND","3RD","4TH","5TH"][i],Vector2(24,176+i*16),Color.CYAN)
|
||||
_text(str(high_score if i==0 else (5-i)*1000).pad_zeros(6),Vector2(64,176+i*16),Color.CYAN)
|
||||
_text(["AAA","BBB","CCC","DDD","EEE"][i],Vector2(144,176+i*16),Color.CYAN)
|
||||
_text("CREDIT %d" % get_parent().credits,Vector2(8,278))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://yjuxbruqjkbf
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene format=3 uid="uid://du56pbkblp1bx"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c7fye4iaedapj" path="res://games/launcher/arcade_launcher.gd" id="1"]
|
||||
|
||||
[node name="ArcadeLauncher" type="Node" unique_id=3868404]
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,65 @@
|
||||
# Arcade launcher and cabinet controls
|
||||
|
||||
For central staff operation, see the separate [Arcade room controller guide](../../tools/room_controller/README.md). Open the controller URL in a browser for the multi-cabinet operator on Mac/iPad, or use `-- --operator` for Cabinet 01 quick controls. Run `-- --managed-cabinet --cabinet=cabinet_01 --gpio=real` on the appropriate Pi. These modes bypass the standalone launcher; managed mode uses central scenario mappings instead of local rewards.
|
||||
|
||||
For physical Raspberry Pi outputs, follow [the GPIO setup and LED test guide](../../tools/pi_gpio/README.md). The launcher forwards matching Hardware Scenario events; its default scenario connects Pac-Man's win to cabinet 01's GPIO17 coin-release pulse. Start the local helper and use `--gpio=real` to enable electrical outputs.
|
||||
|
||||
Open `res://games/launcher/ArcadeLauncher.tscn` and press F6. It lists all seven games with their icons. Move the joystick up/down and press Start to open a game's attract screen; press Start again to begin. Select returns to the list and unloads the game, including its sound players. Mouse selection and double-click also work.
|
||||
|
||||
This is a standalone arcade frontend, separate from the room/scenario runtime. To boot directly into it in an export, use this scene as **Project Settings > Application > Run > Main Scene**.
|
||||
|
||||
## Map your cabinet
|
||||
|
||||
The exported game opens **Control Setup** automatically until a local profile is saved. You can reopen it from the last entry in the game list, or press **F1** on a keyboard as a recovery shortcut. No Godot editor is needed on the Pi.
|
||||
|
||||
1. Connect both controllers before beginning. Press any controller button / Enter, or click **Begin Setup**.
|
||||
2. Follow the prompts for player-one Start, Select, joystick directions and the three game buttons. Release each button or center the joystick before the next prompt. Small axis drift is ignored.
|
||||
3. For Robotron firing, use four face buttons or move the second joystick in each requested direction. Player-one movement is automatically reused for Robotron movement.
|
||||
4. Review the list and test inputs: the monitor shows which controller/input was detected and the actions assigned to it. An unassigned player-two button should show no actions. Move the trackball to confirm mouse motion is arriving.
|
||||
5. Press the learned **Start** button or click **Save Controls**. Learned Select cancels from review. The Cancel button cancels at any point without altering the previous profile. **Escape quits the entire application**, including from setup; unfinished changes are not saved.
|
||||
|
||||
Saving replaces the default bindings for the configured actions instead of adding more overlapping bindings. The saved controls load for standalone game scenes and the room runtime as well as the launcher. Project Settings Input Map remains the fallback until a local profile exists; afterwards, the local profile takes precedence.
|
||||
|
||||
Profiles are saved outside the exported game as `user://arcade_controls.cfg`, usually `~/.local/share/godot/app_userdata/ArcadeEscape/arcade_controls.cfg` on Linux and `%APPDATA%/Godot/app_userdata/ArcadeEscape/arcade_controls.cfg` on Windows. They survive replacing the game export. Configure each machine independently; do not copy Windows bindings onto the Pi. If saving fails, the screen reports it and keeps the previous profile.
|
||||
|
||||
## Two controllers
|
||||
|
||||
The shipped default bindings use All Devices, which lets either gamepad activate the same action. Setup records each joypad's identity and specific input, then binds only that device. Different encoders are resolved by name/GUID when device numbers change. A disconnected controller's inputs are disabled rather than assigned to another controller.
|
||||
|
||||
Identical encoders can have the same name/GUID and no distinguishable serial identity through Godot. These are separated by their instance order, with a visible warning to check player one after reconnecting. Keep them in consistent USB ports and rerun setup if the players swap. If the count of identical encoders changes, affected bindings are disabled until all are connected again or setup is repeated. Changing connections during setup restarts the wizard.
|
||||
|
||||
Keyboard-mode encoders are supported, but two encoders emitting the same keyboard key cannot be distinguished by this setup. Give their players different key codes in the encoder configuration, or use gamepad mode. F1 and Escape are reserved for setup and quitting and cannot be learned as gameplay buttons.
|
||||
|
||||
## Action reference (project defaults)
|
||||
|
||||
| Action | Cabinet use | Keyboard default |
|
||||
| --- | --- | --- |
|
||||
| `arcade_up/down/left/right` | Joystick, menu and most games | Arrow keys |
|
||||
| `arcade_start` | Start | Enter |
|
||||
| `arcade_menu` | Select: leave game | Use your learned Select button; Escape quits the application |
|
||||
| `arcade_action_1` | Fire; Missile Command left battery | Space / left mouse |
|
||||
| `arcade_action_2` | Tempest Superzapper; Missile Command center battery | Z / middle mouse |
|
||||
| `arcade_action_3` | Missile Command right battery | X / right mouse |
|
||||
| `robotron_move_left/right/up/down` | Bind the same movement joystick | A / D / W / S |
|
||||
| `robotron_fire_left/right/up/down` | Four face buttons arranged directionally | Arrow keys |
|
||||
| `arcade_rotate_left/right` | Optional Tempest spinner pulses | Mouse wheel down/up |
|
||||
|
||||
The default gamepad bindings remain in Project Settings. Use Control Setup to learn the actual cabinet buttons rather than assuming printed button numbers match the operating system. Four face buttons cover Robotron, including diagonal shots by holding two buttons; the second joystick can also handle firing. The remaining games need at most three face buttons.
|
||||
|
||||
The trackball must report as a mouse. The launcher enables Missile Command's **Relative Mouse Aim** and captures the pointer for Missile Command and Tempest, so trackball travel continues beyond desktop edges. Select releases the pointer when returning to the menu. Tempest accepts horizontal mouse motion with **Trackball Pixels Per Lane** sensitivity. Continuous mouse motion is handled as motion, not a button action.
|
||||
|
||||
## Raspberry Pi 4/5
|
||||
|
||||
Input Map settings ship with the project/export. Use Linux ARM64 for a 64-bit Raspberry Pi OS installation and retain the Compatibility renderer. There are no Windows-only input APIs in these controls. Keyboard/mouse USB encoders and recognized gamepads use the same actions, but a generic encoder may report different button/axis mappings on Linux: test the actual hardware on the Pi before final deployment. This change does not add a GPIO input driver.
|
||||
|
||||
Godot references: [Linux export](https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_linux.html), [controller mappings](https://docs.godotengine.org/en/stable/tutorials/inputs/controllers_gamepads_joysticks.html).
|
||||
|
||||
## Quick check
|
||||
|
||||
1. Navigate all seven entries with the joystick, open each with Start, begin play, and return with Select.
|
||||
2. Test Missile Command's three batteries independently with the trackball; test Tempest fire and Superzapper separately.
|
||||
3. Test Robotron movement independently of all four fire buttons and their diagonals.
|
||||
4. Save setup, restart the game, and verify the learned bindings persist and unassigned player-two buttons have no effect.
|
||||
5. Repeat on the Pi after export and reconnecting the USB encoders. Check player-one identity if the encoders are identical.
|
||||
|
||||
Automated checks: run `tests/launcher/check_launcher.gd` and `tests/launcher/check_controls.gd` using Godot's headless script runner. The latter uses a temporary profile in `.godot`, not your real saved controls.
|
||||
@@ -0,0 +1,175 @@
|
||||
extends Node
|
||||
@export var hardware_scenario: ScenarioDefinition = preload("res://data/scenarios/proof_of_concept.tres")
|
||||
## Standalone arcade selector. Room/scenario startup remains separate.
|
||||
const GAMES := [
|
||||
["Pac-Man", "pacman/Pacman", "pacman", "JOYSTICK"],
|
||||
["Galaga", "galaga/Galaga", "galaga", "JOYSTICK / FIRE"],
|
||||
["Tempest", "tempest/Tempest", "tempest", "JOYSTICK OR TRACKBALL / FIRE / SUPERZAPPER"],
|
||||
["Missile Command", "missile_command/MissileCommand", "missilecommand", "TRACKBALL / THREE LAUNCH BUTTONS"],
|
||||
["Frogger", "frogger/Frogger", "frogger", "JOYSTICK"],
|
||||
["Space Invaders", "space_invaders/SpaceInvaders", "spaceinvaders", "JOYSTICK / FIRE"],
|
||||
["Robotron 2084", "robotron/Robotron", "robotron", "JOYSTICK / FOUR DIRECTIONAL FIRE BUTTONS"],
|
||||
]
|
||||
var selected := 0
|
||||
var game: Node
|
||||
var menu: Control
|
||||
var list: ItemList
|
||||
var description: Label
|
||||
var switching := false
|
||||
var setup: CanvasLayer
|
||||
var control_status: Label
|
||||
var gpio_status: Label
|
||||
|
||||
func _ready() -> void:
|
||||
if "--managed-cabinet" in OS.get_cmdline_user_args():
|
||||
set_process_input(false)
|
||||
add_child(preload("res://core/network/managed_cabinet.gd").new())
|
||||
return
|
||||
if "--operator" in OS.get_cmdline_user_args():
|
||||
set_process_input(false)
|
||||
add_child(preload("res://operator/ArcadeRoomOperator.tscn").instantiate())
|
||||
return
|
||||
var layer := CanvasLayer.new()
|
||||
add_child(layer)
|
||||
menu = Control.new()
|
||||
menu.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
layer.add_child(menu)
|
||||
var background := ColorRect.new()
|
||||
background.color = Color("080d18")
|
||||
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
menu.add_child(background)
|
||||
var center := CenterContainer.new()
|
||||
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
menu.add_child(center)
|
||||
var column := VBoxContainer.new()
|
||||
column.custom_minimum_size = Vector2(700, 0)
|
||||
column.add_theme_constant_override("separation", 18)
|
||||
center.add_child(column)
|
||||
var title := Label.new()
|
||||
title.text = "ARCADE GALLERY"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.add_theme_font_size_override("font_size", 46)
|
||||
title.modulate = Color("75e8ef")
|
||||
column.add_child(title)
|
||||
list = ItemList.new()
|
||||
list.custom_minimum_size = Vector2(700, 520)
|
||||
list.fixed_icon_size = Vector2i(48, 48)
|
||||
list.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
list.add_theme_font_size_override("font_size", 28)
|
||||
list.add_theme_constant_override("v_separation", 12)
|
||||
list.focus_mode = Control.FOCUS_NONE
|
||||
column.add_child(list)
|
||||
for entry in GAMES:
|
||||
list.add_item(entry[0], load("res://games/launcher/icons/ico_%s.png" % entry[2]))
|
||||
list.add_item("Control Setup")
|
||||
list.item_selected.connect(_select)
|
||||
list.item_activated.connect(func(index: int): _select(index); launch.call_deferred())
|
||||
description = Label.new()
|
||||
description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
description.add_theme_font_size_override("font_size", 18)
|
||||
column.add_child(description)
|
||||
var hint := Label.new()
|
||||
hint.text = "JOYSTICK: CHOOSE START: PLAY SELECT: RETURN TO MENU"
|
||||
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
hint.modulate = Color("91a1bb")
|
||||
column.add_child(hint)
|
||||
control_status = Label.new()
|
||||
control_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
control_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
control_status.add_theme_font_size_override("font_size", 16)
|
||||
column.add_child(control_status)
|
||||
ArcadeControls.changed.connect(_refresh_control_status)
|
||||
_refresh_control_status()
|
||||
gpio_status = Label.new()
|
||||
gpio_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
gpio_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
gpio_status.text = HardwareManager.status
|
||||
column.add_child(gpio_status)
|
||||
HardwareManager.status_changed.connect(func(message: String): gpio_status.text = message)
|
||||
_select(0)
|
||||
if ArcadeControls.bindings.is_empty(): open_setup.call_deferred()
|
||||
|
||||
func _refresh_control_status() -> void:
|
||||
control_status.text = ArcadeControls.problem if not ArcadeControls.problem.is_empty() else "CONTROL SETUP: LAST MENU ENTRY / F1"
|
||||
|
||||
func _select(index: int) -> void:
|
||||
selected = wrapi(index, 0, GAMES.size() + 1)
|
||||
list.select(selected)
|
||||
list.ensure_current_is_visible()
|
||||
description.text = GAMES[selected][3] if selected < GAMES.size() else "CONFIGURE THIS CABINET'S JOYSTICKS AND BUTTONS"
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if ArcadeControls.handle_quit(event): return
|
||||
if is_instance_valid(setup): return
|
||||
if event.is_echo() or switching: return
|
||||
if event is InputEventKey and event.pressed and event.physical_keycode == KEY_F1:
|
||||
get_viewport().set_input_as_handled()
|
||||
if is_instance_valid(game): return_to_menu()
|
||||
open_setup.call_deferred()
|
||||
return
|
||||
if is_instance_valid(game):
|
||||
if event.is_action_pressed("arcade_menu"):
|
||||
get_viewport().set_input_as_handled()
|
||||
switching = true
|
||||
return_to_menu.call_deferred()
|
||||
return
|
||||
if event.is_action_pressed("arcade_down"):
|
||||
_select(selected + 1)
|
||||
elif event.is_action_pressed("arcade_up"):
|
||||
_select(selected - 1)
|
||||
elif event.is_action_pressed("arcade_start"):
|
||||
switching = true
|
||||
launch.call_deferred()
|
||||
else:
|
||||
return
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func launch() -> void:
|
||||
if selected == GAMES.size():
|
||||
open_setup()
|
||||
switching = false
|
||||
return
|
||||
if is_instance_valid(game): return
|
||||
var scene := load("res://games/%s.tscn" % GAMES[selected][1]) as PackedScene
|
||||
if scene == null:
|
||||
switching = false
|
||||
return
|
||||
game = scene.instantiate()
|
||||
RoomRuntime.active_assignment = null
|
||||
if hardware_scenario != null:
|
||||
for assignment in hardware_scenario.assignments:
|
||||
if assignment.game != null and assignment.game.scene == scene:
|
||||
RoomRuntime.configure(assignment)
|
||||
game.game_started.connect(func(): RoomRuntime.fire_game_event(GameEvents.STARTED))
|
||||
game.game_won.connect(func(): RoomRuntime.fire_game_event(GameEvents.WON))
|
||||
game.game_lost.connect(func(): RoomRuntime.fire_game_event(GameEvents.LOST))
|
||||
game.game_reset.connect(func(): RoomRuntime.fire_game_event(GameEvents.RESET))
|
||||
break
|
||||
if GAMES[selected][2] == "missilecommand":
|
||||
game.set("relative_mouse_aim", true)
|
||||
if GAMES[selected][2] in ["missilecommand", "tempest"]:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
menu.hide()
|
||||
add_child(game)
|
||||
# Present the game's attract screen; Start begins its own opening sequence.
|
||||
switching = false
|
||||
|
||||
func open_setup() -> void:
|
||||
if is_instance_valid(setup): return
|
||||
setup = preload("res://games/launcher/control_setup.gd").new()
|
||||
menu.hide()
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
setup.closed.connect(func(): menu.show(); _select(selected))
|
||||
add_child(setup)
|
||||
|
||||
func return_to_menu() -> void:
|
||||
if is_instance_valid(game):
|
||||
remove_child(game)
|
||||
game.queue_free()
|
||||
game = null
|
||||
RoomRuntime.active_assignment = null
|
||||
HardwareManager.active_cabinet = null
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
menu.show()
|
||||
_select(selected)
|
||||
switching = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://c7fye4iaedapj
|
||||
@@ -0,0 +1,177 @@
|
||||
extends CanvasLayer
|
||||
signal closed
|
||||
var panel: Control
|
||||
var heading: Label
|
||||
var prompt: Label
|
||||
var monitor: Label
|
||||
var details: Label
|
||||
var buttons: HBoxContainer
|
||||
var draft: Dictionary = {}
|
||||
var step := -1
|
||||
var waiting: InputEvent
|
||||
var armed_at := 0
|
||||
|
||||
func _ready() -> void:
|
||||
layer = 10
|
||||
panel = ColorRect.new()
|
||||
panel.color = Color("080d18")
|
||||
panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(panel)
|
||||
var center := CenterContainer.new()
|
||||
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
panel.add_child(center)
|
||||
var column := VBoxContainer.new()
|
||||
column.custom_minimum_size = Vector2(1100, 0)
|
||||
column.add_theme_constant_override("separation", 18)
|
||||
center.add_child(column)
|
||||
heading = label(column, 36)
|
||||
prompt = label(column, 26)
|
||||
details = label(column, 18)
|
||||
monitor = label(column, 20)
|
||||
monitor.modulate = Color("75e8ef")
|
||||
buttons = HBoxContainer.new()
|
||||
buttons.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
buttons.add_theme_constant_override("separation", 24)
|
||||
column.add_child(buttons)
|
||||
welcome()
|
||||
Input.joy_connection_changed.connect(_controller_changed)
|
||||
|
||||
func _controller_changed(_id: int, _connected: bool) -> void:
|
||||
waiting = null
|
||||
draft.clear()
|
||||
welcome()
|
||||
prompt.text = "Controllers changed. Begin again with all controllers connected."
|
||||
|
||||
func label(parent: Node, font_size: int) -> Label:
|
||||
var result := Label.new()
|
||||
result.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
result.custom_minimum_size.x = 1100
|
||||
result.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
result.add_theme_font_size_override("font_size", font_size)
|
||||
parent.add_child(result)
|
||||
return result
|
||||
|
||||
func clear_buttons() -> void:
|
||||
for child in buttons.get_children():
|
||||
buttons.remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
func button(text: String, callback: Callable) -> void:
|
||||
var control := Button.new()
|
||||
control.text = text
|
||||
control.add_theme_font_size_override("font_size", 24)
|
||||
control.focus_mode = Control.FOCUS_NONE
|
||||
control.pressed.connect(callback)
|
||||
buttons.add_child(control)
|
||||
|
||||
func welcome() -> void:
|
||||
step = -1
|
||||
heading.text = "CONTROL SETUP"
|
||||
prompt.text = "Configure this cabinet — no editor needed"
|
||||
details.text = "Use PLAYER ONE for movement, Start, Select and game buttons.\nFor Robotron firing, use four face buttons OR your second joystick.\nEach controller is recorded separately. Release each input when asked.\n\nThe trackball works as a mouse; move it to see it detected below.\nKeyboard encoders work too. Escape quits the application. F1 opens setup.\n\nPress any controller button / Enter to begin, or click Begin Setup.\nCancel keeps your saved controls. Escape quits without saving."
|
||||
if not ArcadeControls.problem.is_empty(): details.text += "\n\n" + ArcadeControls.problem
|
||||
monitor.text = "Waiting for input…"
|
||||
clear_buttons()
|
||||
button("Begin Setup", begin)
|
||||
button("Cancel / Keep Current Controls", close)
|
||||
armed_at = Time.get_ticks_msec() + 350
|
||||
|
||||
func begin() -> void:
|
||||
draft.clear()
|
||||
waiting = null
|
||||
step = 0
|
||||
armed_at = Time.get_ticks_msec() + 350
|
||||
show_step()
|
||||
|
||||
func show_step() -> void:
|
||||
clear_buttons()
|
||||
if step >= ArcadeControls.STEPS.size():
|
||||
heading.text = "REVIEW & TEST CONTROLS"
|
||||
prompt.text = "Try both joysticks and buttons; watch the detected actions below."
|
||||
details.text = ""
|
||||
for entry in ArcadeControls.STEPS:
|
||||
details.text += "%s: %s\n" % [entry[0].replace("arcade_", "").replace("robotron_", "Robotron ").replace("_", " ").capitalize(), ArcadeControls.describe(draft[entry[0]])]
|
||||
details.text += "\nPress your learned START to save, SELECT to cancel, or click a button.\nTrackball motion needs no button binding. Identical USB encoders may swap after reconnecting."
|
||||
button("Save Controls", save)
|
||||
button("Start Over", begin)
|
||||
button("Cancel", close)
|
||||
return
|
||||
heading.text = "CONTROL SETUP %d / %d" % [step + 1, ArcadeControls.STEPS.size()]
|
||||
prompt.text = ArcadeControls.STEPS[step][1]
|
||||
details.text = "Move one direction or press one button, then release it.\nUse the actual cabinet controls. Nothing is saved until you finish.\nCancel keeps your saved controls; Escape quits. Trackball motion is shown below but is not bound as a button."
|
||||
button("Start Over", begin)
|
||||
button("Cancel", close)
|
||||
|
||||
func close() -> void:
|
||||
closed.emit()
|
||||
queue_free()
|
||||
|
||||
func save() -> void:
|
||||
var error := ArcadeControls.save_profile(draft)
|
||||
if error != OK:
|
||||
prompt.text = "Could not save controls (%s). Your previous settings are unchanged." % error_string(error)
|
||||
return
|
||||
close()
|
||||
|
||||
func released(event: InputEvent) -> bool:
|
||||
if waiting == null or event.device != waiting.device: return false
|
||||
if waiting is InputEventJoypadMotion and event is InputEventJoypadMotion:
|
||||
return event.axis == waiting.axis and absf(event.axis_value) < .25
|
||||
if waiting is InputEventJoypadButton and event is InputEventJoypadButton:
|
||||
return event.button_index == waiting.button_index and not event.pressed
|
||||
if waiting is InputEventKey and event is InputEventKey:
|
||||
return event.physical_keycode == waiting.physical_keycode and event.keycode == waiting.keycode and not event.pressed
|
||||
return false
|
||||
|
||||
func conflict(action: String, binding: Dictionary) -> String:
|
||||
for other in draft:
|
||||
if draft[other] != binding: continue
|
||||
# Reusing game fire buttons for Robotron is deliberate; within each game it isn't.
|
||||
if action.begins_with("robotron_") != other.begins_with("robotron_") and other not in ["arcade_start", "arcade_menu"]:
|
||||
continue
|
||||
return other
|
||||
return ""
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if ArcadeControls.handle_quit(event): return
|
||||
if event.is_echo(): return
|
||||
if event is InputEventMouseMotion:
|
||||
monitor.text = "Trackball / mouse motion: %s" % event.relative
|
||||
return
|
||||
if event is InputEventMouseButton: return # Leave setup buttons clickable.
|
||||
if not (event is InputEventKey or event is InputEventJoypadButton or event is InputEventJoypadMotion): return
|
||||
get_viewport().set_input_as_handled()
|
||||
if waiting != null:
|
||||
if released(event):
|
||||
waiting = null
|
||||
step += 1
|
||||
armed_at = Time.get_ticks_msec() + 200
|
||||
show_step()
|
||||
return
|
||||
if Time.get_ticks_msec() < armed_at: return
|
||||
if event is InputEventJoypadMotion:
|
||||
if absf(event.axis_value) < .65: return
|
||||
elif not event.is_pressed(): return
|
||||
var binding: Dictionary = ArcadeControls.encode(event, ArcadeControls.devices())
|
||||
if binding.is_empty(): return
|
||||
monitor.text = "Detected: " + ArcadeControls.describe(binding)
|
||||
if step < 0:
|
||||
if event is InputEventJoypadButton or (event is InputEventKey and event.physical_keycode == KEY_ENTER):
|
||||
begin()
|
||||
return
|
||||
if step >= ArcadeControls.STEPS.size():
|
||||
var actions: Array[String] = []
|
||||
for action in draft:
|
||||
if draft[action] == binding: actions.append(action)
|
||||
monitor.text += "\nActions: " + (", ".join(actions) if not actions.is_empty() else "unassigned — will not control gameplay")
|
||||
if binding == draft.arcade_start: save()
|
||||
elif binding == draft.arcade_menu: close()
|
||||
return
|
||||
var action: String = ArcadeControls.STEPS[step][0]
|
||||
var duplicate := conflict(action, binding)
|
||||
if not duplicate.is_empty():
|
||||
monitor.text = "Already used for %s. Choose a different input." % duplicate
|
||||
return
|
||||
draft[action] = binding
|
||||
waiting = event.duplicate()
|
||||
prompt.text = "Recorded. Release the button / center the joystick to continue."
|
||||
@@ -0,0 +1 @@
|
||||
uid://diw7s6drttvyq
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 448 B |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b8bqn824u85vl"
|
||||
path="res://.godot/imported/ico_frogger.png-b57a3a61500c7a320cf15c673d6eb4d6.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/launcher/icons/ico_frogger.png"
|
||||
dest_files=["res://.godot/imported/ico_frogger.png-b57a3a61500c7a320cf15c673d6eb4d6.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Loaded 100 of 505 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user