Compare commits

..

2 Commits

Author SHA1 Message Date
anekdotin 1ebcca3807 Replace tiled skybox background with a procedural starfield shader
The old background tiled a single non-seamless 1024x1024 skybox render
across the map, producing visible seams and stars too faint/small to
survive minification. Replace it with a canvas shader (starfield.gdshader)
that draws layered depth stars + soft nebula tint directly, so there's
no repeating tile (no seams) and stars stay pixel-crisp at any zoom.
Applied to both the in-game map background and the main menu backdrop.

Also fixes a bug the swap introduced: world.gd cast the map's
Background node to TextureRect to read world bounds, which silently
failed once Background became a ColorRect, leaving GameConfig.world_bounds
on its tiny fallback default and hard-clamping every ship into an
invisible box near the map center. Cast to Control instead.

Also clears leftover placeholder Walls/Asteroids tile data in
map_01.tscn that sat directly in the spawn-to-spawn flight corridor —
harmless once bounds were fixed, but still untuned test content per
CLAUDE.md's own Current Tasks list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 21:41:47 -04:00
anekdotin 5fe7c01052 Remove Apex Dynamics faction, retune bullet speed/size
Drop Apex Dynamics from the playable roster (2 races now: Inner Sphere
Navy, Outer Rim Collective) to simplify the 2D art pipeline; World's
race-offer logic just shuffles both remaining races instead of picking
2 of N. Also fix bullets using a constant muzzle velocity instead of
adding the shooter's own velocity, and shrink the bullet hitbox/sprite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 21:41:35 -04:00
13 changed files with 162 additions and 101 deletions
+2
View File
@@ -40,6 +40,8 @@ README for how to run/extend it.
18. **Engine-flame sprite swap on thrust** — 6 of item 17's 9 ships (ISN's Patriot/Barrage/Behemoth, ORC's Rail-Jack/Scrap-Spitter/Iron-Clad) gained a second `_flame.png` sprite alongside their idle one, cropped from the same concept sheets' flame-frame columns (ISN's sheet has a clean idle/flame pair per ship; ORC's only unambiguous case was Rail-Jack's brighter 3rd-column flame, so Scrap-Spitter/Iron-Clad's flame PNGs are just copies of their idle sprite for now — their sheet's other columns are damage/weapon-fire poses, not more exhaust). Behemoth's flame was composited from two separate sheet crops (clean idle hull + a flame-only crop of just its thrusters) rather than cropped directly, since the sheet's flame frame has a large red shield-ability graphic overlapping the hull that isn't exhaust. Apex's 3 ships (Lancet/Pulsar/Sovereign) have no flame art — their concept sheet never drew a thrust pose — so they're untouched and always show their one sprite. Wired into gameplay via a new optional `flame_path` per ship in `team_select.gd`'s `RACES`, threaded through `PlayerRegistry` (`submit_local_loadout`/`register_bot`/the loadout RPCs, new `ship_flame_path` field, default `""`) so bots and remote peers all get it too, not just the local owner. `ship_movement.gd`'s `_set_thrust_sprite()` swaps the `Sprite2D` texture based on whether the up/down thrust key is *currently held*, not current speed — this game's movement has no drag, so a first cut keyed off `velocity.length()` left the flame on long after letting off the gas since a released ship keeps drifting at speed. The owner predicts this locally the instant it samples input (`_owner_tick`); the server does the same for bots/itself (`_server_tick`) and broadcasts its authoritative value as a new `thrusting` bool on the existing `_receive_state` RPC so every other peer's view of a ship — which never sees raw input, only replicated state — stays in sync too, with no separate RPC needed. Ships with no `flame_path` (Apex) just skip the check entirely (`_flame_texture == null` short-circuits). 18. **Engine-flame sprite swap on thrust** — 6 of item 17's 9 ships (ISN's Patriot/Barrage/Behemoth, ORC's Rail-Jack/Scrap-Spitter/Iron-Clad) gained a second `_flame.png` sprite alongside their idle one, cropped from the same concept sheets' flame-frame columns (ISN's sheet has a clean idle/flame pair per ship; ORC's only unambiguous case was Rail-Jack's brighter 3rd-column flame, so Scrap-Spitter/Iron-Clad's flame PNGs are just copies of their idle sprite for now — their sheet's other columns are damage/weapon-fire poses, not more exhaust). Behemoth's flame was composited from two separate sheet crops (clean idle hull + a flame-only crop of just its thrusters) rather than cropped directly, since the sheet's flame frame has a large red shield-ability graphic overlapping the hull that isn't exhaust. Apex's 3 ships (Lancet/Pulsar/Sovereign) have no flame art — their concept sheet never drew a thrust pose — so they're untouched and always show their one sprite. Wired into gameplay via a new optional `flame_path` per ship in `team_select.gd`'s `RACES`, threaded through `PlayerRegistry` (`submit_local_loadout`/`register_bot`/the loadout RPCs, new `ship_flame_path` field, default `""`) so bots and remote peers all get it too, not just the local owner. `ship_movement.gd`'s `_set_thrust_sprite()` swaps the `Sprite2D` texture based on whether the up/down thrust key is *currently held*, not current speed — this game's movement has no drag, so a first cut keyed off `velocity.length()` left the flame on long after letting off the gas since a released ship keeps drifting at speed. The owner predicts this locally the instant it samples input (`_owner_tick`); the server does the same for bots/itself (`_server_tick`) and broadcasts its authoritative value as a new `thrusting` bool on the existing `_receive_state` RPC so every other peer's view of a ship — which never sees raw input, only replicated state — stays in sync too, with no separate RPC needed. Ships with no `flame_path` (Apex) just skip the check entirely (`_flame_texture == null` short-circuits).
19. **Apex Dynamics faction removed** — cut down to 2 playable races (Inner Sphere Navy, Outer Rim Collective) to simplify the 2D art pipeline. `team_select.gd`'s `RACES` array lost its Apex Dynamics block (Lancet/Pulsar/Sovereign), with ISN/ORC renumbered to `id` 1/2. `world.gd`'s `decide_offered_races()` no longer picks "2 of N" races — with only 2 total, that logic was vestigial, so it now just shuffles the order of both remaining races (kept for spawn-side/left-right variety, since `team_select.gd`/`ship_movement.gd` still key off `offered[0]`/`offered[1]`). `overview/racesclasses.md`'s faction-ability Rock-Paper-Scissors loop (which structurally needed 3 legs) is rewritten as a single ISN-shield-vs-ORC-mines counter relationship; `overview.md`/`bots.md`/`structure.md`'s "2 of 3 races" language updated to reflect both races always being fielded. `assets/images/ships/apex/` and `assets/sound/ships/apex/` are left on disk but unreferenced, same as the older unused `terran/mech/vorg` folders from item 17 — not deleted since that wasn't asked for.
## Current Tasks ## Current Tasks
- [ ] Asteroids and environment hazards - [ ] Asteroids and environment hazards
+1 -1
View File
@@ -2,7 +2,7 @@
## Rules ## Rules
- Each casual match picks **2 of the 3 races** at random - Each casual match fields both races (order presented is randomized)
- Bots fill empty slots until the match reaches **7v7** (14 total) - Bots fill empty slots until the match reaches **7v7** (14 total)
- Once 7v7 is reached, **no more bots are added** — additional human players replace bots as they join - Once 7v7 is reached, **no more bots are added** — additional human players replace bots as they join
- Bots are removed when real players join their team slot - Bots are removed when real players join their team slot
+6 -6
View File
@@ -2,13 +2,13 @@
## Concept ## Concept
A 2D pixel-art top-down space shooter inspired by **Subspace Continuum**, built for **Steam Deck and PC**. Three alien races fight for control of a galaxy. Fast matchmaking like Rocket League — quick in, quick out. A 2D pixel-art top-down space shooter inspired by **Subspace Continuum**, built for **Steam Deck and PC**. Two alien races fight for control of a galaxy. Fast matchmaking like Rocket League — quick in, quick out.
## Core Loop ## Core Loop
1. Launch → Main Menu 1. Launch → Main Menu
2. Enter callsign → click CASUAL 2. Enter callsign → click CASUAL
3. World loads → pick your race (2 of 3 randomly offered per match) → pick your ship 3. World loads → pick your race (both offered every match) → pick your ship
4. Fight in a 25v25 battle 4. Fight in a 25v25 battle
5. Return to main menu 5. Return to main menu
@@ -25,7 +25,7 @@ A 2D pixel-art top-down space shooter inspired by **Subspace Continuum**, built
Main Menu Main Menu
└─ CASUAL ──► World loads immediately (game active) └─ CASUAL ──► World loads immediately (game active)
└─ TeamSelect overlay appears └─ TeamSelect overlay appears
├─ Pick race (2 random of 3 offered) ├─ Pick race (both offered every match)
└─ Pick ship (4 available, sprites shown) └─ Pick ship (4 available, sprites shown)
└─ Player spawns centred, invincible briefly └─ Player spawns centred, invincible briefly
└─ ESC / Start ──► Pause menu overlay └─ ESC / Start ──► Pause menu overlay
@@ -38,7 +38,7 @@ Main Menu
## Races ## Races
3 playable races chosen from a pool of 10 concepts — see `racesclasses.md`. Each race has 5 ship classes: 2 playable races (a third, Apex Dynamics, was cut to simplify the art pipeline) — see `racesclasses.md`. Each race has 5 ship classes:
| Class | Role | | Class | Role |
|-------|------| |-------|------|
@@ -52,8 +52,8 @@ Races are visually distinct — critical for reading a 25v25 battlefield at a gl
## Match Setup ## Match Setup
- Each match randomly picks **2 of the 3 races** to field - Both races are fielded every match (order they're presented in is randomized)
- Player picks which of those 2 they fight for, then picks their ship - Player picks which of the 2 they fight for, then picks their ship
- Bot fill ensures a minimum of **7v7** in casual; no bots added beyond that - Bot fill ensures a minimum of **7v7** in casual; no bots added beyond that
## Design Philosophy ## Design Philosophy
+16 -39
View File
@@ -2,30 +2,21 @@
## Setting ## Setting
Three factions fight inside the massive shielding, pipes, and superstructure of Two factions fight inside the massive shielding, pipes, and superstructure of
refineries, shipyards, and mining stations — not open void. Grounded, blue-collar refineries, shipyards, and mining stations — not open void. Grounded, blue-collar
sci-fi in the vein of *The Expanse* (corporate hegemony vs. disciplined inner-planet sci-fi in the vein of *The Expanse* (disciplined inner-planet military vs. gritty
military vs. gritty outer-belt miners), but built as a fast, arcade 2D tactical outer-belt miners), but built as a fast, arcade 2D tactical arena shooter rather
arena shooter rather than a hard-sci-fi sim. Tight factory corridors and choke than a hard-sci-fi sim. Tight factory corridors and choke points are the map
points are the map language — see `multiplayer.md`. language — see `multiplayer.md`.
> **Status:** This replaces the previous 10-alien-race concept pool and the > **Status:** This replaces the previous 10-alien-race concept pool and the
> Terran Republic / Mechanos Sovereignty / Vorg Swarm roster. Doc-only for now — > Terran Republic / Mechanos Sovereignty / Vorg Swarm roster. A third faction,
> game code (`team_select.gd` RACES data, `GameConfig`, existing > Apex Dynamics (The Corporate Syndicate), was cut entirely to simplify the art
> `assets/images/ships/<race>/` art) still reflects the old races until that work > pipeline — no code or docs should reference it going forward.
> is scheduled.
--- ---
## The 3 Factions ## The 2 Factions
### Apex Dynamics — The Corporate Syndicate
**Weapon type:** High-tech energy (coherent light / plasma)
**Visual:** Sleek, pearlescent-white hulls with razor-sharp geometric lines and
glowing neon-blue engine trails — looks more like a high-end luxury smartphone
than a military weapon.
**Feel:** High-tech and precise. Wins by not being seen until it's too late.
**Tactical ability:** **Active Camouflage** — see [Faction Abilities](#faction-abilities-universal-system)
### Inner Sphere Navy (ISN) — The Military ### Inner Sphere Navy (ISN) — The Military
**Weapon type:** Standard-issue ballistics / ordnance **Weapon type:** Standard-issue ballistics / ordnance
@@ -55,14 +46,6 @@ Every faction fields the same 3-ship structure, re-skinned to its own theme:
| **Gunner** | Rapid-fire spray/burst, more shots for less damage each | | **Gunner** | Rapid-fire spray/burst, more shots for less damage each |
| **Tank** | Bigger and slower; fires both bullets and missiles | | **Tank** | Bigger and slower; fires both bullets and missiles |
### Apex Dynamics
| Ship | Class | Weapon |
|------|-------|--------|
| **Lancet** | Fighter | A single, high-precision bolt of blue coherent light |
| **Pulsar** | Gunner | A wide, rapid-fire fan of low-damage green plasma pulses |
| **Sovereign** | Tank | Homing micro-missiles alongside dual heavy plasma batteries |
### Inner Sphere Navy (ISN) ### Inner Sphere Navy (ISN)
| Ship | Class | Weapon | | Ship | Class | Weapon |
@@ -85,7 +68,7 @@ Every faction fields the same 3-ship structure, re-skinned to its own theme:
Each faction has exactly one tactical ability. Every ship in that faction gets Each faction has exactly one tactical ability. Every ship in that faction gets
it — what changes per class is scale, not kind, so the ability reads instantly it — what changes per class is scale, not kind, so the ability reads instantly
off a ship's faction regardless of which of the 3 hulls it's flying. Same energy off a ship's faction regardless of which of the 2 hulls it's flying. Same energy
cost and cooldown across factions; duration/impact scales with the hull: cost and cooldown across factions; duration/impact scales with the hull:
| Class | Scaling rule | | Class | Scaling rule |
@@ -94,12 +77,6 @@ cost and cooldown across factions; duration/impact scales with the hull:
| Gunner | Baseline cooldown and effect | | Gunner | Baseline cooldown and effect |
| Tank | Long cooldown, largest/heaviest effect | | Tank | Long cooldown, largest/heaviest effect |
### Apex Dynamics — Active Camouflage
Ship turns 90% transparent and disappears from radar. Firing or taking damage
breaks it instantly. Fighter gets a quick, short slip; Tank's lasts longest but
is riskiest to commit a slow hull to.
**Use:** Slip past a defender in a narrow pipe, flank, or break a missile lock.
### Inner Sphere Navy — Overcharged Aegis ### Inner Sphere Navy — Overcharged Aegis
A frontal hard-light barrier absorbs 100% of incoming damage from the front for A frontal hard-light barrier absorbs 100% of incoming damage from the front for
a few seconds. Sides and rear stay exposed, turn rate drops, and the ship can't a few seconds. Sides and rear stay exposed, turn rate drops, and the ship can't
@@ -117,15 +94,15 @@ larger blast radius; Fighter's arms faster but hits smaller.
**Use:** Cover a retreat down a corridor, or seed a choke point/objective before **Use:** Cover a retreat down a corridor, or seed a choke point/objective before
a push. a push.
### The Rock-Paper-Scissors Loop ### Faction Interaction
With only 2 factions, the abilities form a single counter rather than a loop:
- **ORC mines counter Apex cloak** — a cloaked ship blundering through a mined
corridor eats the blast, which also breaks the cloak.
- **ISN shield counters ORC mines** — the frontal barrier absorbs a mine - **ISN shield counters ORC mines** — the frontal barrier absorbs a mine
detonation, clearing the path for the team behind it. detonation, clearing the path for the team behind it.
- **Apex cloak counters ISN shield** — a slow, frontal-shielded push leaves the - **ORC mines punish a shield push once it drops** — the shield can't fire and
ISN ship's flank and engines open for a cloaked ship to slip around and loses turn rate while up, so a mine dropped in its path (or behind it, once
punish. the barrier expires) still threatens the push.
No ability wins a fight by itself — it depends on the map and the moment it's No ability wins a fight by itself — it depends on the map and the moment it's
used, same as a smoke/flash/molotov in CS:GO. used, same as a smoke/flash/molotov in CS:GO.
+1 -1
View File
@@ -104,7 +104,7 @@ Player (CharacterBody2D) ← ship_movement.gd
``` ```
main_menu.tscn main_menu.tscn
├── QUICK PLAY → matchmaking queue (casual) → world.tscn ├── QUICK PLAY → matchmaking queue (casual) → world.tscn
│ ├── TeamSelect overlay: pick race (2 random of 3 offered) │ ├── TeamSelect overlay: pick race (both offered every match)
│ ├── TeamSelect overlay: pick ship │ ├── TeamSelect overlay: pick ship
│ └── Player spawns → game live │ └── Player spawns → game live
├── SERVER SELECT → server_select.tscn → pick a server → world.tscn (same TeamSelect flow) ├── SERVER SELECT → server_select.tscn → pick a server → world.tscn (same TeamSelect flow)
+1 -1
View File
@@ -15,7 +15,7 @@ var ship_rotation_speed_held: float = 2.0
# fire their bullets in a side-by-side pair instead of Fighter's single shot # fire their bullets in a side-by-side pair instead of Fighter's single shot
# (see ship_movement.gd's _server_process_shoot()). # (see ship_movement.gd's _server_process_shoot()).
var ship_fire_rate: float = 0.15 var ship_fire_rate: float = 0.15
var bullet_speed: float = 450.0 var bullet_speed: float = 750.0
var ship_bullet_damage_by_role: Dictionary = {"Fighter": 100, "Gunner": 25, "Tank": 50} var ship_bullet_damage_by_role: Dictionary = {"Fighter": 100, "Gunner": 25, "Tank": 50}
var ship_bullet_count_by_role: Dictionary = {"Fighter": 1, "Gunner": 2, "Tank": 2} var ship_bullet_count_by_role: Dictionary = {"Fighter": 1, "Gunner": 2, "Tank": 2}
var bullet_side_spacing: float = 14.0 var bullet_side_spacing: float = 14.0
+3 -9
View File
@@ -55,18 +55,12 @@ func _build_ui() -> void:
func _add_bg() -> void: func _add_bg() -> void:
var bg := TextureRect.new() var bg := ColorRect.new()
bg.texture = load("res://assets/images/background/skybox/2.png") bg.material = ShaderMaterial.new()
bg.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL bg.material.shader = load("res://world/starfield.gdshader")
bg.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(bg) add_child(bg)
var ov := ColorRect.new()
ov.color = Color(0.0, 0.0, 0.0, 0.55)
ov.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(ov)
func _add_logo() -> void: func _add_logo() -> void:
var title := Label.new() var title := Label.new()
+3 -27
View File
@@ -1,7 +1,7 @@
extends CanvasLayer extends CanvasLayer
class_name TeamSelect class_name TeamSelect
# Movement speed factor by role, shared across all 3 factions so e.g. every # Movement speed factor by role, shared across both factions so e.g. every
# faction's Fighter moves at the same speed as every other faction's. # faction's Fighter moves at the same speed as every other faction's.
const SPEED_BY_ROLE := { const SPEED_BY_ROLE := {
"Fighter": 1.3, "Fighter": 1.3,
@@ -11,31 +11,7 @@ const SPEED_BY_ROLE := {
const RACES := [ const RACES := [
{ {
"id": 1, "name": "APEX DYNAMICS", "sub": "Sleek pearlescent-white corporate fleet — precision energy weapons, wins by not being seen", "id": 1, "name": "INNER SPHERE NAVY", "sub": "Gunmetal-gray navy of Earth, Mars, and Mercury — disciplined, frontal, built to hold a line",
"color": Color(0.55, 0.85, 1.0),
"ships": [
{
"name": "LANCET", "role": "Fighter",
"desc": "A single, high-precision bolt of blue coherent light. Built to poke from range and vanish before anything can return fire — the sharpest shot in the fleet, if you can land it.",
"path": "res://assets/images/ships/apex/lancet.png", "scale": 0.296, "speed_factor": SPEED_BY_ROLE.Fighter,
"sound_path": "res://assets/sound/ships/apex/apex_1.wav",
},
{
"name": "PULSAR", "role": "Gunner",
"desc": "A wide, rapid-fire fan of low-damage green plasma pulses. Trades precision for volume — spray enough bolts and something connects.",
"path": "res://assets/images/ships/apex/pulsar.png", "scale": 0.373, "speed_factor": SPEED_BY_ROLE.Gunner,
"sound_path": "res://assets/sound/ships/apex/apex_1.wav",
},
{
"name": "SOVEREIGN", "role": "Tank",
"desc": "Homing micro-missiles alongside dual heavy plasma batteries. Slow and telegraphed, but its lock-on missiles punish anyone who tries to just outrun it.",
"path": "res://assets/images/ships/apex/sovereign.png", "scale": 0.348, "speed_factor": SPEED_BY_ROLE.Tank,
"sound_path": "res://assets/sound/ships/apex/apex_1.wav",
},
],
},
{
"id": 2, "name": "INNER SPHERE NAVY", "sub": "Gunmetal-gray navy of Earth, Mars, and Mercury — disciplined, frontal, built to hold a line",
"color": Color(0.80, 0.32, 0.32), "color": Color(0.80, 0.32, 0.32),
"ships": [ "ships": [
{ {
@@ -62,7 +38,7 @@ const RACES := [
], ],
}, },
{ {
"id": 3, "name": "OUTER RIM COLLECTIVE", "sub": "Gritty, jury-rigged mining rebels — kinetic weapons scavenged and welded onto anything that flies", "id": 2, "name": "OUTER RIM COLLECTIVE", "sub": "Gritty, jury-rigged mining rebels — kinetic weapons scavenged and welded onto anything that flies",
"color": Color(0.82, 0.52, 0.24), "color": Color(0.82, 0.52, 0.24),
"ships": [ "ships": [
{ {
+2 -2
View File
@@ -3,7 +3,7 @@
[ext_resource type="Script" path="res://ships/bullet.gd" id="1"] [ext_resource type="Script" path="res://ships/bullet.gd" id="1"]
[sub_resource type="CircleShape2D" id="CircleShape2D_1"] [sub_resource type="CircleShape2D" id="CircleShape2D_1"]
radius = 4.0 radius = 2.8
[node name="Bullet" type="Area2D"] [node name="Bullet" type="Area2D"]
collision_layer = 0 collision_layer = 0
@@ -15,4 +15,4 @@ shape = SubResource("CircleShape2D_1")
[node name="Polygon2D" type="Polygon2D" parent="."] [node name="Polygon2D" type="Polygon2D" parent="."]
color = Color(1, 0.9, 0.2, 1) color = Color(1, 0.9, 0.2, 1)
polygon = PackedVector2Array(5, 0, 4.33, 2.5, 2.5, 4.33, 0, 5, -2.5, 4.33, -4.33, 2.5, -5, 0, -4.33, -2.5, -2.5, -4.33, 0, -5, 2.5, -4.33, 4.33, -2.5) polygon = PackedVector2Array(3.5, 0, 3.031, 1.75, 1.75, 3.031, 0, 3.5, -1.75, 3.031, -3.031, 1.75, -3.5, 0, -3.031, -1.75, -1.75, -3.031, 0, -3.5, 1.75, -3.031, 3.031, -1.75)
+7 -3
View File
@@ -33,8 +33,8 @@ var _invincible_timer: float = 0.0
var _ship_speed_factor: float = 1.0 var _ship_speed_factor: float = 1.0
# Idle (no-flame) vs engine-flame sprite swap — see _update_thrust_visual(). # Idle (no-flame) vs engine-flame sprite swap — see _update_thrust_visual().
# _flame_texture stays null for ships with no flame_path (e.g. Apex), which # _flame_texture stays null for ships with no flame_path, which just keeps
# just keeps _update_thrust_visual() from ever touching the sprite. # _update_thrust_visual() from ever touching the sprite.
var _idle_texture: Texture2D var _idle_texture: Texture2D
var _flame_texture: Texture2D var _flame_texture: Texture2D
var _thrusting: bool = false var _thrusting: bool = false
@@ -443,7 +443,11 @@ func _server_process_shoot(delta: float, input: Dictionary) -> void:
var forward := Vector2.UP.rotated(rotation) var forward := Vector2.UP.rotated(rotation)
var right := Vector2.RIGHT.rotated(rotation) var right := Vector2.RIGHT.rotated(rotation)
var base_pos := global_position + forward * 40.0 var base_pos := global_position + forward * 40.0
var bullet_vel := forward * GameConfig.bullet_speed + velocity # Constant muzzle velocity regardless of the shooter's own speed --
# used to add `velocity` on top, which made bullets faster fired
# from a ship moving forward and slower (even reversed-looking)
# fired while drifting backward.
var bullet_vel := forward * GameConfig.bullet_speed
var spacing := GameConfig.bullet_side_spacing var spacing := GameConfig.bullet_side_spacing
var start := -spacing * (count - 1) / 2.0 var start := -spacing * (count - 1) / 2.0
for i in count: for i in count:
+6 -7
View File
@@ -1,27 +1,26 @@
[gd_scene format=4 uid="uid://cecn8fghm8h3j"] [gd_scene format=4 uid="uid://cecn8fghm8h3j"]
[ext_resource type="TileSet" uid="uid://cufng7112p6a7" path="res://world/world_tileset.tres" id="1_tileset"] [ext_resource type="TileSet" uid="uid://cufng7112p6a7" path="res://world/world_tileset.tres" id="1_tileset"]
[ext_resource type="Texture2D" uid="uid://d0i6jwqh131b1" path="res://assets/images/background/skybox/1.png" id="2_skybox"] [ext_resource type="Shader" path="res://world/starfield.gdshader" id="2_starfield"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_starfield"]
shader = ExtResource("2_starfield")
[node name="Map01" type="Node2D" unique_id=1843060857] [node name="Map01" type="Node2D" unique_id=1843060857]
[node name="Background" type="TextureRect" parent="." unique_id=1935208901] [node name="Background" type="ColorRect" parent="." unique_id=1935208901]
texture_repeat = 2 material = SubResource("ShaderMaterial_starfield")
offset_left = -7008.0 offset_left = -7008.0
offset_top = -3000.0 offset_top = -3000.0
offset_right = 7008.0 offset_right = 7008.0
offset_bottom = 3000.0 offset_bottom = 3000.0
texture = ExtResource("2_skybox")
expand_mode = 3
[node name="Walls" type="TileMapLayer" parent="." unique_id=1965607014] [node name="Walls" type="TileMapLayer" parent="." unique_id=1965607014]
scale = Vector2(0.15, 0.15) scale = Vector2(0.15, 0.15)
tile_map_data = PackedByteArray("AAAFACYAAAABAAAAAAAFABkAAAABAAAAAAAFAA0AAAABAAAAAAAFAAAAAAABAAAAAAAFAPT/AAABAAAAAAARABEAAAABAAEAAAA=")
tile_set = ExtResource("1_tileset") tile_set = ExtResource("1_tileset")
[node name="Asteroids" type="TileMapLayer" parent="." unique_id=1745417803] [node name="Asteroids" type="TileMapLayer" parent="." unique_id=1745417803]
scale = Vector2(0.15, 0.15) scale = Vector2(0.15, 0.15)
tile_map_data = PackedByteArray("AAAMAAEAAQAAAAMAAAALAPz/AQAAAAMAAAD6////AQAAAAMAAAD6/wgAAQAAAAMAAAAOAAgAAQAAAAMAAAA=")
tile_set = ExtResource("1_tileset") tile_set = ExtResource("1_tileset")
[node name="SpawnPoints" type="Node2D" parent="." unique_id=1668900368] [node name="SpawnPoints" type="Node2D" parent="." unique_id=1668900368]
+108
View File
@@ -0,0 +1,108 @@
shader_type canvas_item;
// Procedural starfield: draws directly over the whole background rect in one
// pass, so there's no repeating tile and therefore no seams (unlike the old
// approach of tiling a single non-seamless skybox render). Star size/glow use
// smoothstep, not texture sampling, so they stay pixel-crisp at any zoom
// instead of getting minified/blurred by texture filtering.
uniform vec4 sky_color: source_color = vec4(0.008, 0.01, 0.025, 1.0);
uniform vec4 nebula_color: source_color = vec4(0.05, 0.08, 0.16, 1.0);
uniform float nebula_scale = 0.0025;
uniform float nebula_strength = 0.5;
uniform float cell_size_far = 56.0;
uniform float density_far = 0.10;
uniform float size_far = 0.7;
uniform float brightness_far = 0.35;
uniform float cell_size_mid = 100.0;
uniform float density_mid = 0.14;
uniform float size_mid = 1.0;
uniform float brightness_mid = 0.5;
uniform float cell_size_near = 200.0;
uniform float density_near = 0.18;
uniform float size_near = 1.4;
uniform float brightness_near = 0.7;
varying vec2 world_pos;
void vertex() {
world_pos = VERTEX;
}
float hash11(float p) {
p = fract(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
float hash21(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec2 hash22(vec2 p) {
return vec2(hash21(p), hash21(p + 19.19));
}
float value_noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = hash21(i);
float b = hash21(i + vec2(1.0, 0.0));
float c = hash21(i + vec2(0.0, 1.0));
float d = hash21(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amp = 0.5;
for (int i = 0; i < 4; i++) {
value += amp * value_noise(p);
p *= 2.0;
amp *= 0.5;
}
return value;
}
// One depth layer of stars on an independent grid, so far/mid/near layers
// never line up with each other.
vec3 star_layer(vec2 pos, float cell_size, float density, float size, float brightness) {
vec2 cell = floor(pos / cell_size);
float roll = hash21(cell);
float has_star = step(1.0 - density, roll);
vec2 jitter = hash22(cell + 0.5);
vec2 star_pos = (cell + jitter) * cell_size;
float d = length(pos - star_pos);
float core = smoothstep(size, 0.0, d);
float halo = smoothstep(size * 3.5, 0.0, d) * 0.15;
float glow = (core + halo) * has_star * brightness;
float tint_roll = hash21(cell + 7.7);
vec3 tint = mix(vec3(0.75, 0.82, 1.0), vec3(1.0, 0.92, 0.75), step(0.5, tint_roll));
tint = mix(vec3(1.0), tint, 0.5);
return tint * glow;
}
void fragment() {
vec2 pos = world_pos;
float nebula = fbm(pos * nebula_scale);
vec3 color = mix(sky_color.rgb, nebula_color.rgb, clamp(nebula * nebula_strength, 0.0, 1.0));
vec3 stars = vec3(0.0);
stars += star_layer(pos, cell_size_far, density_far, size_far, brightness_far);
stars += star_layer(pos + vec2(500.0, 0.0), cell_size_mid, density_mid, size_mid, brightness_mid);
stars += star_layer(pos + vec2(0.0, 900.0), cell_size_near, density_near, size_near, brightness_near);
COLOR = vec4(color + stars, 1.0);
}
+6 -5
View File
@@ -20,7 +20,7 @@ var _offered_race_ids: Array = []
func _ready() -> void: func _ready() -> void:
var map := MAP_SCENE.instantiate() var map := MAP_SCENE.instantiate()
_map_container.add_child(map) _map_container.add_child(map)
var background := map.get_node_or_null("Background") as TextureRect var background := map.get_node_or_null("Background") as Control
if background: if background:
GameConfig.world_bounds = Rect2(background.position, background.size) GameConfig.world_bounds = Rect2(background.position, background.size)
_build_tile_collisions(map) _build_tile_collisions(map)
@@ -36,15 +36,16 @@ func _ready() -> void:
spawn_peer(peer_id) spawn_peer(peer_id)
# Picks the match's 2 offered races once and caches them, so every caller # Picks the match's offered races once and caches them, so every caller
# (the server's own TeamSelect and every remote client's request) converges # (the server's own TeamSelect and every remote client's request) converges
# on the same pair regardless of call order. Also kicks off bot fill for # on the same pair regardless of call order. Also kicks off bot fill for
# those 2 races — see bots/bot_manager.gd. # those races — see bots/bot_manager.gd. Both factions are always offered
# (only order is randomized, for spawn-side variety).
func decide_offered_races() -> Array: func decide_offered_races() -> Array:
if _offered_race_ids.is_empty(): if _offered_race_ids.is_empty():
var ids := range(1, TeamSelect.RACES.size() + 1) var ids: Array = TeamSelect.RACES.map(func(race): return race.id)
ids.shuffle() ids.shuffle()
_offered_race_ids = [ids[0], ids[1]] _offered_race_ids = ids
# Deferred: this can run from TeamSelect._ready(), which (as World's # Deferred: this can run from TeamSelect._ready(), which (as World's
# child) fires before World's own _ready() — before _players/_spawner # child) fires before World's own _ready() — before _players/_spawner
# are assigned. Deferring runs it after the whole scene's _ready # are assigned. Deferring runs it after the whole scene's _ready