Added Glitch Room Source code to Gitty

This commit is contained in:
Jeff Luke committed 2026-10-04 11:31:17 -07:00
commit 1ac9eb5644
505 files changed
+28504

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
[gd_scene format=3 uid="uid://y0xg0i8yacqd"]
[ext_resource type="Script" uid="uid://c375fg6kitvex" path="res://games/pacman/pacman.gd" id="1"]
[ext_resource type="Script" uid="uid://c0kalctx5x8nf" path="res://games/pacman/pacman_editor_preview.gd" id="2"]
[node name="Pacman" type="Node2D" unique_id=1012196150]
script = ExtResource("1")
[node name="Background" type="ColorRect" parent="." unique_id=1925270229]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0, 0, 0, 1)
[node name="EditorPreview" type="Node2D" parent="." unique_id=452219297]
texture_filter = 1
position = Vector2(624, 108)
scale = Vector2(3, 3)
script = ExtResource("2")
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://np8iifxlrof5"
path="res://.godot/imported/sprites.png-6a0c66632a3c198b081d2b7b7452fab9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/pacman/art/sprites.png"
dest_files=["res://.godot/imported/sprites.png-6a0c66632a3c198b081d2b7b7452fab9.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: 3.0 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cbq03kf3nual3"
path="res://.godot/imported/text.png-d0d1c131f1ae1bb662e9ed4b6ddfead5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/pacman/art/text.png"
dest_files=["res://.godot/imported/text.png-d0d1c131f1ae1bb662e9ed4b6ddfead5.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://b54r4c60urcai"
path="res://.godot/imported/credit.wav-92f0d7ae9a7bc6bec91c661f1ee37d4b.sample"
[deps]
source_file="res://games/pacman/audio/credit.wav"
dest_files=["res://.godot/imported/credit.wav-92f0d7ae9a7bc6bec91c661f1ee37d4b.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://ovd85rcstsbj"
path="res://.godot/imported/death_0.wav-9cac572d18aa62fc6445fd773e82e8fe.sample"
[deps]
source_file="res://games/pacman/audio/death_0.wav"
dest_files=["res://.godot/imported/death_0.wav-9cac572d18aa62fc6445fd773e82e8fe.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://c4htnok4dd2h2"
path="res://.godot/imported/death_1.wav-3480eee71db390581fe6b7c8f5b1ab41.sample"
[deps]
source_file="res://games/pacman/audio/death_1.wav"
dest_files=["res://.godot/imported/death_1.wav-3480eee71db390581fe6b7c8f5b1ab41.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bwtevq2kndkjy"
path="res://.godot/imported/eat_dot_0.wav-e785d924a8ad68c8cad22f77721e8d3f.sample"
[deps]
source_file="res://games/pacman/audio/eat_dot_0.wav"
dest_files=["res://.godot/imported/eat_dot_0.wav-e785d924a8ad68c8cad22f77721e8d3f.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bhai4c6nel4hr"
path="res://.godot/imported/eat_dot_1.wav-589291afc9dc717182937ec5a490b116.sample"
[deps]
source_file="res://games/pacman/audio/eat_dot_1.wav"
dest_files=["res://.godot/imported/eat_dot_1.wav-589291afc9dc717182937ec5a490b116.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bltx7wcc4rxas"
path="res://.godot/imported/eat_fruit.wav-a61a6e480dfc9456d45ca90aab280a2d.sample"
[deps]
source_file="res://games/pacman/audio/eat_fruit.wav"
dest_files=["res://.godot/imported/eat_fruit.wav-a61a6e480dfc9456d45ca90aab280a2d.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bi6eyjbt8nkbh"
path="res://.godot/imported/eat_ghost.wav-6928c0d767e65ac3b8dc104f903ce44e.sample"
[deps]
source_file="res://games/pacman/audio/eat_ghost.wav"
dest_files=["res://.godot/imported/eat_ghost.wav-6928c0d767e65ac3b8dc104f903ce44e.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cn4y8jnq27rkf"
path="res://.godot/imported/extend.wav-22067b1829b78c441ada74f3450451a7.sample"
[deps]
source_file="res://games/pacman/audio/extend.wav"
dest_files=["res://.godot/imported/extend.wav-22067b1829b78c441ada74f3450451a7.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cy433r2abm2qm"
path="res://.godot/imported/eyes.wav-6339bb3cfd78ee9fb7c65c8d14fae1b4.sample"
[deps]
source_file="res://games/pacman/audio/eyes.wav"
dest_files=["res://.godot/imported/eyes.wav-6339bb3cfd78ee9fb7c65c8d14fae1b4.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://d4ljtyavowqbk"
path="res://.godot/imported/eyes_firstloop.wav-135f00eb080c2f8bf64b2349457c055e.sample"
[deps]
source_file="res://games/pacman/audio/eyes_firstloop.wav"
dest_files=["res://.godot/imported/eyes_firstloop.wav-135f00eb080c2f8bf64b2349457c055e.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cw8kuxiv30fh4"
path="res://.godot/imported/fright.wav-0174c816078cec81baabdba1e8269e91.sample"
[deps]
source_file="res://games/pacman/audio/fright.wav"
dest_files=["res://.godot/imported/fright.wav-0174c816078cec81baabdba1e8269e91.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://drsvbhyuv3yp4"
path="res://.godot/imported/fright_firstloop.wav-f882fcff985ed68cb9a48727df0588e7.sample"
[deps]
source_file="res://games/pacman/audio/fright_firstloop.wav"
dest_files=["res://.godot/imported/fright_firstloop.wav-f882fcff985ed68cb9a48727df0588e7.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://nqaa736y0aa2"
path="res://.godot/imported/intermission.wav-9454a9c38b8a759357091a745dfdacc8.sample"
[deps]
source_file="res://games/pacman/audio/intermission.wav"
dest_files=["res://.godot/imported/intermission.wav-9454a9c38b8a759357091a745dfdacc8.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://ceg1xejgjr70f"
path="res://.godot/imported/siren0.wav-c81bb85bf12b08da36d0bfd788c39b44.sample"
[deps]
source_file="res://games/pacman/audio/siren0.wav"
dest_files=["res://.godot/imported/siren0.wav-c81bb85bf12b08da36d0bfd788c39b44.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://dn36vo2x0ub13"
path="res://.godot/imported/siren0_firstloop.wav-812fe5df64e7c2d782c1107cee0cb3ec.sample"
[deps]
source_file="res://games/pacman/audio/siren0_firstloop.wav"
dest_files=["res://.godot/imported/siren0_firstloop.wav-812fe5df64e7c2d782c1107cee0cb3ec.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://djv5wb36s67eb"
path="res://.godot/imported/siren1.wav-cbe72c5a62b545c10c2aa59940f52d3d.sample"
[deps]
source_file="res://games/pacman/audio/siren1.wav"
dest_files=["res://.godot/imported/siren1.wav-cbe72c5a62b545c10c2aa59940f52d3d.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://c5oq5f1ly4ifq"
path="res://.godot/imported/siren1_firstloop.wav-166981f88f13d9478d3107ed333295d3.sample"
[deps]
source_file="res://games/pacman/audio/siren1_firstloop.wav"
dest_files=["res://.godot/imported/siren1_firstloop.wav-166981f88f13d9478d3107ed333295d3.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bdb6mkd0vbbe5"
path="res://.godot/imported/siren2.wav-c006186d3fd86cbc8d3312d6a077819e.sample"
[deps]
source_file="res://games/pacman/audio/siren2.wav"
dest_files=["res://.godot/imported/siren2.wav-c006186d3fd86cbc8d3312d6a077819e.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://dx6gucejy14ii"
path="res://.godot/imported/siren2_firstloop.wav-c5cd8e073ceeb2cf1d5f2a52b1591395.sample"
[deps]
source_file="res://games/pacman/audio/siren2_firstloop.wav"
dest_files=["res://.godot/imported/siren2_firstloop.wav-c5cd8e073ceeb2cf1d5f2a52b1591395.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bk86t2emh1t0v"
path="res://.godot/imported/siren3.wav-8a120be99cb0e0f3dfa8ad4aef35bd9c.sample"
[deps]
source_file="res://games/pacman/audio/siren3.wav"
dest_files=["res://.godot/imported/siren3.wav-8a120be99cb0e0f3dfa8ad4aef35bd9c.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://bksxvwormaye0"
path="res://.godot/imported/siren3_firstloop.wav-78226539db9aee757f5df5e53e8e99e0.sample"
[deps]
source_file="res://games/pacman/audio/siren3_firstloop.wav"
dest_files=["res://.godot/imported/siren3_firstloop.wav-78226539db9aee757f5df5e53e8e99e0.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://08e4iamm6wor"
path="res://.godot/imported/siren4.wav-c19a24ba9d0aa6a0dbdea17d8bc123e5.sample"
[deps]
source_file="res://games/pacman/audio/siren4.wav"
dest_files=["res://.godot/imported/siren4.wav-c19a24ba9d0aa6a0dbdea17d8bc123e5.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://cln07kvdnwb4e"
path="res://.godot/imported/siren4_firstloop.wav-f3e6af1140d2a0e60d5bce993412e499.sample"
[deps]
source_file="res://games/pacman/audio/siren4_firstloop.wav"
dest_files=["res://.godot/imported/siren4_firstloop.wav-f3e6af1140d2a0e60d5bce993412e499.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.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://di0ql7bdioby4"
path="res://.godot/imported/start.wav-03500820d755c9b2b3fd094e7dac60a6.sample"
[deps]
source_file="res://games/pacman/audio/start.wav"
dest_files=["res://.godot/imported/start.wav-03500820d755c9b2b3fd094e7dac60a6.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
+82
View File
@@ -0,0 +1,82 @@
extends ArcadeGame
const Model = preload("res://games/pacman/pacman_model.gd")
const View = preload("res://games/pacman/pacman_view.gd")
const Sound = preload("res://games/pacman/pacman_audio.gd")
@export_group("Room Objective")
## Zero means unlimited boards. Set a score target or use arcade mode.
@export_range(0, 100, 1) var boards_to_win: int = 1
@export_range(0, 1000000, 100) var score_to_win: int = 0
@export_range(1, 9, 1) var starting_lives: int = 3
@export var extra_life_score: int = 10000
var model = Model.new()
var view
var audio
var accumulator := 0.0
func _ready() -> void:
view = View.new()
view.model = model
add_child(view)
audio = Sound.new()
add_child(audio)
model.sound_requested.connect(audio.play_effect)
model.finished.connect(_on_finished)
_apply_config()
func configure(config: Dictionary = {}) -> void:
super.configure(config)
_apply_config()
func _apply_config() -> void:
model.boards_to_win = maxi(0, int(run_config.get("boards_to_win", boards_to_win)))
model.score_to_win = maxi(0, int(run_config.get("score_to_win", score_to_win)))
model.initial_lives = clampi(int(run_config.get("starting_lives", starting_lives)), 1, 9)
model.extra_life_score = maxi(0, int(run_config.get("extra_life_score", extra_life_score)))
func start_game() -> bool:
if is_running: return false
_apply_config()
audio.stop_all()
model.start()
accumulator = 0.0
return super.start_game()
func reset_game() -> void:
audio.stop_all()
model.reset()
accumulator = 0.0
super.reset_game()
func win_game() -> void:
if not is_running: return
model.state = "won"
audio.stop_all()
super.win_game()
func lose_game() -> void:
if not is_running: return
model.state = "lost"
audio.stop_all()
super.lose_game()
func _on_finished(won: bool) -> void:
if won: win_game()
else: lose_game()
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("arcade_start"):
start_game()
get_viewport().set_input_as_handled()
for pair in [["arcade_up", Vector2i.UP], ["arcade_left", Vector2i.LEFT], ["arcade_down", Vector2i.DOWN], ["arcade_right", Vector2i.RIGHT]]:
if event.is_action_pressed(pair[0]): model.requested = pair[1]
func _physics_process(delta: float) -> void:
if not is_running:
model.clock += delta
if is_running:
accumulator += minf(delta, 0.25)
while accumulator >= 1.0 / 60.0 and is_running:
model.update(1.0 / 60.0)
accumulator -= 1.0 / 60.0
if audio != null: audio.set_background(model.background_sound() if is_running else "")
+1
View File
@@ -0,0 +1 @@
uid://c375fg6kitvex
+96
View File
@@ -0,0 +1,96 @@
extends Node
## Separate effects from ambience so pellet sounds never restart the siren.
## Tune this to the end of the fourth wah in death_0.wav.
const DEATH_INTRO_SECONDS := 1.35
var death_cutoff: Timer
var effects: Array[AudioStreamPlayer] = []
var ambience: AudioStreamPlayer
var streams := {}
var background := ""
var effect_cursor := 0
var death_tail := false
var intro_pending := false
func _ready() -> void:
death_cutoff = Timer.new()
death_cutoff.one_shot = true
add_child(death_cutoff)
death_cutoff.timeout.connect(_death_finished)
for i in range(4):
var voice := AudioStreamPlayer.new()
voice.volume_db = -10.0
add_child(voice)
effects.append(voice)
ambience = AudioStreamPlayer.new()
ambience.volume_db = -15.0
add_child(ambience)
ambience.finished.connect(_ambient_finished)
effects[3].finished.connect(_death_finished)
func stream(name: String) -> AudioStream:
if not streams.has(name):
var path := "res://games/pacman/audio/%s.wav" % name
if not ResourceLoader.exists(path): return null
streams[name] = load(path)
return streams[name]
func play_effect(name: String) -> void:
if name == "death":
stop_all()
death_tail = true
effects[3].stream = stream("death_0")
effects[3].play()
death_cutoff.start(DEATH_INTRO_SECONDS)
return
var voice: AudioStreamPlayer = effects[0] if name.begins_with("eat_dot") else effects[1 + effect_cursor % 2]
if not name.begins_with("eat_dot"): effect_cursor += 1
voice.stream = stream(name)
if voice.stream != null: voice.play()
func _death_finished() -> void:
if not death_tail: return
death_tail = false
death_cutoff.stop()
effects[3].stop()
effects[3].stream = stream("death_1")
effects[3].play()
func set_background(name: String) -> void:
if name == background: return
background = name
ambience.stop()
intro_pending = false
if name.is_empty(): return
var intro := stream(name + "_firstloop")
# Join PCM samples before playback: no finished-signal gap at the intro seam.
ambience.stream = _loop_stream(name, intro)
if ambience.stream != null: ambience.play()
func _loop_stream(name: String, intro: AudioStream = null) -> AudioStream:
var source := stream(name)
if source is AudioStreamWAV:
var loop: AudioStreamWAV = source.duplicate()
loop.loop_mode = AudioStreamWAV.LOOP_FORWARD
loop.loop_begin = 0
loop.loop_end = roundi(loop.get_length() * loop.mix_rate)
if intro is AudioStreamWAV and intro.format == loop.format and intro.mix_rate == loop.mix_rate and intro.stereo == loop.stereo:
var intro_frames := roundi(intro.get_length() * intro.mix_rate)
loop.data = intro.data + loop.data
loop.loop_begin = intro_frames
loop.loop_end += intro_frames
return loop
return source
func _ambient_finished() -> void:
if background.is_empty(): return
intro_pending = false
ambience.stream = _loop_stream(background)
if ambience.stream != null: ambience.play()
func stop_all() -> void:
background = ""
death_tail = false
intro_pending = false
if death_cutoff != null: death_cutoff.stop()
if ambience != null: ambience.stop()
for voice in effects: voice.stop()
+1
View File
@@ -0,0 +1 @@
uid://bs1fyngribtup
+42
View File
@@ -0,0 +1,42 @@
@tool
extends "res://games/pacman/pacman_view.gd"
## A static design-time board, using the same renderer as the running game.
## No gameplay model, audio, input handlers, or room events are instantiated.
func _ready() -> void:
super._ready()
if not Engine.is_editor_hint():
hide()
set_process(false)
return
var preview_ghosts: Array[Dictionary] = []
for i in range(4):
var ghost := Maze.actor([Maze.DOOR, Vector2i(13, 14), Vector2i(11, 14), Vector2i(15, 14)][i], Vector2i.LEFT)
ghost["status"] = "active" if i == 0 else "house"
ghost["frightened"] = false
preview_ghosts.append(ghost)
model = {
"state": "ready", "phase_time": 2.0, "clock": 0.0,
"score": 0, "high_score": 0, "level": 1, "lives": 3,
"dots": Maze.pellets(), "player": Maze.actor(Maze.PLAYER_START, Vector2i.LEFT),
"ghosts": preview_ghosts, "fruit_time": 0.0, "fright_time": 0.0,
"popup_time": 0.0,
}
_update_preview()
func _process(_delta: float) -> void:
if Engine.is_editor_hint(): _update_preview()
func _update_preview() -> void:
# Match runtime placement inside the configured game viewport, independently
# of the editor's current panel size, zoom, and pan.
var screen_size := Vector2(
ProjectSettings.get_setting("display/window/size/viewport_width", 1920),
ProjectSettings.get_setting("display/window/size/viewport_height", 1080)
)
var factor := minf(screen_size.x / GAME_RECT.size.x, screen_size.y / GAME_RECT.size.y)
if factor >= 1.0: factor = floorf(factor)
scale = Vector2.ONE * factor
position = ((screen_size - GAME_RECT.size * factor) * 0.5).floor()
model.lives = int(get_parent().get("starting_lives"))
queue_redraw()
@@ -0,0 +1 @@
uid://c0kalctx5x8nf
+78
View File
@@ -0,0 +1,78 @@
extends RefCounted
## Logical map: artwork is never used as collision geometry.
const ROWS := [
"############################",
"#............##............#",
"#.####.#####.##.#####.####.#",
"#o####.#####.##.#####.####o#",
"#.####.#####.##.#####.####.#",
"#..........................#",
"#.####.##.########.##.####.#",
"#.####.##.########.##.####.#",
"#......##....##....##......#",
"######.##### ## #####.######",
" #.##### ## #####.# ",
" #.## ##.# ",
" #.## ###--### ##.# ",
"######.## # # ##.######",
" . # # . ",
"######.## # # ##.######",
" #.## ######## ##.# ",
" #.## ##.# ",
" #.## ######## ##.# ",
"######.## ######## ##.######",
"#............##............#",
"#.####.#####.##.#####.####.#",
"#.####.#####.##.#####.####.#",
"#o..##....... .......##..o#",
"###.##.##.########.##.##.###",
"###.##.##.########.##.##.###",
"#......##....##....##......#",
"#.##########.##.##########.#",
"#.##########.##.##########.#",
"#..........................#",
"############################",
]
const DIRS := [Vector2i.UP, Vector2i.LEFT, Vector2i.DOWN, Vector2i.RIGHT]
const PLAYER_START := Vector2i(13, 23)
const DOOR := Vector2i(13, 11)
static func neighbor(tile: Vector2i, direction: Vector2i) -> Vector2i:
var result := tile + direction
if result.y == 14:
result.x = posmod(result.x, 28)
return result
static func walkable(tile: Vector2i, eyes := false) -> bool:
if tile.x < 0 or tile.x >= 28 or tile.y < 0 or tile.y >= 31:
return false
# Blank regions outside the maze are not corridors.
if tile.y != 14 and tile.y >= 9 and tile.y <= 19 and (tile.x < 5 or tile.x > 22):
return false
var cell: String = ROWS[tile.y][tile.x]
return cell != "#" and (cell != "-" or eyes)
static func pellets() -> Dictionary:
var result := {}
for y in range(31):
for x in range(28):
if ROWS[y][x] in [".", "o"]:
result[Vector2i(x, y)] = ROWS[y][x] == "o"
return result
static func actor(tile: Vector2i, direction: Vector2i) -> Dictionary:
return {
"tile": tile,
"next": tile,
"progress": 0.0,
"dir": direction,
"spawn_offset": Vector2.ZERO,
}
static func position(actor_data: Dictionary) -> Vector2:
var offset := Vector2(actor_data.next - actor_data.tile)
if abs(offset.x) > 1.0:
offset.x = float(actor_data.dir.x)
# Actor centers use the logical maze lane. Rendering and collision share
# this exact position so a sprite cannot be shifted into a neighboring wall.
return Vector2(actor_data.tile) + actor_data.get("spawn_offset", Vector2.ZERO) + offset * actor_data.progress
+1
View File
@@ -0,0 +1 @@
uid://ccswn2ej4ufva
+378
View File
@@ -0,0 +1,378 @@
extends RefCounted
const Maze = preload("res://games/pacman/pacman_maze.gd")
const Rules = preload("res://games/pacman/pacman_rules.gd")
signal sound_requested(name: String)
signal finished(won: bool)
var state := "idle"
var phase_time := 0.0
var clock := 0.0
var player_anim_time := 0.0
var score := 0
var high_score := 0
var lives := 3
var level := 1
var boards_cleared := 0
var boards_to_win := 1
var score_to_win := 0
var initial_lives := 3
var extra_life_score := 10000
var extra_awarded := false
var dots := {}
var player := {}
var ghosts: Array[Dictionary] = []
var requested := Vector2i.LEFT
var fright_time := 0.0
var ghost_chain := 0
var mode_index := 0
var mode_time := 0.0
var pellet_pause := 0.0
var release_idle := 0.0
var release_counts := [0, 0, 0, 0]
var after_death := false
var global_dots := 0
var fruit_time := 0.0
var fruit_spawned := 0
var popup_time := 0.0
var popup_text := ""
var popup_position := Vector2.ZERO
var rng := RandomNumberGenerator.new()
const DEATH_PAUSE_SECONDS := 1.5
const DEATH_ANIMATION_SECONDS := 1.55
const DEATH_POP_SECONDS := 0.24
func _init() -> void:
reset()
func reset() -> void:
state = "idle"
player_anim_time = 0.0
score = 0
lives = initial_lives
level = 1
boards_cleared = 0
extra_awarded = false
dots = Maze.pellets()
fruit_spawned = 0
after_death = false
_reset_actors()
func start() -> void:
reset()
state = "ready"
phase_time = 4.25
sound_requested.emit("start")
func _reset_actors() -> void:
player = Maze.actor(Maze.PLAYER_START, Vector2i.LEFT)
requested = Vector2i.LEFT
ghosts.clear()
for i in range(4):
var g := Maze.actor([Maze.DOOR, Vector2i(13, 14), Vector2i(11, 14), Vector2i(15, 14)][i], Vector2i.LEFT)
# Center the opening formation like the arcade cabinet. This is cleared
# on the first gameplay movement so normal lane centers stay unchanged.
g["spawn_offset"] = Vector2(0.5, 0.0)
g["status"] = "active" if i == 0 else "house"
g["frightened"] = false
g["reverse"] = false
ghosts.append(g)
mode_index = 0
mode_time = 0.0
fright_time = 0.0
ghost_chain = 0
pellet_pause = 0.0
release_idle = 0.0
release_counts = [0, 0, 0, 0]
global_dots = 0
fruit_time = 0.0
popup_time = 0.0
rng.seed = 12345
func update(dt: float) -> void:
# Keep every clock-driven visual frozen during the pre-death pause. The
# phase timer still advances below, so the death animation starts on time.
if state != "death_pause":
clock += dt
popup_time = maxf(0.0, popup_time - dt)
if state in ["idle", "won", "lost"]:
return
if state != "playing":
phase_time -= dt
if phase_time <= 0.0:
match state:
"ready", "ghost_pause": state = "playing"
"death_pause":
state = "dying"
phase_time = DEATH_ANIMATION_SECONDS
sound_requested.emit("death")
"intermission": _next_level()
"dying":
state = "death_pop"
phase_time = DEATH_POP_SECONDS
"death_pop":
lives -= 1
if lives <= 0:
_end(false)
else:
after_death = true
_reset_actors()
state = "ready"
phase_time = 2.0
"clear":
boards_cleared += 1
if boards_to_win > 0 and boards_cleared >= boards_to_win:
_end(true)
elif level in [2, 5] or (level >= 9 and (level - 9) % 4 == 0):
state = "intermission"
phase_time = 5.28
sound_requested.emit("intermission")
else:
_next_level()
return
if fright_time > 0:
fright_time = maxf(0.0, fright_time - dt)
if fright_time == 0.0:
for g in ghosts: g.frightened = false
else:
mode_time += dt
var schedule: Array = Rules.schedule(level)
if mode_time >= schedule[mode_index]:
mode_time = 0.0
mode_index = mini(mode_index + 1, 7)
for g in ghosts:
if g.status == "active": g.reverse = true
fruit_time = maxf(0.0, fruit_time - dt)
release_idle += dt
_release_ghosts()
if pellet_pause > 0.0:
pellet_pause -= dt
else:
_move_player(_player_speed() * dt)
if state != "playing": return
_check_collisions()
if state != "playing": return
# Animate Pac-Man only while he is actually travelling. This keeps the
# mouth cadence steady when the global clock is affected by pauses or
# unrelated game state changes.
if pellet_pause <= 0.0 and player.tile != player.next:
player_anim_time += dt
elif player.tile == player.next:
player_anim_time = 0.0
for i in range(4):
_move_ghost(i, _ghost_speed(i) * dt)
_check_collisions()
if state != "playing": return
func _player_speed() -> float:
var ratio := 0.8 if level == 1 else (0.9 if level < 5 or level >= 21 else 1.0)
if fright_time > 0.0: ratio = 0.9 if level == 1 else (0.95 if level < 5 else 1.0)
return 9.46875 * ratio
func _ghost_speed(index: int) -> float:
var g: Dictionary = ghosts[index]
if g.status == "eyes": return 15.0
if g.status in ["house", "leaving", "entering"]: return 4.0
if g.tile.y == 14 and (g.tile.x < 6 or g.tile.x > 21):
return 9.46875 * (0.4 if level == 1 else (0.45 if level < 5 else 0.5))
if g.frightened: return 9.46875 * (0.5 if level == 1 else (0.55 if level < 5 else 0.6))
var ratio := 0.75 if level == 1 else (0.85 if level < 5 else 0.95)
if index == 0 and _elroy_active():
ratio += 0.05 if dots.size() > Rules.elroy_threshold(level) / 2 else 0.1
return 9.46875 * ratio
func _elroy_active() -> bool:
return dots.size() <= Rules.elroy_threshold(level) and (not after_death or ghosts[3].status != "house")
func _move_player(distance: float) -> void:
# Reversing is legal between tile centers. Other turns are buffered.
if player.tile != player.next and requested == -player.dir:
var previous: Vector2i = player.tile
player.tile = player.next
player.next = previous
player.progress = 1.0 - player.progress
player.dir = requested
while distance > 0.000001:
if player.tile == player.next:
_consume()
if state != "playing" or pellet_pause > 0.0: return
if Maze.walkable(Maze.neighbor(player.tile, requested)):
player.dir = requested
var destination: Vector2i = Maze.neighbor(player.tile, player.dir)
if not Maze.walkable(destination): return
player.next = destination
var step := minf(distance, 1.0 - player.progress)
player.progress += step
distance -= step
if player.progress >= 0.999999:
player.tile = player.next
player.progress = 0.0
_consume()
if pellet_pause > 0.0 or state != "playing": return
func _consume() -> void:
if dots.has(player.tile):
var power: bool = dots[player.tile]
dots.erase(player.tile)
_add_score(50 if power else 10)
if state != "playing": return
pellet_pause = (3.0 if power else 1.0) / 60.0
sound_requested.emit("eat_dot_%d" % (dots.size() % 2))
release_idle = 0.0
if after_death:
global_dots += 1
else:
for i in range(1, 4):
if ghosts[i].status == "house":
release_counts[i] += 1
break
if power:
fright_time = Rules.frightened_seconds(level)
ghost_chain = 0
for g in ghosts:
if g.status not in ["eyes", "entering"]:
g.frightened = fright_time > 0.0
if g.status == "active": g.reverse = true
var eaten := 244 - dots.size()
if (eaten >= 70 and fruit_spawned == 0) or (eaten >= 170 and fruit_spawned == 1):
fruit_spawned += 1
fruit_time = rng.randf_range(9.0, 10.0)
if dots.is_empty():
state = "clear"
phase_time = 2.5
if fruit_time > 0 and player.tile in [Vector2i(13, 17), Vector2i(14, 17)]:
fruit_time = 0.0
var points: int = Rules.fruit_score(level)
_add_score(points)
_popup(str(points), Maze.position(player))
sound_requested.emit("eat_fruit")
func _add_score(points: int) -> void:
score += points
high_score = maxi(high_score, score)
if not extra_awarded and extra_life_score > 0 and score >= extra_life_score:
extra_awarded = true
lives += 1
sound_requested.emit("extend")
if score_to_win > 0 and score >= score_to_win: _end(true)
func _release_ghosts() -> void:
# Release one ghost at a time; returning ghosts take the same door route.
for g in ghosts:
if g.status in ["leaving", "entering"]: return
for i in range(1, 4):
if ghosts[i].status != "house": continue
var threshold: int = [0, 0, 30 if level == 1 else 0, 60 if level == 1 else (50 if level == 2 else 0)][i]
var ready: bool = release_counts[i] >= threshold
if after_death: ready = global_dots >= [0, 7, 17, 32][i]
if ready or release_idle >= (4.0 if level < 5 else 3.0):
ghosts[i].status = "leaving"
release_idle = 0.0
return
func _move_ghost(index: int, distance: float) -> void:
var g: Dictionary = ghosts[index]
g["spawn_offset"] = Vector2.ZERO
if g.status == "house": return
while distance > 0.000001:
if g.tile == g.next:
if g.status == "eyes" and g.tile == Maze.DOOR:
g.status = "entering"
if g.status == "entering" and g.tile == Vector2i(13, 14):
g.status = "leaving"
g.frightened = false
if g.status == "leaving" and g.tile == Maze.DOOR:
g.status = "active"
g.dir = Vector2i.LEFT
var direction := Vector2i.ZERO
if g.status == "leaving":
direction = Vector2i(signi(13 - g.tile.x), 0) if g.tile.x != 13 else Vector2i.UP
elif g.status == "entering": direction = Vector2i.DOWN
elif g.reverse:
direction = -g.dir
g.reverse = false
else: direction = _ghost_direction(index)
if direction == Vector2i.ZERO: return
g.dir = direction
g.next = Maze.neighbor(g.tile, direction)
var step := minf(distance, 1.0 - g.progress)
g.progress += step
distance -= step
if g.progress >= 0.999999:
g.tile = g.next
g.progress = 0.0
func _ghost_direction(index: int) -> Vector2i:
var g: Dictionary = ghosts[index]
var target: Vector2i = Rules.SCATTER[index]
if g.status == "eyes": target = Maze.DOOR
elif mode_index % 2 == 1 or (index == 0 and _elroy_active()):
target = Rules.target(index, player.tile, player.dir, ghosts[0].tile, g.tile)
var candidates: Array[Vector2i] = []
for direction in Maze.DIRS:
if direction == -g.dir: continue
if not Maze.walkable(Maze.neighbor(g.tile, direction)): continue
# Original maze disallows upward turns at these junctions in normal modes.
if g.status == "active" and not g.frightened and direction == Vector2i.UP and g.tile in [Vector2i(12, 11), Vector2i(15, 11), Vector2i(12, 23), Vector2i(15, 23)]: continue
candidates.append(direction)
if candidates.is_empty():
return -g.dir if Maze.walkable(Maze.neighbor(g.tile, -g.dir)) else Vector2i.ZERO
if g.frightened and g.status == "active": return candidates[rng.randi_range(0, candidates.size() - 1)]
var chosen: Vector2i = candidates[0]
var best := INF
for direction in candidates:
var test: Vector2i = Maze.neighbor(g.tile, direction)
var distance := Vector2(test).distance_squared_to(Vector2(target))
if distance < best:
best = distance
chosen = direction
return chosen
func _check_collisions() -> void:
for g in ghosts:
if g.status != "active": continue
var difference: Vector2 = Maze.position(g) - Maze.position(player)
if absf(difference.x) > 14.0: difference.x = 28.0 - absf(difference.x)
if difference.length_squared() > 0.45 * 0.45: continue
if g.frightened:
g.status = "eyes"
g.frightened = false
g.reverse = false
var points := 200 * (1 << mini(ghost_chain, 3))
ghost_chain += 1
_add_score(points)
if state != "playing": return
_popup(str(points), Maze.position(g))
state = "ghost_pause"
phase_time = 0.6
sound_requested.emit("eat_ghost")
else:
state = "death_pause"
phase_time = DEATH_PAUSE_SECONDS
fruit_time = 0.0
return
func _next_level() -> void:
level += 1
dots = Maze.pellets()
fruit_spawned = 0
after_death = false
_reset_actors()
state = "ready"
phase_time = 2.0
func _popup(text: String, pos: Vector2) -> void:
popup_text = text
popup_position = pos
popup_time = 1.0
func _end(won: bool) -> void:
if state in ["won", "lost"]: return
state = "won" if won else "lost"
finished.emit(won)
func background_sound() -> String:
if state != "playing": return ""
for g in ghosts:
if g.status in ["eyes", "entering"]: return "eyes"
if fright_time > 0.0: return "fright"
return "siren%d" % mini(4, int((244 - dots.size()) / 50.0))
+1
View File
@@ -0,0 +1 @@
uid://1wwerfcb54mj
+37
View File
@@ -0,0 +1,37 @@
extends RefCounted
## Tables and target rules are based on https://pacman.holenet.info/.
const SCATTER := [Vector2i(25, -3), Vector2i(2, -3), Vector2i(27, 32), Vector2i(0, 32)]
const FRIGHT := [6.0, 5.0, 4.0, 3.0, 2.0, 5.0, 2.0, 2.0, 1.0, 5.0, 2.0, 1.0, 1.0, 3.0, 1.0, 1.0, 0.0, 1.0]
static func frightened_seconds(level: int) -> float:
return FRIGHT[level - 1] if level <= FRIGHT.size() else 0.0
static func schedule(level: int) -> Array:
if level == 1:
return [7.0, 20.0, 7.0, 20.0, 5.0, 20.0, 5.0, INF]
return [7.0 if level < 5 else 5.0, 20.0, 7.0 if level < 5 else 5.0, 20.0, 5.0, 1033.0 if level < 5 else 1037.0, 1.0 / 60.0, INF]
static func target(index: int, player_tile: Vector2i, direction: Vector2i, blinky: Vector2i, ghost_tile: Vector2i) -> Vector2i:
match index:
0: return player_tile
1: return player_tile + direction * 4 + (Vector2i.LEFT * 4 if direction == Vector2i.UP else Vector2i.ZERO)
2:
var ahead := player_tile + direction * 2 + (Vector2i.LEFT * 2 if direction == Vector2i.UP else Vector2i.ZERO)
return ahead * 2 - blinky
_: return player_tile if Vector2(ghost_tile).distance_squared_to(Vector2(player_tile)) >= 64.0 else SCATTER[3]
static func elroy_threshold(level: int) -> int:
if level == 1: return 20
if level == 2: return 30
if level <= 5: return 40
if level <= 8: return 50
if level <= 11: return 60
if level <= 14: return 80
if level <= 18: return 100
return 120
static func fruit_index(level: int) -> int:
return mini(7, int(level / 2.0))
static func fruit_score(level: int) -> int:
return [100, 300, 500, 700, 1000, 2000, 3000, 5000][fruit_index(level)]
+1
View File
@@ -0,0 +1 @@
uid://c5qe5vdxboj22
+147
View File
@@ -0,0 +1,147 @@
@tool
extends Node2D
const Maze = preload("res://games/pacman/pacman_maze.gd")
const Rules = preload("res://games/pacman/pacman_rules.gd")
const SHEET = preload("res://games/pacman/art/sprites.png")
const MAZE_RECT := Rect2(0, 24, 224, 248)
const GAME_RECT := Rect2(0, 0, 224, 288)
const FONT = preload("res://games/pacman/art/text.png")
const DEATH_ANIMATION_SECONDS := 1.55
const DEATH_POP_SECONDS := 0.24
var model
var debug_grid := false
func _ready() -> void:
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
func _process(_dt: float) -> void:
if Engine.is_editor_hint(): return
var available := get_viewport_rect().size
var factor := minf(available.x / 224.0, available.y / 288.0)
if factor >= 1.0: factor = floorf(factor)
scale = Vector2.ONE * factor
position = ((available - Vector2(224, 288) * factor) * 0.5).floor()
queue_redraw()
func _draw() -> void:
draw_rect(_viewport_bounds(), Color.BLACK)
if model == null: return
if model.state == "intermission":
_intermission()
_draw_edge_mask(GAME_RECT)
return
var flash: bool = model.state == "clear" and int(model.phase_time * 5) % 2 == 0
draw_texture_rect_region(SHEET, MAZE_RECT, Rect2(228, 0, 224, 248), Color(0.7, 0.7, 1.0) if flash else Color.WHITE)
for tile in model.dots:
var center := _pixel(Vector2(tile))
if model.dots[tile]:
if int(model.clock * 4) % 2 == 0: draw_circle(center, 3.5, Color("ffb8ae"))
else: draw_rect(Rect2(center - Vector2.ONE, Vector2(2, 2)), Color("ffb8ae"))
if model.fruit_time > 0:
_sprite(Rect2(504 + Rules.fruit_index(model.level) * 16, 48, 16, 16), _actor_pixel(Vector2(13.5, 17)))
if model.state not in ["ready", "dying", "death_pop", "lost", "won", "clear"]:
for i in range(4): _ghost(i)
if model.state not in ["ready", "death_pop", "lost", "won", "clear"]: _player()
if model.state == "death_pop":
_sprite(Rect2(664, 0, 16, 16), _actor_pixel(Maze.position(model.player)))
if model.popup_time > 0: _text(model.popup_text, _pixel(model.popup_position) - Vector2(12, 4), Color.CYAN)
_draw_edge_mask(MAZE_RECT)
_text("1UP", Vector2(24, 0))
_text("HIGH SCORE", Vector2(80, 0))
_text(str(model.score).pad_zeros(2), Vector2(8, 8))
_text(str(model.high_score).pad_zeros(2), Vector2(88, 8))
for i in range(mini(model.lives - 1, 5)):
_sprite(Rect2(600, 16, 16, 16), Vector2(16 + i * 16, 280))
_text("L%d" % model.level, Vector2(192, 276), Color("ffb852"))
match model.state:
"idle":
_banner("PRESS START", Color.YELLOW)
_text("ARROWS TO MOVE", Vector2(56, 256))
"ready":
_banner("READY!", Color.YELLOW)
_text("PLAYER ONE", Vector2(72, 112), Color("33aaff"))
"won": _banner("YOU WIN!", Color.CYAN)
"lost": _banner("GAME OVER", Color.RED)
if debug_grid:
for y in range(31):
for x in range(28):
if Maze.walkable(Vector2i(x,y)):
draw_rect(Rect2(x * 8, y * 8 + 24, 8, 8), Color(0, 1, 0, 0.25), false)
func _viewport_bounds() -> Rect2:
if Engine.is_editor_hint(): return GAME_RECT
return get_global_transform_with_canvas().affine_inverse() * get_viewport_rect()
func _draw_edge_mask(visible_area: Rect2) -> void:
var outer := _viewport_bounds().merge(visible_area)
draw_rect(Rect2(outer.position, Vector2(visible_area.position.x - outer.position.x, outer.size.y)), Color.BLACK)
draw_rect(Rect2(Vector2(visible_area.end.x, outer.position.y), Vector2(outer.end.x - visible_area.end.x, outer.size.y)), Color.BLACK)
draw_rect(Rect2(Vector2(visible_area.position.x, outer.position.y), Vector2(visible_area.size.x, visible_area.position.y - outer.position.y)), Color.BLACK)
draw_rect(Rect2(Vector2(visible_area.position.x, visible_area.end.y), Vector2(visible_area.size.x, outer.end.y - visible_area.end.y)), Color.BLACK)
func _pixel(tile: Vector2) -> Vector2:
return Vector2(4, 28) + tile * 8.0
func _actor_pixel(tile: Vector2) -> Vector2:
return _pixel(tile)
func _intermission() -> void:
var elapsed: float = 5.28 - model.phase_time
var frame := int(model.clock * 12) % 2
if elapsed < 2.64:
var x := 240.0 - elapsed * 105.0
_sprite(Rect2(456 + frame * 16, 16, 16, 16), Vector2(x, 144))
_sprite(Rect2(488 + frame * 16, 64, 16, 16), Vector2(x + 40, 144))
else:
var x := -40.0 + (elapsed - 2.64) * 110.0
_sprite(Rect2(584 + frame * 16, 64, 16, 16), Vector2(x + 55, 144))
draw_texture_rect_region(SHEET, Rect2(x - 16, 128, 32, 32), Rect2(504 + frame * 32, 16, 32, 32))
_text("LEVEL %d CLEAR" % model.level, Vector2(56, 200), Color.YELLOW)
func _sprite(region: Rect2, center: Vector2) -> void:
draw_texture_rect_region(SHEET, Rect2(center.round() - Vector2(8, 8), Vector2(16, 16)), region)
if center.x < 8: draw_texture_rect_region(SHEET, Rect2(center.round() + Vector2(216, -8), Vector2(16, 16)), region)
elif center.x > 216: draw_texture_rect_region(SHEET, Rect2(center.round() - Vector2(232, 8), Vector2(16, 16)), region)
func _player() -> void:
var p: Dictionary = model.player
var region := Rect2(456, 0, 16, 16)
if model.state == "dying":
var elapsed: float = DEATH_ANIMATION_SECONDS - model.phase_time
var frame := clampi(int(elapsed / DEATH_ANIMATION_SECONDS * 11.0), 0, 10)
region = Rect2(488 + frame * 16, 0, 16, 16)
else:
var row: int = {Vector2i.RIGHT: 0, Vector2i.LEFT: 1, Vector2i.UP: 2, Vector2i.DOWN: 3}.get(p.dir, 0)
var frame: int = [0, 1, 2, 1][int(model.player_anim_time * 12.0) % 4] if p.tile != p.next else 1
region = Rect2(456 + frame * 16, row * 16, 16, 16)
_sprite(region, _actor_pixel(Maze.position(p)))
func _ghost(index: int) -> void:
var g: Dictionary = model.ghosts[index]
var direction: int = {Vector2i.RIGHT: 0, Vector2i.LEFT: 1, Vector2i.UP: 2, Vector2i.DOWN: 3}.get(g.dir, 0)
var frame := int(model.clock * 8) % 2
var region := Rect2(456 + direction * 32 + frame * 16, 64 + index * 16, 16, 16)
if g.status in ["eyes", "entering"]: region = Rect2(584 + direction * 16, 80, 16, 16)
elif g.frightened:
var white: bool = model.fright_time < 2.0 and int(model.fright_time * 6) % 2 == 0
region = Rect2(584 + frame * 16 + (32 if white else 0), 64, 16, 16)
var center := _actor_pixel(Maze.position(g))
if g.status == "house": center.y += sin(model.clock * 6 + index) * 2
_sprite(region, center)
func _banner(message: String, color: Color) -> void:
var width := message.length() * 8
draw_rect(Rect2(112 - width / 2 - 4, 160, width + 8, 12), Color.BLACK)
_text(message, Vector2(112 - width / 2, 162), color)
func _text(message: String, at: Vector2, color := Color.WHITE) -> void:
for i in range(message.length()):
var c := message[i]
var region := Rect2()
var code := c.unicode_at(0)
if code >= 65 and code <= 79: region = Rect2((code - 65) * 8, 0, 8, 8)
elif code >= 80 and code <= 90: region = Rect2((code - 80) * 8, 8, 8, 8)
elif code >= 48 and code <= 57: region = Rect2((code - 48) * 8, 16, 8, 8)
elif c == "!": region = Rect2(88, 8, 8, 8)
else: continue
draw_texture_rect_region(FONT, Rect2(at + Vector2(i * 8, 0), Vector2(8, 8)), region, color)
+1
View File
@@ -0,0 +1 @@
uid://df6mgds15wbar