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

+58
View File
@@ -0,0 +1,58 @@
# Robotron 2084 setup and fidelity notes
Robotron is installed in the project and registered in the Games database. Open Robotron.tscn and run the current scene to play. The game uses the framework's ArcadeGame start, win, loss, and reset signals. Existing cabinet assignments remain unchanged.
## Controls
- Enter, arcade_start, or gamepad Start begins a fresh game, including during a demonstration.
- WASD moves; arrow keys fire independently in eight directions. Diagonal speed is normalized.
- On a gamepad, the left stick moves and the right stick fires. Releasing the firing stick stops shooting.
- The cabinet requires dual_joystick_8way and start_button capabilities. Add these to a cabinet profile only when that cabinet provides the matching controls.
The game installs default input actions at runtime only when they do not already exist. For a cabinet encoder, define robotron_move_left/right/up/down and robotron_fire_left/right/up/down in Project Settings, Input Map, with the desired bindings. Existing definitions take precedence. These use their own names so Robotron's arrow-key firing does not change the other games' movement actions. Physical cabinet/gamepad testing remains necessary.
## Scene settings
Select the Robotron root to change Waves To Win (default 1; set 0 for endless), Starting Lives (3), Difficulty (1), Extra Life Interval (25000; 0 disables), Sound Enabled, CRT Aspect, and Reduced Flashes. Save the scene before running through a cabinet. Settings are scene exports, not run_config overrides.
Waves To Win defaults to one for escape-room completion. Set it to 0 to keep playing through brain waves starting at 5 and tank waves starting at 7. Completing the configured wave emits game_won and holds the victory screen. Losing all lives emits game_lost, then returns to the idle sequence. Reset preserves only the session high score.
## Included gameplay
Independent eight-way movement and rapid directional fire, four simultaneous player shots, human rescue chains worth 1000 through 5000, repeat extra-life thresholds, electrodes, and increasingly crowded waves are implemented. Shooting does not hurt humans. Player respawn preserves surviving entities and grants a short protective interval.
Grunts chase the player and can destroy themselves on electrodes. Hulks survive shots, can be pushed back, and kill humans. Spheroids spawn enforcers; enforcers roam and fire. Quarks spawn tanks whose shells bounce off the arena boundaries. Brains take three hits, seek humans, convert them into hostile Progs, and launch homing cruise missiles. Enemy projectiles can be shot. Remaining hulks, electrodes, and humans do not prevent a wave from ending.
The idle loop has an animated title, instruction screen, enemy roster, silent gameplay demonstration, and session high-score screen. Demonstration scores cannot change the real high score, and the demo never emits room start/win/loss events. Start interrupts any idle phase and initializes a fresh run.
## Art and display
All four supplied PNGs are included unchanged. Runtime sprite crops preserve existing transparency and opaque white details. The title uses the supplied logo/credit artwork, with extracted Williams emblems moving around its perimeter and a cycling logo palette. The screenshot's fixed credit count is replaced with FREE PLAY. HUD and instruction lettering use an approximate pixel font because no complete font sheet was supplied.
The logical viewport is 292 by 240 with nearest-neighbor presentation and black margins. CRT Aspect presents it at 4:3; disable it for square source pixels. The renderer also provides an editor preview. Reduced Flashes stops the title movement/palette cycling and uses a steady accent color. Transition geometry still animates.
## Sound mapping
All 23 supplied WAVs are copied unchanged and loaded by the audio controller. Startup plays once when the runtime scene opens; game-start plays for a new run. Player-lazer accompanies firing, grunt-walk follows active grunts, and the human-save/human-die clips accompany rescue or loss. Robot-explode is the generic enemy destruction sound, with dedicated electrode, tank, brain, and spheroid destruction clips. Spheroid-spawn and quark-spawn accompany spawning. Enforcer-fire and tank-fire accompany projectiles, with tank-projectile-bounce on reflection. Human-to-prog and prog-transformation mark the two conversion stages. Player-death, extra-life, level-transition, and brain-wave-start follow their corresponding events. High-score-top plays when a completed losing run meets the session high score.
Sound effects are muted during demo play. A single audio channel plays effects: each accepted new sound stops the previous sound before starting, preventing overlapping echoes. Repeated effects remain rate limited to avoid rapid retriggering in crowded waves. Audio timing approximates the original sound hardware.
## Fidelity boundaries
This is a playable recreation, not ROM emulation. Wave populations and placements, spawn quotas, shot limits, movement speeds, collision radii, projectile paths, respawn protection, and timing are approximations that still need side-by-side tuning. In particular, the original Brain target-human bug and exact Enforcer firing behavior are not reproduced. Sprite variants are drawn from the supplied sheets but not every original palette/animation state is implemented. The story, roster, transition effects, and demo are recreations rather than exact playback of the original sequences. Play is one-player free play; coin accounting, alternating two-player games, initials entry, and persistent high scores are not implemented.
Visual reference: [supplied arcade footage](https://www.youtube.com/watch?v=l800GL6NQPY&t=28s). The native display dimensions and 4:3 presentation are corroborated by the [FinalBurn Neo machine definition](https://wiki.neo-source.com/database/view/arcade/robotron). Original manual references are catalogued in the [Robotron document archive](https://www.robotron-2084.co.uk/williams/documents/robotron).
## Validation
Automated checks pass for start guards, independent input actions, diagonal movement, firing release, rescue chains, extra-life thresholds, hulk knockback, human bullet immunity, electrode destruction, hulk kills, brain conversion, three-hit brains, spawners, shell bounces, brain/tank waves, wave completion, endless progression, room events, reset, and demo isolation. Tests ran in Godot 4.6.1 Compatibility; the project remains configured for 4.7. Title, roster, and gameplay captures were visually inspected. A desktop dense-wave render check reached 120 entities without a game-script failure; Raspberry Pi performance is unverified. Sandbox certificate-store and shader-cache warnings occurred, and desktop test shutdown reported a retained resource.
Run tests/robotron/check_robotron.gd with Godot --headless --path . --script. The same directory includes render_preview.gd and benchmark.gd.
Play checklist:
- Move diagonally while firing in another direction, then release the firing control.
- Rescue several humans and check increasing awards; lose a life and verify the chain resets.
- Shoot a hulk and an electrode; confirm the hulk survives and the electrode disappears.
- In endless mode, check brain conversion on wave 5 and tank shells on wave 7.
- Start during the demo; confirm clean score/lives and no demo-triggered room events.
- Verify cabinet input, sound levels, room completion, reset, and display scaling on the target machine.
+33
View File
@@ -0,0 +1,33 @@
[gd_scene format=3 uid="uid://bbe38558vhtp1"]
[ext_resource type="Script" uid="uid://dllelyq8hgx07" path="res://games/robotron/robotron.gd" id="1"]
[ext_resource type="Script" uid="uid://dhxkitgihoy3d" path="res://games/robotron/board.gd" id="2"]
[ext_resource type="Script" uid="uid://dxycwontqumst" path="res://games/robotron/display.gd" id="3"]
[ext_resource type="Script" uid="uid://c1xbioehp2ccr" path="res://games/robotron/audio.gd" id="4"]
[ext_resource type="Shader" uid="uid://bowtwv6ih8lj" path="res://games/robotron/title_palette.gdshader" id="5"]
[sub_resource type="ShaderMaterial" id="TitlePalette"]
shader = ExtResource("5")
shader_parameter/animate_title = true
shader_parameter/palette_step = 0
[node name="Robotron" type="Node" unique_id=593921370]
script = ExtResource("1")
waves_to_win = 0
[node name="PixelScreen" type="SubViewport" parent="." unique_id=230356224]
disable_3d = true
size = Vector2i(292, 240)
render_target_update_mode = 4
[node name="Board" type="Node2D" parent="PixelScreen" unique_id=1006156591]
texture_filter = 1
script = ExtResource("2")
[node name="Display" type="Node2D" parent="." unique_id=97971978]
texture_filter = 1
material = SubResource("TitlePalette")
script = ExtResource("3")
[node name="Audio" type="Node" parent="." unique_id=342326582]
script = ExtResource("4")
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

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

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

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cpd7qsb1dffdp"
path="res://.godot/imported/robotrons.png-2115096d401f70a24af3c3b7502e23bf.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/robotron/art/robotrons.png"
dest_files=["res://.godot/imported/robotrons.png-2115096d401f70a24af3c3b7502e23bf.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: 5.1 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://vnrsif2q6d5r"
path="res://.godot/imported/titlescreen_example.png-60f0e70647e7cef015516ffef23c2f0f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/robotron/art/titlescreen_example.png"
dest_files=["res://.godot/imported/titlescreen_example.png-60f0e70647e7cef015516ffef23c2f0f.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
+21
View File
@@ -0,0 +1,21 @@
extends Node
var tracks: Dictionary = {}
var voice := AudioStreamPlayer.new()
var gates: Dictionary = {}
func _ready() -> void:
for name in ["human-die","startup","electrode-explode","level-transition","spheroid-spawn","enforcer-fire","player-lazer","grunt-walk","human-save","robot-explode","player-death","game-start","tank-explode","tank-projectile-bounce","tank-fire","quark-spawn","brain-explode","prog-transformation","human-to-prog","extra-life","high-score-top","brain-wave-start","spheroid-pop"]:
tracks[name] = load("res://games/robotron/audio/"+name+".wav")
voice.volume_db = -15
add_child(voice)
func effect(name: String) -> void:
if get_parent().demo or not get_parent().sound_enabled or not tracks.has(name): return
var now := Time.get_ticks_msec()
if now<int(gates.get(name,0)): return
# Limit repeated triggers while allowing each accepted sound to replace the last.
gates[name] = now+(90 if name=="player-lazer" else 180)
voice.stop()
voice.stream = tracks[name]
voice.play()
func stop_all() -> void:
voice.stop()
gates.clear()
+1
View File
@@ -0,0 +1 @@
uid://c1xbioehp2ccr
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://d1f8hmol234b7"
path="res://.godot/imported/brain-explode.wav-eb257aa4582e409968f11cff0dfaecb2.sample"
[deps]
source_file="res://games/robotron/audio/brain-explode.wav"
dest_files=["res://.godot/imported/brain-explode.wav-eb257aa4582e409968f11cff0dfaecb2.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://dev6i7giud6ks"
path="res://.godot/imported/brain-wave-start.wav-d5d31728a09fe9adc41f02c9be62d889.sample"
[deps]
source_file="res://games/robotron/audio/brain-wave-start.wav"
dest_files=["res://.godot/imported/brain-wave-start.wav-d5d31728a09fe9adc41f02c9be62d889.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://c510aq3wmxfr8"
path="res://.godot/imported/electrode-explode.wav-94deded81f87387aa01d2cf8aaf59b0b.sample"
[deps]
source_file="res://games/robotron/audio/electrode-explode.wav"
dest_files=["res://.godot/imported/electrode-explode.wav-94deded81f87387aa01d2cf8aaf59b0b.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://fnwq6lxfkd64"
path="res://.godot/imported/enforcer-fire.wav-fbdf919096441f6fdd3dc412c82b809f.sample"
[deps]
source_file="res://games/robotron/audio/enforcer-fire.wav"
dest_files=["res://.godot/imported/enforcer-fire.wav-fbdf919096441f6fdd3dc412c82b809f.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://c2lbp0mfeodwk"
path="res://.godot/imported/extra-life.wav-19a70d7388e345af9f7f730bc4b90652.sample"
[deps]
source_file="res://games/robotron/audio/extra-life.wav"
dest_files=["res://.godot/imported/extra-life.wav-19a70d7388e345af9f7f730bc4b90652.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://c6typpwkisj8m"
path="res://.godot/imported/game-start.wav-4b865d843701ab928493a76855499efb.sample"
[deps]
source_file="res://games/robotron/audio/game-start.wav"
dest_files=["res://.godot/imported/game-start.wav-4b865d843701ab928493a76855499efb.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://bcehmbcjwptsx"
path="res://.godot/imported/grunt-walk.wav-faf58376d15b68b3a815bb279e447006.sample"
[deps]
source_file="res://games/robotron/audio/grunt-walk.wav"
dest_files=["res://.godot/imported/grunt-walk.wav-faf58376d15b68b3a815bb279e447006.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://bwn6jkq0xpuu0"
path="res://.godot/imported/high-score-top.wav-8e6de3f4bbf1fece31c5fa6282adf209.sample"
[deps]
source_file="res://games/robotron/audio/high-score-top.wav"
dest_files=["res://.godot/imported/high-score-top.wav-8e6de3f4bbf1fece31c5fa6282adf209.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://cn2fw55nvynf5"
path="res://.godot/imported/human-die.wav-99fd3bed7e7e5c07dd59719c2228724a.sample"
[deps]
source_file="res://games/robotron/audio/human-die.wav"
dest_files=["res://.godot/imported/human-die.wav-99fd3bed7e7e5c07dd59719c2228724a.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://b3ww2vonrhhkd"
path="res://.godot/imported/human-save.wav-dee20155e9e26ff4bd95b65e30c1d026.sample"
[deps]
source_file="res://games/robotron/audio/human-save.wav"
dest_files=["res://.godot/imported/human-save.wav-dee20155e9e26ff4bd95b65e30c1d026.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://qx72ejxfca08"
path="res://.godot/imported/human-to-prog.wav-7089b66bf4dcb3d404cb16fcc2dd6529.sample"
[deps]
source_file="res://games/robotron/audio/human-to-prog.wav"
dest_files=["res://.godot/imported/human-to-prog.wav-7089b66bf4dcb3d404cb16fcc2dd6529.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://dimmu61s3m5ew"
path="res://.godot/imported/level-transition.wav-6fc33834b681710f4c9cbb77b6f5f3cb.sample"
[deps]
source_file="res://games/robotron/audio/level-transition.wav"
dest_files=["res://.godot/imported/level-transition.wav-6fc33834b681710f4c9cbb77b6f5f3cb.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://beqa8vnny763m"
path="res://.godot/imported/player-death.wav-55b9d5daa99d827e04bb3f544c18dada.sample"
[deps]
source_file="res://games/robotron/audio/player-death.wav"
dest_files=["res://.godot/imported/player-death.wav-55b9d5daa99d827e04bb3f544c18dada.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://dr78jauiuuhnl"
path="res://.godot/imported/player-lazer.wav-05da460a1294a0fe996d94cf88c81bf2.sample"
[deps]
source_file="res://games/robotron/audio/player-lazer.wav"
dest_files=["res://.godot/imported/player-lazer.wav-05da460a1294a0fe996d94cf88c81bf2.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://qfgui3ep0118"
path="res://.godot/imported/prog-transformation.wav-bb6c2bd0e6e8c190e9882d119dde61c2.sample"
[deps]
source_file="res://games/robotron/audio/prog-transformation.wav"
dest_files=["res://.godot/imported/prog-transformation.wav-bb6c2bd0e6e8c190e9882d119dde61c2.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://cr35y8iwvy7ck"
path="res://.godot/imported/quark-spawn.wav-9c0f31ab2f22bfe191e0be8f03b0b11b.sample"
[deps]
source_file="res://games/robotron/audio/quark-spawn.wav"
dest_files=["res://.godot/imported/quark-spawn.wav-9c0f31ab2f22bfe191e0be8f03b0b11b.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://cmlyl3e33eh4l"
path="res://.godot/imported/robot-explode.wav-8eea8ea0a6c79e087ab9c54c1b6454dd.sample"
[deps]
source_file="res://games/robotron/audio/robot-explode.wav"
dest_files=["res://.godot/imported/robot-explode.wav-8eea8ea0a6c79e087ab9c54c1b6454dd.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://bh3mjr3r6nnft"
path="res://.godot/imported/spheroid-pop.wav-77f9dcf5365d5202df99a46f1492107a.sample"
[deps]
source_file="res://games/robotron/audio/spheroid-pop.wav"
dest_files=["res://.godot/imported/spheroid-pop.wav-77f9dcf5365d5202df99a46f1492107a.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://6hq1t3mexcyw"
path="res://.godot/imported/spheroid-spawn.wav-7681b106e9d24fa45a1a55c0eddb90c5.sample"
[deps]
source_file="res://games/robotron/audio/spheroid-spawn.wav"
dest_files=["res://.godot/imported/spheroid-spawn.wav-7681b106e9d24fa45a1a55c0eddb90c5.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://c33524qlyohrh"
path="res://.godot/imported/startup.wav-09f3a543c3cc1641263246bb8a2e314b.sample"
[deps]
source_file="res://games/robotron/audio/startup.wav"
dest_files=["res://.godot/imported/startup.wav-09f3a543c3cc1641263246bb8a2e314b.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://fh5303842hua"
path="res://.godot/imported/tank-explode.wav-06ef0590bf491e2fa992dd03e3e11cb9.sample"
[deps]
source_file="res://games/robotron/audio/tank-explode.wav"
dest_files=["res://.godot/imported/tank-explode.wav-06ef0590bf491e2fa992dd03e3e11cb9.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://6xpa0q7yeb7y"
path="res://.godot/imported/tank-fire.wav-aa436e73a9bdf6b01d0c6684a52ffd1a.sample"
[deps]
source_file="res://games/robotron/audio/tank-fire.wav"
dest_files=["res://.godot/imported/tank-fire.wav-aa436e73a9bdf6b01d0c6684a52ffd1a.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://dgucnurwckx8t"
path="res://.godot/imported/tank-projectile-bounce.wav-8189827f0f7c9a2573cc9caad22d14e6.sample"
[deps]
source_file="res://games/robotron/audio/tank-projectile-bounce.wav"
dest_files=["res://.godot/imported/tank-projectile-bounce.wav-8189827f0f7c9a2573cc9caad22d14e6.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
+188
View File
@@ -0,0 +1,188 @@
@tool
extends Node2D
const PLAYER = preload("res://games/robotron/art/player.png")
const HUMANS = preload("res://games/robotron/art/humans.png")
const ROBOTS = preload("res://games/robotron/art/robotrons.png")
const TITLE = preload("res://games/robotron/art/titlescreen_example.png")
const COLORS := [Color.WHITE,Color.YELLOW,Color("ff8000"),Color.MAGENTA,Color.GREEN,Color.RED,Color.BLUE]
# Small pixel font for HUD and instructions; artwork remains in supplied sheets.
const FONT := {
"A":"01110100011000111111100011000110001","B":"11110100011000111110100011000111110","C":"01111100001000010000100001000001111","D":"11110100011000110001100011000111110",
"E":"11111100001000011110100001000011111","F":"11111100001000011110100001000010000","G":"01111100001000010111100011000101111","H":"10001100011000111111100011000110001",
"I":"11111001000010000100001000010011111","J":"00111000100001000010000101001001100","K":"10001100101010011000101001001010001","L":"10000100001000010000100001000011111",
"M":"10001110111010110101100011000110001","N":"10001110011010110011100011000110001","O":"01110100011000110001100011000101110","P":"11110100011000111110100001000010000",
"Q":"01110100011000110001101011001001101","R":"11110100011000111110101001001010001","S":"01111100001000001110000010000111110","T":"11111001000010000100001000010000100",
"U":"10001100011000110001100011000101110","V":"10001100011000110001100010101000100","W":"10001100011000110101101011101110001","X":"10001100010101000100010101000110001",
"Y":"10001100010101000100001000010000100","Z":"11111000010001000100010001000011111","0":"01110100011001110101110011000101110","1":"00100011000010000100001000010001110",
"2":"01110100010000100010001000100011111","3":"11110000010000101110000010000111110","4":"00010001100101010010111110001000010","5":"11111100001000011110000010000111110",
"6":"01110100001000011110100011000101110","7":"11111000010001000100010000100001000","8":"01110100011000101110100011000101110","9":"01110100011000101111000010000101110",
"-":"00000000000000011111000000000000000",":":"00000001000010000000001000010000000","!":"00100001000010000100001000000000100",".":"00000000000000000000000000010000100"}
var sprites: Dictionary = {}
var game: Node
var badge: ImageTexture
func _ready() -> void:
game = get_parent().get_parent()
var player := PLAYER.get_image()
for f in range(3):
for d in range(4):
register("player%d_%d"%[d,f],player,Rect2i([0,7,14,23][d],[0,12,25][f],[5,5,7,7][d],[11,12,10][f]),Vector2i(8,12))
var human := HUMANS.get_image()
for f in range(3):
for d in range(4):
register("human0_%d_%d"%[d,f],human,Rect2i([0,9,19,30][d],[0,13,27][f],[8,8,9,8][d],12),Vector2i(10,14))
register("human1_%d_%d"%[d,f],human,Rect2i([0,9,18,28][d],[41,54,70][f],[8,8,10,10][d],12 if f==0 else 14),Vector2i(10,14))
register("human2_%d_%d"%[d,f],human,Rect2i([1,7,12,18][d],[85,96,108][f],[4,4,5,4][d],10 if f==0 else 11),Vector2i(6,12))
register("prog",human,Rect2i(0,120,11,11))
var robots := ROBOTS.get_image()
for f in range(3):
register("grunt%d"%f,robots,Rect2i(f*12,0,9,13),Vector2i(10,14))
register("hulk%d"%f,robots,Rect2i(14,[14,30,46][f],11,15),Vector2i(14,16))
register("brain%d"%f,robots,Rect2i(1,[141,157,174][f],12,15),Vector2i(14,16))
register("spheroid",robots,Rect2i(45,65,11,11))
register("enforcer",robots,Rect2i(39,79,11,11))
register("quark",robots,Rect2i(27,103,9,9))
register("tank0",robots,Rect2i(29,116,14,17))
register("tank1",robots,Rect2i(44,116,14,17))
register("tank2",robots,Rect2i(59,116,14,17))
register("shell",robots,Rect2i(0,134,7,7))
for i in range(6):
var key := "electrode%d"%i
register(key,robots,[Rect2i(1,220,7,7),Rect2i(11,220,7,7),Rect2i(21,220,7,7),Rect2i(32,220,7,7),Rect2i(49,220,7,7),Rect2i(84,220,7,7)][i])
var mask: Image = sprites[key].get_image()
for y in range(mask.get_height()):
for x in range(mask.get_width()):
var alpha := mask.get_pixel(x,y).a
mask.set_pixel(x,y,Color(1,1,1,alpha))
sprites[key] = ImageTexture.create_from_image(mask)
# Reuse the supplied title's exact Williams emblem as a tintable mask.
var emblem := TITLE.get_image().get_region(Rect2i(48,16,56,54))
emblem.convert(Image.FORMAT_RGBA8)
for y in range(emblem.get_height()):
for x in range(emblem.get_width()):
var c := emblem.get_pixel(x,y)
emblem.set_pixel(x,y,Color.WHITE if c.r>.8 and c.g>.8 and c.b>.8 else Color.TRANSPARENT)
badge = ImageTexture.create_from_image(emblem)
func register(key: String, image: Image, rect: Rect2i, canvas: Vector2i = Vector2i.ZERO) -> void:
var piece := image.get_region(rect)
if canvas!=Vector2i.ZERO:
var padded := Image.create(canvas.x,canvas.y,false,Image.FORMAT_RGBA8)
piece.convert(Image.FORMAT_RGBA8)
padded.blit_rect(piece,Rect2i(Vector2i.ZERO,piece.get_size()),Vector2i((canvas.x-piece.get_width())/2,canvas.y-piece.get_height()))
piece = padded
sprites[key] = ImageTexture.create_from_image(piece)
func sprite(key: String, point: Vector2, tint: Color = Color.WHITE) -> void:
var texture: Texture2D = sprites[key]
draw_texture(texture,(point-texture.get_size()*.5).round(),tint)
func text(value: String, point: Vector2, color: Color = Color.WHITE) -> void:
for i in range(value.length()):
if not FONT.has(value[i]): continue
var bits: String = FONT[value[i]]
for pixel in range(35):
if bits[pixel]=="1": draw_rect(Rect2(point+Vector2(i*6+pixel%5,pixel/5),Vector2.ONE),color)
func center(value: String,y: float,color: Color = Color.WHITE) -> void:
text(value,Vector2(roundf((292-value.length()*6)/2.0),y),color)
func direction_index(v: Vector2) -> int:
if absf(v.x)>absf(v.y): return 0 if v.x<0 else 1
return 3 if v.y<0 else 2
func draw_title() -> void:
draw_texture_rect(TITLE,Rect2(0,0,292,240),false)
# Animated emblems move around the perimeter, rather than freezing the screenshot.
draw_rect(Rect2(0,0,292,44),Color.BLACK)
draw_rect(Rect2(0,200,292,40),Color.BLACK)
draw_rect(Rect2(0,44,36,164),Color.BLACK)
draw_rect(Rect2(256,44,36,164),Color.BLACK)
var perimeter := 2.0*(252+200)
var shift: float = 0.0 if game.reduced_flashes else game.phase_time*24
for i in range(28):
var distance := fposmod(i*perimeter/28+shift,perimeter)
var p: Vector2
if distance<252: p=Vector2(20+distance,20)
elif distance<452: p=Vector2(272,20+distance-252)
elif distance<704: p=Vector2(272-(distance-452),220)
else: p=Vector2(20,220-(distance-704))
draw_texture_rect(badge,Rect2((p-Vector2(13,13)).round(),Vector2(26,26)),false,COLORS[(i+int(game.phase_time*2))%COLORS.size()] if not game.reduced_flashes else COLORS[i%COLORS.size()])
draw_rect(Rect2(108,158,76,10),Color.BLACK)
center("FREE PLAY",160,Color.BLUE)
func _draw() -> void:
if game==null: return
draw_rect(Rect2(0,0,292,240),Color.BLACK)
if game.phase=="title":
draw_title()
return
var accent: Color = COLORS[int(game.clock*3)%COLORS.size()] if not game.reduced_flashes else Color.RED
draw_rect(Rect2(4,16,284,216),Color.BLUE if game.phase in ["story","roster","scores"] else Color.RED,false,1)
text(str(game.last_score if game.demo else game.score),Vector2(52,4))
if game.phase in ["story","roster","scores"]:
center("FREE PLAY",233,Color.GREEN)
else:
for i in range(mini(maxi(0,game.lives-1),7)): sprite("player2_0",Vector2(134+i*10,8))
center("%d WAVE"%game.wave,233,Color.BLUE)
if game.phase=="story":
center("ROBOTRON: 2084",30,accent)
var lines := ["YOU ARE THE LAST HOPE OF MANKIND.","SAVE THE LAST HUMAN FAMILY!","MOVE AND FIRE IN EIGHT DIRECTIONS.","WASD MOVES ARROWS FIRE","RESCUE HUMANS FOR 1000 TO 5000","EXTRA LIFE EVERY %d POINTS"%game.extra_life_interval if game.extra_life_interval>0 else "BONUS LIVES DISABLED"]
for i in range(lines.size()):
if game.phase_time>i*.8: center(lines[i],56+i*22,COLORS[(i+2)%7])
sprite("player2_0",Vector2(114,205))
sprite("human0_2_0",Vector2(146,205))
return
if game.phase=="roster":
center("THE ROBOTRONS",30,accent)
var names := ["GRUNT","HULK","SPHEROID","ENFORCER","BRAIN","QUARK","TANK","PROG"]
var keys := ["grunt0","hulk0","spheroid","enforcer","brain0","quark","tank0","prog"]
for i in range(8):
sprite(keys[i],Vector2(48,54+i*20))
text(names[i],Vector2(76,50+i*20),COLORS[i%7])
return
if game.phase=="scores":
center("ROBOTRON HERO",60,accent)
center("HIGH SCORE",92)
center(str(game.high_score),110,Color.YELLOW)
center("PUSH START",176,accent)
return
for entity in game.entities:
if not entity.alive: continue
var frame := int(game.clock*8+entity.frame)%3
var key: String = entity.kind
match key:
"human": key="human%d_%d_%d"%[entity.human_type,direction_index(entity.velocity),frame]
"grunt","hulk","brain","tank": key+=str(frame)
"electrode": key+=str((game.wave+entity.id)%6)
"converting": key="prog"
var tint := Color.WHITE
if entity.kind=="converting": tint = accent
elif entity.kind=="electrode": tint = [Color.GREEN,Color.RED,Color.BLUE,Color.YELLOW,Color.MAGENTA,Color.CYAN][(game.wave-1)%6]
sprite(key,entity.position,tint)
if game.phase=="dead":
for i in range(8):
var offset: Vector2 = Vector2.from_angle(i*TAU/8)*(1.2-game.phase_time)*30
sprite("player%d_%d"%[direction_index(game.facing),i%3],game.player+offset,Color(1,1,1,clampf(game.phase_time,0,1)))
elif game.phase not in ["won","lost","transition"]:
if game.invulnerable<=0 or int(game.clock*12)%2==0:
sprite("player%d_%d"%[direction_index(game.facing),int(game.clock*10)%3 if game.moving else 0],game.player)
for shot in game.shots:
var tail: Vector2 = shot.position-shot.velocity.normalized()*5
draw_line(tail.round(),shot.position.round(),Color.WHITE,1)
for shot in game.enemy_shots:
if shot.kind=="shell": sprite("shell",shot.position)
else: draw_rect(Rect2(shot.position.round()-Vector2(1,1),Vector2(3,3)),accent)
for effect in game.effects:
if effect.label!="": text(effect.label,effect.position.round()-Vector2(10,8),accent)
else:
for i in range(8):
var p: Vector2 = effect.position+Vector2.from_angle(i*TAU/8)*(1-effect.life/.25)*12
draw_rect(Rect2(p.round(),Vector2(2,2)),accent)
if game.phase=="ready": center("GET READY",110,accent)
if game.phase=="transition":
for i in range(8):
var expansion: float = fposmod((1.8-game.phase_time)*150+i*18,150)
draw_rect(Rect2(Vector2(146,124)-Vector2(expansion,expansion*.7),Vector2(expansion*2,expansion*1.4)),accent,false,1)
if game.phase=="won": center("YOU WIN!",114,accent)
if game.phase=="lost" or game.demo: center("GAME OVER",114,accent)
+1
View File
@@ -0,0 +1 @@
uid://dhxkitgihoy3d
+12
View File
@@ -0,0 +1,12 @@
@tool
extends Node2D
func _process(_delta: float) -> void:
material.set_shader_parameter("animate_title",get_parent().phase=="title" and not get_parent().reduced_flashes)
material.set_shader_parameter("palette_step",int(get_parent().phase_time*2)%7)
queue_redraw()
func _draw() -> void:
var available := get_viewport_rect().size if not Engine.is_editor_hint() else Vector2(960,720)
var aspect := Vector2(4,3) if get_parent().crt_aspect else Vector2(292,240)
var size := aspect*minf(available.x/aspect.x,available.y/aspect.y)
draw_rect(Rect2(Vector2.ZERO,available),Color.BLACK)
draw_texture_rect($"../PixelScreen".get_texture(),Rect2(((available-size)*.5).floor(),size),false)
+1
View File
@@ -0,0 +1 @@
uid://dxycwontqumst
+429
View File
@@ -0,0 +1,429 @@
@tool
extends ArcadeGame
@export_range(0,99) var waves_to_win: int = 1
@export_range(1,9) var starting_lives: int = 3
@export_range(.5,2,.1) var difficulty: float = 1.0
@export_range(0,100000,5000) var extra_life_interval: int = 25000
@export var sound_enabled: bool = true
@export var crt_aspect: bool = true
@export var reduced_flashes: bool = false
const ARENA := Rect2(8,20,276,208)
const POINTS := {"grunt":100,"hulk":0,"spheroid":1000,"enforcer":150,"quark":1000,"tank":200,"brain":500,"prog":100,"electrode":0}
var phase := "title"
var phase_time := 0.0
var clock := 0.0
var demo := false
var demo_time := 0.0
var score := 0
var high_score := 0
var last_score := 0
var lives := 3
var wave := 1
var rescue_chain := 0
var next_extra := 25000
var player := Vector2(146,124)
var facing := Vector2.DOWN
var moving := false
var fire_delay := 0.0
var invulnerable := 0.0
var grunt_delay := 0.0
var entities: Array[Dictionary] = []
var shots: Array[Dictionary] = []
var enemy_shots: Array[Dictionary] = []
var effects: Array[Dictionary] = []
var rng := RandomNumberGenerator.new()
var next_id := 0
@onready var board: Node2D = $PixelScreen/Board
func _ready() -> void:
lives = starting_lives
rng.seed = 2084
setup_wave()
if not Engine.is_editor_hint():
$Audio.effect("startup")
func _unhandled_input(event: InputEvent) -> void:
if Engine.is_editor_hint() or event.is_echo(): return
if event.is_action_pressed("arcade_start"):
start_game()
func eight_way(vector: Vector2) -> Vector2:
if vector.length()<.25: return Vector2.ZERO
return Vector2.from_angle(roundf(vector.angle()/(PI/4))*(PI/4)).normalized()
func control_vector(prefix: String) -> Vector2:
return eight_way(Input.get_vector("robotron_"+prefix+"left","robotron_"+prefix+"right","robotron_"+prefix+"up","robotron_"+prefix+"down"))
func start_game() -> bool:
if is_running: return false
demo = false
score = 0
lives = starting_lives
wave = 1
next_extra = extra_life_interval
rng.randomize()
setup_wave()
phase = "ready"
phase_time = 2.0
$Audio.stop_all()
$Audio.effect("game-start")
return super.start_game()
func reset_game() -> void:
demo = false
score = 0
last_score = 0
wave = 1
lives = starting_lives
setup_wave()
phase = "title"
phase_time = 0
$Audio.stop_all()
super.reset_game()
func spawn(kind: String, position: Vector2) -> Dictionary:
next_id += 1
var entity := {"id":next_id,"kind":kind,"position":position,"velocity":Vector2.from_angle(rng.randf()*TAU),"timer":rng.randf_range(.6,2.0),"age":0.0,"frame":rng.randi_range(0,2),"alive":true,"spawns":0,"human_type":rng.randi_range(0,2),"hp":3 if kind=="brain" else 1}
entities.append(entity)
return entity
func spawn_point() -> Vector2:
for attempt in range(80):
var p := Vector2(rng.randf_range(15,277),rng.randf_range(28,220))
if p.distance_to(player)<=48: continue
var occupied := false
for e in entities:
if e.alive and p.distance_squared_to(e.position)<100:
occupied = true
break
if not occupied: return p
return Vector2(20,30)
func setup_wave() -> void:
entities.clear()
shots.clear()
enemy_shots.clear()
effects.clear()
player = Vector2(146,124)
facing = Vector2.DOWN
rescue_chain = 0
fire_delay = 0
invulnerable = 1.5
var brain_wave := wave%5==0
var tank_wave := wave>=7 and wave%5==2
for i in range(mini(15+wave*3,65)): spawn("grunt",spawn_point())
for i in range(5+mini(wave/3,5)): spawn("hulk",spawn_point())
for i in range(15 if not brain_wave else 5): spawn("electrode",spawn_point())
for i in range(20 if brain_wave else 3+mini(wave,5)): spawn("human",spawn_point())
if brain_wave:
for i in range(8+mini(wave,12)): spawn("brain",spawn_point())
elif tank_wave:
for i in range(5+mini(wave/5,5)): spawn("quark",spawn_point())
elif wave>=2:
for i in range(mini(wave,8)): spawn("spheroid",spawn_point())
func _physics_process(delta: float) -> void:
clock += delta
if Engine.is_editor_hint():
phase_time = fmod(clock,12)
board.queue_redraw()
return
if phase in ["title","story","roster","scores"]:
phase_time += delta
if phase_time>12:
phase_time = 0
match phase:
"title": phase = "story"
"story": phase = "roster"
"roster": begin_demo()
"scores": phase = "title"
elif phase in ["ready","dead","transition","lost"]:
phase_time -= delta
if phase_time<=0:
match phase:
"ready": phase = "play"
"dead":
if lives<=0: finish(false)
else: respawn()
"transition":
if not demo and waves_to_win>0 and wave>=waves_to_win: finish(true)
else:
wave += 1
setup_wave()
phase = "ready"
phase_time = 1.5
if wave%5==0: $Audio.effect("brain-wave-start")
"lost":
phase = "scores"
phase_time = 0
elif phase == "play":
var movement := Vector2.ZERO
var firing := Vector2.ZERO
if demo:
var target := Vector2(146+cos(demo_time*.8)*88,124+sin(demo_time*.8)*70)
movement = eight_way(target-player)
var enemy := nearest(player,false)
if not enemy.is_empty(): firing = eight_way(enemy.position-player)
else:
movement = control_vector("move_")
firing = control_vector("fire_")
step_world(delta,movement,firing)
if demo:
demo_time += delta
if demo_time>25: end_demo()
board.queue_redraw()
func begin_demo() -> void:
demo = true
demo_time = 0
score = 0
lives = 3
wave = 3
rng.seed = 2084
setup_wave()
phase = "play"
$Audio.stop_all()
func end_demo() -> void:
demo = false
score = last_score
phase = "scores"
phase_time = 0
$Audio.stop_all()
func clamp_arena(p: Vector2, margin: float = 4) -> Vector2:
return p.clamp(ARENA.position+Vector2.ONE*margin,ARENA.end-Vector2.ONE*margin)
func nearest(p: Vector2, human: bool) -> Dictionary:
var best: Dictionary = {}
var distance := INF
for e in entities:
if not e.alive or (e.kind=="human")!=human or e.kind in ["electrode","hulk"]: continue
var d: float = p.distance_squared_to(e.position)
if d<distance:
distance = d
best = e
return best
func fire(direction: Vector2) -> bool:
if phase!="play" or direction==Vector2.ZERO or fire_delay>0 or shots.size()>=4: return false
shots.append({"position":player+direction*5,"velocity":direction*260})
fire_delay = .085
$Audio.effect("player-lazer")
return true
func radius(kind: String) -> float:
return 7.0 if kind in ["brain","hulk","tank"] else (5.0 if kind in ["spheroid","quark"] else 3.5)
func projectile(kind: String, origin: Vector2, target: Vector2) -> void:
if enemy_shots.size()>=70: return
var velocity := (target-origin).normalized()*(90 if kind=="shell" else 65)
enemy_shots.append({"kind":kind,"position":origin,"velocity":velocity,"life":9.0,"bounces":0})
$Audio.effect("tank-fire" if kind=="shell" else "enforcer-fire")
func step_world(delta: float, movement: Vector2, firing: Vector2) -> void:
moving = movement!=Vector2.ZERO
if moving: facing = movement
player = clamp_arena(player+movement*86*delta)
invulnerable = maxf(0,invulnerable-delta)
fire_delay = maxf(0,fire_delay-delta)
grunt_delay -= delta
if grunt_delay<=0:
grunt_delay = .6
if entities.any(func(e): return e.alive and e.kind=="grunt"): $Audio.effect("grunt-walk")
fire(firing)
for i in range(effects.size()-1,-1,-1):
effects[i].life -= delta
if effects[i].life<=0: effects.remove_at(i)
# Spawners append enemies; the snapshot prevents updating new enemies twice.
for e in entities.duplicate():
if not e.alive: continue
e.age += delta
e.timer -= delta
var direction: Vector2 = (player-e.position).normalized()
match e.kind:
"human":
if e.timer<=0:
e.velocity = eight_way(Vector2(rng.randf_range(-1,1),rng.randf_range(-1,1)))
e.timer = rng.randf_range(.4,1.4)
e.position = clamp_arena(e.position+e.velocity*12*delta)
if player.distance_to(e.position)<7: save_human(e)
"grunt","prog":
e.position += eight_way(direction)*(32 if e.kind=="grunt" else 58)*difficulty*delta
if e.kind=="grunt":
for obstacle in entities:
if obstacle.alive and obstacle.kind=="electrode" and e.position.distance_to(obstacle.position)<6:
e.alive = false
obstacle.alive = false
burst(e.position)
break
"hulk","brain":
var human := nearest(e.position,true)
var destination: Vector2 = human.position if not human.is_empty() else player
if e.timer<=0:
e.velocity = eight_way(destination-e.position)
e.timer = .6 if e.kind=="hulk" else .2
e.position = clamp_arena(e.position+e.velocity*(17 if e.kind=="hulk" else 22)*difficulty*delta)
if not human.is_empty() and e.position.distance_to(human.position)<8:
human.alive = false
if e.kind=="brain":
var prog := spawn("converting",human.position)
prog.timer = .7
$Audio.effect("human-to-prog")
else:
burst(human.position)
$Audio.effect("human-die")
if e.kind=="brain" and int(e.age/2.2)!=int((e.age-delta)/2.2): projectile("cruise",e.position,player)
"converting":
if e.timer<=0:
e.kind = "prog"
$Audio.effect("prog-transformation")
"spheroid","quark":
var next: Vector2 = e.position+e.velocity*(45 if e.kind=="spheroid" else 28)*delta
if next.x<14 or next.x>278: e.velocity.x *= -1
if next.y<26 or next.y>222: e.velocity.y *= -1
e.position = clamp_arena(e.position+e.velocity*40*delta)
if e.timer<=0:
spawn("enforcer" if e.kind=="spheroid" else "tank",e.position)
e.spawns += 1
e.timer = 1.4
$Audio.effect("spheroid-spawn" if e.kind=="spheroid" else "quark-spawn")
if e.spawns>=4:
e.alive = false
burst(e.position)
"enforcer","tank":
if e.timer<=0:
e.velocity = Vector2.from_angle(rng.randf()*TAU)
e.timer = rng.randf_range(.8,1.7)/difficulty
projectile("spark" if e.kind=="enforcer" else "shell",e.position,player)
e.position = clamp_arena(e.position+e.velocity*(30 if e.kind=="enforcer" else 18)*delta)
if e.alive and e.kind!="human" and e.kind!="converting" and player.distance_to(e.position)<radius(e.kind)+2 and invulnerable<=0:
die()
return
update_shots(delta)
update_enemy_shots(delta)
entities = entities.filter(func(e): return e.alive)
if phase=="play" and remaining_enemies()==0:
phase = "transition"
phase_time = 1.8
enemy_shots.clear()
shots.clear()
$Audio.stop_all()
$Audio.effect("level-transition")
func update_shots(delta: float) -> void:
for i in range(shots.size()-1,-1,-1):
var shot: Dictionary = shots[i]
var distance: float = shot.velocity.length()*delta
var direction: Vector2 = shot.velocity.normalized()
var hit := false
while distance>0 and not hit:
var step := minf(2,distance)
shot.position += direction*step
distance -= step
if not ARENA.has_point(shot.position): hit = true
for enemy in entities:
if hit: break
if not enemy.alive or enemy.kind in ["human","converting"]: continue
if shot.position.distance_to(enemy.position)<radius(enemy.kind):
hit = true
if enemy.kind=="hulk":
enemy.position = clamp_arena(enemy.position+direction*4)
enemy.velocity = direction
enemy.timer = .3
else: damage(enemy)
for bullet in enemy_shots:
if not hit and bullet.life>0 and shot.position.distance_to(bullet.position)<4:
bullet.life = 0
hit = true
add_score(50 if bullet.kind=="shell" else (75 if bullet.kind=="cruise" else 25))
if hit: shots.remove_at(i)
func update_enemy_shots(delta: float) -> void:
for i in range(enemy_shots.size()-1,-1,-1):
var b: Dictionary = enemy_shots[i]
b.life -= delta
if b.kind=="cruise": b.velocity = b.velocity.move_toward((player-b.position).normalized()*78,delta*45)
b.position += b.velocity*delta
if not ARENA.has_point(b.position):
if b.kind=="shell" and b.bounces<4:
if b.position.x<ARENA.position.x or b.position.x>ARENA.end.x: b.velocity.x *= -1
if b.position.y<ARENA.position.y or b.position.y>ARENA.end.y: b.velocity.y *= -1
b.position = clamp_arena(b.position,1)
b.bounces += 1
$Audio.effect("tank-projectile-bounce")
else: b.life = 0
if b.life>0 and b.position.distance_to(player)<5 and invulnerable<=0:
die()
return
if b.life<=0: enemy_shots.remove_at(i)
func damage(enemy: Dictionary) -> void:
enemy.hp -= 1
if enemy.hp>0: return
enemy.alive = false
add_score(POINTS.get(enemy.kind,0))
burst(enemy.position)
var sounds := {"brain":"brain-explode","tank":"tank-explode","electrode":"electrode-explode","spheroid":"spheroid-pop"}
$Audio.effect(sounds.get(enemy.kind,"robot-explode"))
func save_human(human: Dictionary) -> void:
if not human.alive: return
human.alive = false
rescue_chain = mini(5,rescue_chain+1)
add_score(rescue_chain*1000)
burst(human.position,str(rescue_chain*1000))
$Audio.effect("human-save")
func add_score(value: int) -> void:
score += value
if demo: return
high_score = maxi(high_score,score)
while extra_life_interval>0 and score>=next_extra:
lives += 1
next_extra += extra_life_interval
$Audio.effect("extra-life")
func burst(position: Vector2, label: String = "") -> void:
effects.append({"position":position,"life":.55 if label!="" else .25,"label":label})
func remaining_enemies() -> int:
var count := 0
for e in entities:
if e.alive and e.kind not in ["human","hulk","electrode"]: count += 1
return count
func die() -> void:
if phase!="play": return
lives -= 1
rescue_chain = 0
phase = "dead"
phase_time = 1.2
$Audio.stop_all()
$Audio.effect("player-death")
func respawn() -> void:
player = Vector2(146,124)
shots.clear()
enemy_shots.clear()
invulnerable = 2
for e in entities:
if e.alive and e.kind!="human" and e.position.distance_to(player)<48: e.position = spawn_point()
phase = "ready"
phase_time = 1
func finish(won: bool) -> void:
if demo:
end_demo()
return
if not is_running: return
last_score = score
phase = "won" if won else "lost"
phase_time = 5
$Audio.stop_all()
if won: win_game()
else:
lose_game()
if score>0 and score>=high_score: $Audio.effect("high-score-top")
+1
View File
@@ -0,0 +1 @@
uid://dllelyq8hgx07
+14
View File
@@ -0,0 +1,14 @@
shader_type canvas_item;
render_mode unshaded;
uniform bool animate_title = false;
uniform int palette_step = 0;
void fragment() {
vec4 pixel = texture(TEXTURE, UV) * COLOR;
// Only the supplied title lettering cycles; credits and sprites keep their palette.
if (animate_title && UV.x>0.13 && UV.x<0.86 && UV.y>0.20 && UV.y<0.49 && pixel.r>0.8 && pixel.b<0.1) {
vec3 palette[7] = vec3[7](vec3(1,0,0),vec3(1,0.5,0),vec3(1,1,0),vec3(0,1,0),vec3(0,1,1),vec3(1,0,1),vec3(1,1,1));
int index = (palette_step + (pixel.g>0.8 ? 2 : 0)) % 7;
pixel.rgb = palette[index];
}
COLOR = pixel;
}
@@ -0,0 +1 @@
uid://bowtwv6ih8lj