From 5fe7c010526da6ff2899ba9e06896b8e6239ae3d Mon Sep 17 00:00:00 2001 From: Edwin Eames Date: Thu, 16 Jul 2026 21:41:35 -0400 Subject: [PATCH] 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 --- CLAUDE.md | 2 ++ overview/bots.md | 2 +- overview/overview.md | 12 +++---- overview/racesclasses.md | 55 ++++++++++---------------------- overview/structure.md | 2 +- spacewar/autoload/game_config.gd | 2 +- spacewar/menu/team_select.gd | 30 ++--------------- spacewar/ships/bullet.tscn | 4 +-- spacewar/ships/ship_movement.gd | 10 ++++-- spacewar/world/world.gd | 9 +++--- 10 files changed, 44 insertions(+), 84 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c3661c0..2f3e8bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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). +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 - [ ] Asteroids and environment hazards diff --git a/overview/bots.md b/overview/bots.md index 4b3bc1a..e705ca1 100644 --- a/overview/bots.md +++ b/overview/bots.md @@ -2,7 +2,7 @@ ## 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) - 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 diff --git a/overview/overview.md b/overview/overview.md index b236e29..38f545a 100644 --- a/overview/overview.md +++ b/overview/overview.md @@ -2,13 +2,13 @@ ## 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 1. Launch → Main Menu 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 5. Return to main menu @@ -25,7 +25,7 @@ A 2D pixel-art top-down space shooter inspired by **Subspace Continuum**, built Main Menu └─ CASUAL ──► World loads immediately (game active) └─ TeamSelect overlay appears - ├─ Pick race (2 random of 3 offered) + ├─ Pick race (both offered every match) └─ Pick ship (4 available, sprites shown) └─ Player spawns centred, invincible briefly └─ ESC / Start ──► Pause menu overlay @@ -38,7 +38,7 @@ Main Menu ## 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 | |-------|------| @@ -52,8 +52,8 @@ Races are visually distinct — critical for reading a 25v25 battlefield at a gl ## Match Setup -- Each match randomly picks **2 of the 3 races** to field -- Player picks which of those 2 they fight for, then picks their ship +- Both races are fielded every match (order they're presented in is randomized) +- 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 ## Design Philosophy diff --git a/overview/racesclasses.md b/overview/racesclasses.md index b93e88a..039c287 100644 --- a/overview/racesclasses.md +++ b/overview/racesclasses.md @@ -2,30 +2,21 @@ ## 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 -sci-fi in the vein of *The Expanse* (corporate hegemony vs. disciplined inner-planet -military vs. gritty outer-belt miners), but built as a fast, arcade 2D tactical -arena shooter rather than a hard-sci-fi sim. Tight factory corridors and choke -points are the map language — see `multiplayer.md`. +sci-fi in the vein of *The Expanse* (disciplined inner-planet military vs. gritty +outer-belt miners), but built as a fast, arcade 2D tactical arena shooter rather +than a hard-sci-fi sim. Tight factory corridors and choke points are the map +language — see `multiplayer.md`. > **Status:** This replaces the previous 10-alien-race concept pool and the -> Terran Republic / Mechanos Sovereignty / Vorg Swarm roster. Doc-only for now — -> game code (`team_select.gd` RACES data, `GameConfig`, existing -> `assets/images/ships//` art) still reflects the old races until that work -> is scheduled. +> Terran Republic / Mechanos Sovereignty / Vorg Swarm roster. A third faction, +> Apex Dynamics (The Corporate Syndicate), was cut entirely to simplify the art +> pipeline — no code or docs should reference it going forward. --- -## The 3 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) +## The 2 Factions ### Inner Sphere Navy (ISN) — The Military **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 | | **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) | 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 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: | Class | Scaling rule | @@ -94,12 +77,6 @@ cost and cooldown across factions; duration/impact scales with the hull: | Gunner | Baseline cooldown and 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 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 @@ -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 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 detonation, clearing the path for the team behind it. -- **Apex cloak counters ISN shield** — a slow, frontal-shielded push leaves the - ISN ship's flank and engines open for a cloaked ship to slip around and - punish. +- **ORC mines punish a shield push once it drops** — the shield can't fire and + loses turn rate while up, so a mine dropped in its path (or behind it, once + the barrier expires) still threatens the push. 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. diff --git a/overview/structure.md b/overview/structure.md index 3bec2f3..7d76cef 100644 --- a/overview/structure.md +++ b/overview/structure.md @@ -104,7 +104,7 @@ Player (CharacterBody2D) ← ship_movement.gd ``` main_menu.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 │ └── Player spawns → game live ├── SERVER SELECT → server_select.tscn → pick a server → world.tscn (same TeamSelect flow) diff --git a/spacewar/autoload/game_config.gd b/spacewar/autoload/game_config.gd index 45fab6b..be301bb 100644 --- a/spacewar/autoload/game_config.gd +++ b/spacewar/autoload/game_config.gd @@ -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 # (see ship_movement.gd's _server_process_shoot()). 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_count_by_role: Dictionary = {"Fighter": 1, "Gunner": 2, "Tank": 2} var bullet_side_spacing: float = 14.0 diff --git a/spacewar/menu/team_select.gd b/spacewar/menu/team_select.gd index 3895270..8eeb061 100644 --- a/spacewar/menu/team_select.gd +++ b/spacewar/menu/team_select.gd @@ -1,7 +1,7 @@ extends CanvasLayer 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. const SPEED_BY_ROLE := { "Fighter": 1.3, @@ -11,31 +11,7 @@ const SPEED_BY_ROLE := { const RACES := [ { - "id": 1, "name": "APEX DYNAMICS", "sub": "Sleek pearlescent-white corporate fleet — precision energy weapons, wins by not being seen", - "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", + "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.80, 0.32, 0.32), "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), "ships": [ { diff --git a/spacewar/ships/bullet.tscn b/spacewar/ships/bullet.tscn index c9e708e..f6c869d 100644 --- a/spacewar/ships/bullet.tscn +++ b/spacewar/ships/bullet.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://ships/bullet.gd" id="1"] [sub_resource type="CircleShape2D" id="CircleShape2D_1"] -radius = 4.0 +radius = 2.8 [node name="Bullet" type="Area2D"] collision_layer = 0 @@ -15,4 +15,4 @@ shape = SubResource("CircleShape2D_1") [node name="Polygon2D" type="Polygon2D" parent="."] 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) diff --git a/spacewar/ships/ship_movement.gd b/spacewar/ships/ship_movement.gd index 6b25b0b..7522c2e 100644 --- a/spacewar/ships/ship_movement.gd +++ b/spacewar/ships/ship_movement.gd @@ -33,8 +33,8 @@ var _invincible_timer: float = 0.0 var _ship_speed_factor: float = 1.0 # 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 -# just keeps _update_thrust_visual() from ever touching the sprite. +# _flame_texture stays null for ships with no flame_path, which just keeps +# _update_thrust_visual() from ever touching the sprite. var _idle_texture: Texture2D var _flame_texture: Texture2D var _thrusting: bool = false @@ -443,7 +443,11 @@ func _server_process_shoot(delta: float, input: Dictionary) -> void: var forward := Vector2.UP.rotated(rotation) var right := Vector2.RIGHT.rotated(rotation) 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 start := -spacing * (count - 1) / 2.0 for i in count: diff --git a/spacewar/world/world.gd b/spacewar/world/world.gd index b1b05ca..7227784 100644 --- a/spacewar/world/world.gd +++ b/spacewar/world/world.gd @@ -36,15 +36,16 @@ func _ready() -> void: 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 # 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: 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() - _offered_race_ids = [ids[0], ids[1]] + _offered_race_ids = ids # Deferred: this can run from TeamSelect._ready(), which (as World's # child) fires before World's own _ready() — before _players/_spawner # are assigned. Deferring runs it after the whole scene's _ready