Initial commit before folder reorganization

Checkpoint of the existing flat file layout prior to restructuring
into autoload/, menu/, ships/, world/, and assets/ folders.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 20:26:48 -04:00
commit e1410c234a
63 changed files with 3926 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
# Spacewar
2D multiplayer space shooter inspired by Subspace Continuum. Built in **Godot 4.7** (GDScript). Target platform: Steam Deck + PC. Three races fight for galaxy control across casual (25v25) and ranked (5v5) modes.
## Design Docs (`overview/`)
| File | Contents |
|------|----------|
| `overview.md` | Concept, core loop, game flow, match sizes |
| `racesclasses.md` | 3 chosen races, 5 ship classes each |
| `multiplayer.md` | Maps, game modes, galaxy war meta |
| `bots.md` | Bot fill rules for casual matches |
| `tech.md` | Engine, networking architecture, checklist |
| `money.md` | Monetization strategy |
| `structure.md` | Project file tree, scene graph, input map |
## Completed
1. **Server browser** — player name input, race toggles, server list (Trench Wars 0/32), JOIN button; stores name + race in `GameConfig` autoload
2. **Main menu** — CASUAL / RANKED (disabled) buttons, callsign input, rank badge, QUIT; clears player state on load
3. **In-game pause menu** — ESC / controller Start button toggles overlay; game keeps running; RESUME, SETTINGS (stub), SELECT TEAM (stub), QUIT TO MENU, QUIT TO DESKTOP
4. **Team & ship selection** — shows on world load before player spawns; 2 races randomly offered per match; ship grid with sprites; player spawns with chosen ship after selection
## Current Tasks
- [ ] Replace placeholder race names + ship sprites with the 3 chosen races (see `racesclasses.md`)
- [ ] Asteroids and environment hazards
- [ ] Bot fill for casual (minimum 7v7, bots fill empty slots — see `bots.md`)
- [ ] Sound effects (thrust, shoot, explosion, UI)
- [ ] Multiplayer networking — ENet authoritative server (see `tech.md`)
- [ ] Galaxy war meta / sector control for casual (see `multiplayer.md`)
- [ ] Ranked matchmaking + MMR
+21
View File
@@ -0,0 +1,21 @@
# Bots — Casual Mode Fill
## Rules
- Each casual match picks **2 of the 3 races** at random
- 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
## Why 7v7 Minimum
- Ensures the match feels active even at low population
- Avoids the ghost-town feeling of a 25v25 map with 3 players
- Bots are not added beyond 7v7 so human players always dominate strategy
## Implementation Notes (not yet built)
- Bots use simplified AI: thrust toward nearest enemy, shoot when in range
- Bot difficulty: intentionally easy (this is casual — bots are filler, not challenge)
- Bot names: procedurally generated callsigns so they're not obviously bots in the HUD
- Server tracks human vs bot count per team and manages swap-in on player join
+5
View File
@@ -0,0 +1,5 @@
# Dev Notes
- First Godot project — learning the engine while building
- GDScript is the primary language; UI is built programmatically in `_ready()` rather than in the editor scene tree
- Steam Deck is the primary target: fixed 1280×800, controller support required alongside keyboard/mouse
+45
View File
@@ -0,0 +1,45 @@
# Monetization
## The Problem
- Selling for **$5** reduces hacking/smurfing (small barrier to entry)
- But limits long-term revenue after launch
---
## Monetization Options
### Option A — Buy to Play + Cosmetics (Recommended)
- **$5 upfront** on Steam (barrier against hackers/smurfs)
- Free cosmetic battle pass each season (optional paid tier)
- Cosmetic shop: ship skins, thruster trails, kill effects, UI themes
- No pay-to-win — all gameplay items are free
### Option B — Free to Play + Cosmetics
- Lower barrier to entry = more players = better matchmaking
- Revenue comes 100% from cosmetic shop and battle pass
- Risk: harder to combat hackers/smurfs without a price barrier
- Mitigation: phone verification or Steam account age requirement
### Option C — Buy to Play + Expansions
- $5 base game
- Future paid DLC: new races, new map packs
- Risk: splits playerbase
---
## Recommendation
**Option A** is the safest starting point:
- $5 keeps bad actors out
- Cosmetics fund ongoing development
- No pay-to-win preserves competitive integrity
- Can always go F2P later if playerbase needs a boost
---
## Anti-Cheat / Anti-Smurf Notes
- $5 price is a soft deterrent, not a hard one
- Consider VAC (Steam's anti-cheat) integration via Godot Steam plugin
- Phone verification for ranked queue is another option (like Valorant)
+59
View File
@@ -0,0 +1,59 @@
# Multiplayer — Maps & Game Modes
## Maps
- **10 maps total** at launch
- Design philosophy: **CSGO-tight** — intentional lanes, clear sightlines, no wasted space
- Subspace Continuum's Trench Wars maps were too large and too open — avoid this
- All maps are 2D top-down space environments (asteroid fields, space stations, nebulae, etc.)
| Map Size | Used For |
|----------|----------|
| Small | Ranked 5v5 |
| Medium | Ranked 5v5 / Casual 25v25 |
| Large | Casual 25v25 only |
---
## Ranked Mode — 5v5
Competitive, skill-based, MMR/ELO ladder. Penalty for leaving mid-match. Smallmedium maps only.
### Ranked Mode Ideas
| Mode | Description |
|------|-------------|
| **Flagship Assault** | Each team has a Flagship (large ship). Destroy the enemy Flagship while defending yours. *(Recommended starting mode)* |
| **Control Point** | One central point on the map. Hold it longer than the enemy to win. First to X seconds wins. |
| **Deathmatch** | First team to X kills wins. Clean, simple, easy to understand. |
| **Relay Capture** | A neutral carrier drifts across the map. Escort it to the enemy base to score. |
> **Suggestion:** Start with **Flagship Assault** only. It's unique, has natural tension (offense and defense), and maps can be designed around it. Add modes later as seasons.
---
## Casual Mode — 25v25
No penalty for leaving. Larger maps. Chaotic and fun — you can drop in mid-match.
### Casual Mode Ideas
| Mode | Description |
|------|-------------|
| **Sector Control** | Map divided into sectors. Hold more sectors at time limit. Ties to galactic war meta. |
| **King of the Hill** | One contested zone in the center. Hold it to rack up points. |
| **Annihilation** | Pure kills. Last team standing or first to X kills. Chaos mode. |
| **Base Assault** | One team attacks a fortified base, one defends. Roles swap each round. |
| **Convoy Escort** | One team escorts a slow-moving freighter across the map, other team destroys it. |
> **Suggestion:** Start with **Sector Control** for casual. It ties into the galaxy meta (Helldivers-style), encourages teamwork without hard requirements, and naturally scales to 25v25.
---
## Galaxy Meta (Helldivers-Style)
- The galaxy is divided into **sectors**
- Casual 25v25 matches determine which race controls which sector
- Ranked 5v5 results also contribute to sector control
- A live galaxy map on the main screen shows the current war state
- This gives low-stakes players (casual) real impact on the ongoing war
+64
View File
@@ -0,0 +1,64 @@
# Space Game — Project Overview
## 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.
## 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
4. Fight in a 25v25 battle
5. Return to main menu
## Game Modes
| Mode | Size | Status |
|------|------|--------|
| Casual | 25v25 (bots fill to 7v7 minimum) | In progress |
| Ranked | 5v5, competitive MMR | Disabled — coming later |
## Current Game Flow (as built)
```
Main Menu
└─ CASUAL ──► World loads immediately (game active)
└─ TeamSelect overlay appears
├─ Pick race (2 random of 3 offered)
└─ Pick ship (4 available, sprites shown)
└─ Player spawns centred, invincible briefly
└─ ESC / Start ──► Pause menu overlay
├─ RESUME
├─ SETTINGS (stub)
├─ SELECT TEAM (stub)
├─ QUIT TO MENU
└─ QUIT TO DESKTOP
```
## Races
3 playable races chosen from a pool of 10 concepts — see `racesclasses.md`. Each race has 5 ship classes:
| Class | Role |
|-------|------|
| Interceptor | Fast, agile generalist |
| Bomber | Area damage, slow |
| Support | Heal/rally; allies can attach |
| Stealth | Assassin, hit-and-run |
| Heavy | Tank, massive firepower |
Races are visually distinct — critical for reading a 25v25 battlefield at a glance.
## 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
- Bot fill ensures a minimum of **7v7** in casual; no bots added beyond that
## Design Philosophy
- Maps inspired by CSGO — tight, intentional, not the sprawling Subspace Trench Wars layout
- 10 maps at launch (small for ranked, medium/large for casual)
- All art: pixel art
- Platform primary: Steam Deck (1280×800, controller-first)
+84
View File
@@ -0,0 +1,84 @@
# Races & Ship Classes
## Chosen Races: 1, 4, 5
From the 10 concepts below, races **1 (Terran Republic)**, **4 (Mechanos Sovereignty)**, and **5 (Void Wraiths)** were selected.
> **Current in-game status:** The team selection screen uses temporary placeholder names ("Terran Federation", "The Collective", "Iron Order") with placeholder sprites until proper art and lore are integrated for the 3 chosen races.
---
## The 3 Chosen Races
### Race 1 — Terran Republic ✅ Chosen
**Weapon type:** Projectile / Ballistic
**Visual:** Angular gray-blue military ships, recognizable fighter jet silhouettes, clean livery
**Feel:** Standard military. Balanced and familiar. Easiest to learn.
| # | Ship | Role | Description |
|---|------|------|-------------|
| 1 | **Falcon** | Interceptor | Fast all-rounder. Standard guns, high agility. Best 1v1 duelist. |
| 2 | **Devastator** | Bomber | Launches grenades + missiles with large splash radius. Slow, massive damage. |
| 3 | **Sentinel** | Support | Deploys repair drones + turrets. Allies can attach. Mobile spawn point. |
| 4 | **Ranger** | Stealth | Cloaks, fires a single high-damage sniper shot, recloaks. |
| 5 | **Juggernaut** | Heavy | Armored fortress. Drops proximity mines, fires suppression bursts. Near-unkillable solo. |
---
### Race 4 — Mechanos Sovereignty ✅ Chosen
**Weapon type:** Railgun / Drone / EMP
**Visual:** Perfectly geometric chrome-and-gold ships. No organic curves. Modular, interchangeable parts. Clearly machine-made.
**Feel:** Pure precision AI. Railgun sniping, drone swarms, and tactical disruption.
| # | Ship | Role | Description |
|---|------|------|-------------|
| 1 | **Warframe** | Interceptor | Fires precise railgun bursts. Fast. Rewards aim skill. |
| 2 | **Fabricator** | Bomber | Deploys explosive drone clusters — set-and-forget area denial. |
| 3 | **Core Hub** | Support | Broadcasts repair nanobots to nearby allies. Creates mobile spawn beacon. |
| 4 | **Phantom Unit** | Stealth | Hacks enemy targeting — turns off their HUD radar briefly. Hard to detect. |
| 5 | **Siege Engine** | Heavy | Locks into stationary mode for massive railgun barrage. Vulnerable while moving. |
---
### Race 5 — Void Wraiths ✅ Chosen
**Weapon type:** Dark Energy / Gravity / Phase
**Visual:** Inky black ships with white particle trails. Look like holes in space. Shifting, unstable edges. Eerie.
**Feel:** Terrifying and unpredictable. Gravity wells, phase attacks, hard to pin down.
| # | Ship | Role | Description |
|---|------|------|-------------|
| 1 | **Shade** | Interceptor | Partially phases — hard to hit. Fires void energy bolts. |
| 2 | **Rift Caster** | Bomber | Opens gravity rifts that pull nearby enemies toward the center then collapse. |
| 3 | **Nexus Wraith** | Support | Creates phase portals allies can teleport through. Ally rally point. |
| 4 | **Specter** | Stealth | Fully phased and invisible. Only visible for 0.3s when firing. |
| 5 | **Void Titan** | Heavy | Absorbs incoming damage, then releases it all in a single omni-directional burst. |
---
## All 10 Race Concepts (Reference)
<details>
<summary>Click to expand all 10 races</summary>
### Race 2 — Xel'Nara Collective
**Weapon type:** Laser / Crystal Energy | **Feel:** Alien elegance, glass cannon
### Race 3 — Vorg Swarm
**Weapon type:** Bio / Organic (acid, spores) | **Feel:** Zergs in space, DOT chaos
### Race 6 — Solari Imperium
**Weapon type:** Plasma / Solar Fire | **Feel:** Ancient empire, slow and devastating
### Race 7 — Kryx Hivemind
**Weapon type:** Swarm Missiles / Acid | **Feel:** Insectoid swarm, volume of fire
### Race 8 — Nebulon Drifters
**Weapon type:** Gas Clouds / Toxin | **Feel:** Environmental control, zone denial
### Race 9 — Iron Covenant
**Weapon type:** EMP / Railgun / Hacking | **Feel:** Disruptor faction, breaks enemy systems
### Race 10 — Eldari Ascendancy
**Weapon type:** Psionic / Reality Distortion | **Feel:** Psychic manipulation, bullets that curve
</details>
+91
View File
@@ -0,0 +1,91 @@
# Project Structure
## File Tree
```
spacewar/ ← repo root
└── spacewar/ ← Godot project root (open this in Godot editor)
├── project.godot
├── game_config.gd ← autoload singleton (tuning values, player state, signals)
├── main_menu.tscn/gd ← entry point / main scene
├── server_browser.tscn/gd ← server browser UI (built, not in active flow yet)
├── world.tscn ← game world
├── node_2d.tscn ← player ship scene
├── ship_movement.gd ← player ship logic
├── bullet.tscn/gd ← projectile
├── pause_menu.tscn/gd ← in-game pause overlay (ESC / Start button)
├── team_select.tscn/gd ← race + ship selection overlay (shown on world load)
├── asteroid_movement.gd ← stub
└── images/
├── background/skybox/ ← 6 space background PNGs (1.png 6.png)
└── ships/Example_ships/ ← 4 placeholder ship sprites (1.png, 1B.png, 2a.png, 3b.png)
```
## Scene Graph
### `main_menu.tscn` — entry point
```
MainMenu (Control) ← main_menu.gd builds all UI in _ready()
```
### `world.tscn` — game world
```
World (Node2D)
├── TextureRect ← static space background (skybox/1.png)
├── Player ← instance of node_2d.tscn
├── HUD (CanvasLayer)
│ └── HealthLabel
├── PauseMenu (CanvasLayer, layer=10) ← pause_menu.tscn
└── TeamSelect (CanvasLayer, layer=20) ← team_select.tscn; queue_free()s after selection
```
### `node_2d.tscn` — player ship
```
Player (CharacterBody2D) ← ship_movement.gd
├── CollisionShape2D ← CircleShape2D
├── Sprite2D ← texture set at runtime from GameConfig.player_ship_path
└── VisibleOnScreenNotifier2D
```
## Input Map
| Action | Key | Controller |
|--------|-----|------------|
| `move_up` | W | — |
| `move_left` | A | — |
| `move_right` | D | — |
| `move_down` | S | — |
| `shoot` | Space | — |
| `toggle_pause` | Esc | Start / Options button (JoyButton 6) |
## Game Flow
```
main_menu.tscn
├── CASUAL → world.tscn
│ ├── TeamSelect overlay: pick race (2 random of 3 offered)
│ ├── TeamSelect overlay: pick ship
│ └── Player spawns → game live
└── RANKED → disabled (coming soon)
```
> **Note:** `server_browser.tscn` was built (Task 1) and is functional, but the current flow bypasses it — CASUAL goes directly to the world and TeamSelect handles name/race/ship. The server browser will be re-integrated when real multiplayer server listing is built.
## Key Autoload — `GameConfig`
| Property | Type | Set by |
|----------|------|--------|
| `player_name` | String | Main menu callsign input |
| `player_race` | int | TeamSelect race pick |
| `player_ship_path` | String | TeamSelect ship pick |
| `team_selected` | signal | Emitted by TeamSelect when done |
| `ship_thrust` | float | Tuning constant |
| `ship_max_speed` | float | Tuning constant |
| `ship_rotation_speed` | float | Tuning constant |
| `ship_fire_rate` | float | Tuning constant |
| `bullet_speed` | float | Tuning constant |
| `ship_max_health` | int | Tuning constant |
| `ship_respawn_delay` | float | Tuning constant |
| `ship_invincibility_time` | float | Tuning constant |
| `bullet_damage` | int | Tuning constant |
| `ship_wall_damage` | int | Tuning constant |
+29
View File
@@ -0,0 +1,29 @@
# Task Log
## Completed
| # | Task | Notes |
|---|------|-------|
| 0 | Format all design docs as Markdown | Done |
| 1 | Design 10 races, pick 3 | Races 1, 4, 5 chosen — see `racesclasses.md` |
| 2 | Ranked + casual mode ideas | See `multiplayer.md` |
| 3 | Multiplayer engineering doc | See `tech.md` |
| 4 | Server browser screen | Player name, race buttons, server list, JOIN — stores in `GameConfig` |
| 5 | Main menu | CASUAL / RANKED (disabled), callsign input, rank badge, quit button |
| 6 | In-game pause menu | ESC / controller Start; game runs behind it; working: RESUME, QUIT TO MENU, QUIT TO DESKTOP |
| 7 | Team & ship selection | On world load; 2 random races offered; ship grid with real sprites; player spawns after |
## Up Next
| # | Task | Priority |
|---|------|----------|
| 8 | Replace placeholder races with chosen 3 (Terran Republic, Mechanos Sovereignty, Void Wraiths) | High |
| 9 | Real ship sprites for all 3 races (5 ships each) | High |
| 10 | Asteroids + environment hazards | Medium |
| 11 | Bot fill for casual (min 7v7) | Medium — see `bots.md` |
| 12 | Sound effects (thrust, shoot, explosion, UI clicks) | Medium |
| 13 | Multiplayer networking — ENet authoritative server | High (big) |
| 14 | Galaxy war meta + sector control | Low (post-networking) |
| 15 | Ranked matchmaking + MMR | Low (post-networking) |
| 16 | Settings screen (audio, controls, display) | Low |
| 17 | Select Team in pause menu (live team swap) | Low |
+89
View File
@@ -0,0 +1,89 @@
# Tech Stack
## Engine & Language
- **Engine:** Godot 4.7 (Forward Plus renderer)
- **Language:** GDScript
- **Project path:** `/mnt/code/spacewar/spacewar/` (inner folder is the Godot project root)
- **Platform target:** Linux (Steam Deck native, 1280×800), Windows PC
- **Art style:** Pixel art (placeholder sprites in use currently)
See `structure.md` for the full file tree and scene graph.
---
## Multiplayer Architecture (Planned)
```
[Client] ──── [Matchmaking Server] ──── [Dedicated Game Server]
[Backend API]
(queue, ranks, accounts)
```
### Authoritative Server Model
- Dedicated game servers run the authoritative simulation (not player-hosted)
- Clients send **inputs** (movement, fire, ability) — server validates and broadcasts results
- No client-side cheating possible on critical state (position, HP, kills)
- Player position simulated server-side; clients predict locally and reconcile
### Godot Networking Layer
```gdscript
# Server: move a player
@rpc("authority", "call_local", "reliable")
func set_position(new_pos: Vector2) -> void:
position = new_pos
# Client: send input to server
@rpc("any_peer", "call_remote", "unreliable")
func send_input(dir: Vector2, firing: bool) -> void:
pass
```
- **Transport:** ENet (UDP, built into Godot) for low-latency game data
- **Reliable channel:** critical state (HP, death, respawn)
- **Unreliable channel:** high-frequency position/velocity updates
### Client-Side Prediction & Lag Compensation
1. Client predicts own movement locally (feels instant)
2. Server confirms or corrects (reconciliation)
3. Other players interpolated between last two known positions
4. Lag compensation: server rewinds state slightly to validate hitscan shots
### Matchmaking Backend
| Component | Tech |
|-----------|------|
| API Server | Go or Node.js |
| Queue / State | Redis |
| Database (accounts, ranks) | PostgreSQL |
| Hosting | VPS (Hetzner / DigitalOcean) or self-hosted |
**Flow:**
1. Client sends "find match" with MMR + mode
2. Backend queues player, finds lobby within MMR range
3. Lobby full → backend assigns a game server
4. Backend sends client the server IP + session token
5. Client connects directly to game server
### Steam Integration
- **GodotSteam** plugin (open source, wraps Steamworks SDK)
- Handles: Steam auth, VAC anti-cheat, Steam lobbies, achievements
- No separate account system needed at launch
---
## Networking Checklist
- [ ] Godot ENet server/client setup
- [ ] Player input RPC structure
- [ ] Position sync with interpolation
- [ ] Ship class registration per peer
- [ ] Server-authoritative health/death
- [ ] Matchmaking API (queue + lobby assignment)
- [ ] GodotSteam auth + VAC
- [ ] Lag compensation (basic rewind)
+4
View File
@@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8
+2
View File
@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
+3
View File
@@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/
+21
View File
@@ -0,0 +1,21 @@
extends Area2D
var velocity: Vector2 = Vector2.ZERO
var source: Node = null
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _process(delta: float) -> void:
global_position += velocity * delta
var screen = get_viewport_rect().size
if (global_position.x < 0 or global_position.x > screen.x or
global_position.y < 0 or global_position.y > screen.y):
queue_free()
func _on_body_entered(body: Node) -> void:
if body == source:
return
if body.has_method("take_damage"):
body.take_damage(GameConfig.bullet_damage)
queue_free()
+1
View File
@@ -0,0 +1 @@
uid://dho8dpmkj8bdu
+16
View File
@@ -0,0 +1,16 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://bullet.gd" id="1"]
[sub_resource type="CircleShape2D" id="CircleShape2D_1"]
radius = 4.0
[node name="Bullet" type="Area2D"]
script = ExtResource("1")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dglp131j5tga6"
path="res://.godot/imported/bwQ7rQ.png-13ccf92d28893d68bca8cee95abb7d20.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://bwQ7rQ.png"
dest_files=["res://.godot/imported/bwQ7rQ.png-13ccf92d28893d68bca8cee95abb7d20.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
+24
View File
@@ -0,0 +1,24 @@
extends Node
signal team_selected
# Ship movement
var ship_thrust: float = 250.0
var ship_max_speed: float = 300.0
var ship_rotation_speed: float = 4.0
# Shooting
var ship_fire_rate: float = 0.15
var bullet_speed: float = 450.0
# Health & respawn
var ship_max_health: int = 100
var ship_respawn_delay: float = 3.0
var ship_invincibility_time: float = 2.0
var ship_wall_damage: int = 1
var bullet_damage: int = 40
# Set before entering a game
var player_name: String = ""
var player_race: int = 0
var player_ship_path: String = ""
+1
View File
@@ -0,0 +1 @@
uid://cdh612na2digh
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 995 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b527hu5te2484"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d0i6jwqh131b1"
path="res://.godot/imported/1.png-97f1111764d7d26127f844f9bb99447f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/background/skybox/1.png"
dest_files=["res://.godot/imported/1.png-97f1111764d7d26127f844f9bb99447f.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: 326 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://uchnddsvimku"
path="res://.godot/imported/2.png-535b6f5f8c8d79f538e82478a5ae919b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/background/skybox/2.png"
dest_files=["res://.godot/imported/2.png-535b6f5f8c8d79f538e82478a5ae919b.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: 299 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://tcs117dqynbv"
path="res://.godot/imported/3.png-1ad36fe04660abf5feafa71bee86d926.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/background/skybox/3.png"
dest_files=["res://.godot/imported/3.png-1ad36fe04660abf5feafa71bee86d926.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: 508 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://65mjcpw5rk06"
path="res://.godot/imported/4.png-f0201325d495db0494d2b1f7bbeec9a9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/background/skybox/4.png"
dest_files=["res://.godot/imported/4.png-f0201325d495db0494d2b1f7bbeec9a9.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: 507 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bc1onqc7d21w7"
path="res://.godot/imported/5.png-08cd6a64e2f902af1b1785a272d9c50f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/background/skybox/5.png"
dest_files=["res://.godot/imported/5.png-08cd6a64e2f902af1b1785a272d9c50f.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: 636 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cvyfcu03jdqas"
path="res://.godot/imported/6.png-21d9e30a24ce4c43ebcbe0ea36daa029.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/background/skybox/6.png"
dest_files=["res://.godot/imported/6.png-21d9e30a24ce4c43ebcbe0ea36daa029.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
@@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar
14 rue de Plaisance, 75014 Paris, France
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ded6ek6l7n8gb"
path="res://.godot/imported/explosion1.png-ebaaa46c76a7bf39885ed36c21d9704d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/effects/explosion1.png"
dest_files=["res://.godot/imported/explosion1.png-ebaaa46c76a7bf39885ed36c21d9704d.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: 1.9 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://td2tbmkm3r6u"
path="res://.godot/imported/1.png-6cd3243048854fb354d682a18817356e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/ships/Example_ships/1.png"
dest_files=["res://.godot/imported/1.png-6cd3243048854fb354d682a18817356e.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: 15 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cgfd5fboa45dn"
path="res://.godot/imported/1B.png-991b0890a43069731cad413022099b02.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/ships/Example_ships/1B.png"
dest_files=["res://.godot/imported/1B.png-991b0890a43069731cad413022099b02.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: 1.7 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://wnrdhn33ce50"
path="res://.godot/imported/2a.png-9bfe77c7bec5a2e8a116acfd3eb6380a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/ships/Example_ships/2a.png"
dest_files=["res://.godot/imported/2a.png-9bfe77c7bec5a2e8a116acfd3eb6380a.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: 1.6 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b6xkpyy1rg74v"
path="res://.godot/imported/3b.png-d95c52be2bb572cb2c8ffd7d62d1118b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://images/ships/Example_ships/3b.png"
dest_files=["res://.godot/imported/3b.png-d95c52be2bb572cb2c8ffd7d62d1118b.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
+338
View File
@@ -0,0 +1,338 @@
extends Control
var _name_input: LineEdit
var _casual_btn: Button
var _ranked_btn: Button
const RANK_DATA := [
{"label": "CADET", "color": Color(0.72, 0.48, 0.22)},
{"label": "PILOT", "color": Color(0.78, 0.78, 0.82)},
{"label": "ACE", "color": Color(1.0, 0.82, 0.22)},
{"label": "COMMANDER", "color": Color(0.35, 0.88, 1.0)},
{"label": "ADMIRAL", "color": Color(0.62, 0.28, 1.0)},
{"label": "LEGEND", "color": Color(1.0, 0.28, 0.10)},
]
const CURRENT_RANK := 0 # placeholder until backend exists
func _ready() -> void:
GameConfig.player_race = 0
GameConfig.player_ship_path = ""
_build_ui()
func _build_ui() -> void:
_add_bg()
_add_title()
_add_play_section()
_add_rank_badge()
_add_quit_btn()
_add_version_label()
func _add_bg() -> void:
var bg := TextureRect.new()
bg.texture = load("res://images/background/skybox/2.png")
bg.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
bg.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(bg)
var ov := ColorRect.new()
ov.color = Color(0.01, 0.02, 0.09, 0.80)
ov.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(ov)
func _add_title() -> void:
var title := Label.new()
title.text = "S P A C E W A R"
title.set_anchors_preset(Control.PRESET_TOP_WIDE)
title.offset_top = 72.0
title.offset_bottom = 148.0
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 56)
title.add_theme_color_override("font_color", Color(0.45, 0.88, 1.0))
add_child(title)
var sub := Label.new()
sub.text = "FIGHT FOR THE GALAXY"
sub.set_anchors_preset(Control.PRESET_TOP_WIDE)
sub.offset_top = 152.0
sub.offset_bottom = 178.0
sub.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
sub.add_theme_font_size_override("font_size", 13)
sub.add_theme_color_override("font_color", Color(0.42, 0.54, 0.70))
add_child(sub)
func _add_play_section() -> void:
const BTN_W := 480.0
const BTN_H := 230.0
const GAP := 40.0
const LEFT_X := (1280.0 - (BTN_W * 2.0 + GAP)) / 2.0 # 140
const TOP_Y := 220.0
_casual_btn = _make_mode_btn(
LEFT_X, TOP_Y, BTN_W, BTN_H,
"CASUAL",
"25 vs 25",
"Drop in, drop out anytime.\nNo rank at stake.",
Color(0.06, 0.22, 0.08),
Color(0.30, 0.90, 0.40),
)
_casual_btn.pressed.connect(_on_play_casual)
add_child(_casual_btn)
_ranked_btn = _make_mode_btn(
LEFT_X + BTN_W + GAP, TOP_Y, BTN_W, BTN_H,
"RANKED",
"5 vs 5",
"Competitive ladder.\nYour rank is on the line.",
Color(0.06, 0.10, 0.26),
Color(0.40, 0.72, 1.00),
)
_ranked_btn.disabled = true
_ranked_btn.pressed.connect(_on_play_ranked)
add_child(_ranked_btn)
var coming_soon := Label.new()
coming_soon.text = "— COMING SOON —"
coming_soon.set_anchor_and_offset(SIDE_LEFT, 0.0, LEFT_X + BTN_W + GAP)
coming_soon.set_anchor_and_offset(SIDE_TOP, 0.0, TOP_Y + BTN_H - 36.0)
coming_soon.set_anchor_and_offset(SIDE_RIGHT, 0.0, LEFT_X + BTN_W * 2.0 + GAP)
coming_soon.set_anchor_and_offset(SIDE_BOTTOM, 0.0, TOP_Y + BTN_H - 8.0)
coming_soon.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
coming_soon.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
coming_soon.add_theme_font_size_override("font_size", 11)
coming_soon.add_theme_color_override("font_color", Color(0.30, 0.36, 0.48, 0.70))
coming_soon.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(coming_soon)
# Callsign label
var lbl := Label.new()
lbl.text = "CALLSIGN"
lbl.set_anchor_and_offset(SIDE_LEFT, 0.0, LEFT_X)
lbl.set_anchor_and_offset(SIDE_TOP, 0.0, TOP_Y + BTN_H + 26.0)
lbl.set_anchor_and_offset(SIDE_RIGHT, 0.0, LEFT_X + 95.0)
lbl.set_anchor_and_offset(SIDE_BOTTOM, 0.0, TOP_Y + BTN_H + 62.0)
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
lbl.add_theme_font_size_override("font_size", 12)
lbl.add_theme_color_override("font_color", Color(0.45, 0.55, 0.70))
add_child(lbl)
_name_input = LineEdit.new()
_name_input.placeholder_text = "Enter your callsign..."
_name_input.set_anchor_and_offset(SIDE_LEFT, 0.0, LEFT_X + 100.0)
_name_input.set_anchor_and_offset(SIDE_TOP, 0.0, TOP_Y + BTN_H + 26.0)
_name_input.set_anchor_and_offset(SIDE_RIGHT, 0.0, LEFT_X + BTN_W * 2.0 + GAP)
_name_input.set_anchor_and_offset(SIDE_BOTTOM, 0.0, TOP_Y + BTN_H + 62.0)
_name_input.add_theme_font_size_override("font_size", 15)
if GameConfig.player_name != "":
_name_input.text = GameConfig.player_name
_name_input.text_changed.connect(_on_name_changed)
add_child(_name_input)
_update_play_buttons()
func _make_mode_btn(
x: float, y: float, w: float, h: float,
mode: String, fmt: String, desc: String,
bg: Color, accent: Color) -> Button:
var btn := Button.new()
btn.text = ""
btn.set_anchor_and_offset(SIDE_LEFT, 0.0, x)
btn.set_anchor_and_offset(SIDE_TOP, 0.0, y)
btn.set_anchor_and_offset(SIDE_RIGHT, 0.0, x + w)
btn.set_anchor_and_offset(SIDE_BOTTOM, 0.0, y + h)
var sn := _flat(bg.lightened(0.05), 12)
sn.set_border_width_all(2)
sn.border_color = accent.darkened(0.45)
var sh := _flat(bg.lightened(0.18), 12)
sh.set_border_width_all(2)
sh.border_color = accent
var sd := _flat(Color(0.04, 0.04, 0.06, 0.5), 12)
sd.set_border_width_all(1)
sd.border_color = Color(0.2, 0.22, 0.28, 0.5)
btn.add_theme_stylebox_override("normal", sn)
btn.add_theme_stylebox_override("hover", sh)
btn.add_theme_stylebox_override("pressed", sh)
btn.add_theme_stylebox_override("disabled", sd)
var vb := VBoxContainer.new()
vb.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
vb.offset_left = 24.0
vb.offset_top = 20.0
vb.offset_right = -24.0
vb.offset_bottom = -20.0
vb.alignment = BoxContainer.ALIGNMENT_CENTER
vb.add_theme_constant_override("separation", 12)
vb.mouse_filter = Control.MOUSE_FILTER_IGNORE
btn.add_child(vb)
var mode_lbl := Label.new()
mode_lbl.text = mode
mode_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
mode_lbl.add_theme_font_size_override("font_size", 40)
mode_lbl.add_theme_color_override("font_color", accent)
mode_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(mode_lbl)
var fmt_lbl := Label.new()
fmt_lbl.text = fmt
fmt_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
fmt_lbl.add_theme_font_size_override("font_size", 22)
fmt_lbl.add_theme_color_override("font_color", Color(0.85, 0.90, 0.96))
fmt_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(fmt_lbl)
var sep := HSeparator.new()
sep.add_theme_color_override("color", accent.darkened(0.5))
sep.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(sep)
var desc_lbl := Label.new()
desc_lbl.text = desc
desc_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
desc_lbl.add_theme_font_size_override("font_size", 13)
desc_lbl.add_theme_color_override("font_color", Color(0.55, 0.63, 0.76))
desc_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD
desc_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(desc_lbl)
return btn
func _add_rank_badge() -> void:
const W := 210.0
const H := 60.0
const MARGIN := 16.0
var panel := Panel.new()
panel.set_anchors_preset(Control.PRESET_TOP_RIGHT)
panel.offset_left = -(W + MARGIN)
panel.offset_top = MARGIN
panel.offset_right = -MARGIN
panel.offset_bottom = MARGIN + H
var ps := _flat(Color(0.04, 0.07, 0.16, 0.92), 8)
ps.set_border_width_all(1)
ps.border_color = Color(0.18, 0.36, 0.62, 0.85)
panel.add_theme_stylebox_override("panel", ps)
add_child(panel)
var rank := RANK_DATA[CURRENT_RANK]
# Colored badge block on the left
var badge := Panel.new()
badge.set_anchor_and_offset(SIDE_LEFT, 0.0, 8.0)
badge.set_anchor_and_offset(SIDE_TOP, 0.0, 8.0)
badge.set_anchor_and_offset(SIDE_RIGHT, 0.0, 50.0)
badge.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -8.0)
var bs := _flat(rank.color.darkened(0.45), 5)
bs.set_border_width_all(2)
bs.border_color = rank.color
badge.add_theme_stylebox_override("panel", bs)
panel.add_child(badge)
var init_lbl := Label.new()
init_lbl.text = rank.label.left(1)
init_lbl.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
init_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
init_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
init_lbl.add_theme_font_size_override("font_size", 20)
init_lbl.add_theme_color_override("font_color", rank.color)
badge.add_child(init_lbl)
# Rank name
var rank_lbl := Label.new()
rank_lbl.text = rank.label
rank_lbl.set_anchor_and_offset(SIDE_LEFT, 0.0, 58.0)
rank_lbl.set_anchor_and_offset(SIDE_TOP, 0.0, 0.0)
rank_lbl.set_anchor_and_offset(SIDE_RIGHT, 1.0, -8.0)
rank_lbl.set_anchor_and_offset(SIDE_BOTTOM, 0.0, H / 2.0 + 2.0)
rank_lbl.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
rank_lbl.add_theme_font_size_override("font_size", 15)
rank_lbl.add_theme_color_override("font_color", rank.color)
panel.add_child(rank_lbl)
var rating_lbl := Label.new()
rating_lbl.text = "Rating: —"
rating_lbl.set_anchor_and_offset(SIDE_LEFT, 0.0, 58.0)
rating_lbl.set_anchor_and_offset(SIDE_TOP, 0.0, H / 2.0)
rating_lbl.set_anchor_and_offset(SIDE_RIGHT, 1.0, -8.0)
rating_lbl.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -6.0)
rating_lbl.vertical_alignment = VERTICAL_ALIGNMENT_TOP
rating_lbl.add_theme_font_size_override("font_size", 11)
rating_lbl.add_theme_color_override("font_color", Color(0.45, 0.54, 0.68))
panel.add_child(rating_lbl)
func _add_quit_btn() -> void:
const W := 110.0
const H := 38.0
const MARGIN := 20.0
var btn := Button.new()
btn.text = "QUIT"
btn.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
btn.offset_left = -(W + MARGIN)
btn.offset_top = -(H + MARGIN)
btn.offset_right = -MARGIN
btn.offset_bottom = -MARGIN
btn.add_theme_font_size_override("font_size", 13)
btn.add_theme_color_override("font_color", Color(0.48, 0.54, 0.65))
var sn := _flat(Color(0.05, 0.06, 0.11, 0.80), 6)
sn.set_border_width_all(1)
sn.border_color = Color(0.18, 0.22, 0.33, 0.70)
var sh := _flat(Color(0.10, 0.12, 0.20, 0.90), 6)
sh.set_border_width_all(1)
sh.border_color = Color(0.32, 0.38, 0.52)
btn.add_theme_stylebox_override("normal", sn)
btn.add_theme_stylebox_override("hover", sh)
btn.pressed.connect(func(): get_tree().quit())
add_child(btn)
func _add_version_label() -> void:
var lbl := Label.new()
lbl.text = "v0.1-dev"
lbl.set_anchor_and_offset(SIDE_LEFT, 0.0, 14.0)
lbl.set_anchor_and_offset(SIDE_TOP, 1.0, -30.0)
lbl.set_anchor_and_offset(SIDE_RIGHT, 0.0, 100.0)
lbl.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -10.0)
lbl.add_theme_font_size_override("font_size", 11)
lbl.add_theme_color_override("font_color", Color(0.3, 0.35, 0.45))
add_child(lbl)
func _flat(col: Color, radius: int = 0) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = col
s.set_corner_radius_all(radius)
return s
func _on_name_changed(_text: String) -> void:
_update_play_buttons()
func _update_play_buttons() -> void:
var ok := not _name_input.text.strip_edges().is_empty()
_casual_btn.disabled = not ok
_ranked_btn.disabled = true
func _on_play_casual() -> void:
GameConfig.player_name = _name_input.text.strip_edges()
get_tree().change_scene_to_file("res://world.tscn")
func _on_play_ranked() -> void:
GameConfig.player_name = _name_input.text.strip_edges()
GameConfig.player_race = 1 # TODO: race selection screen
get_tree().change_scene_to_file("res://world.tscn")
+1
View File
@@ -0,0 +1 @@
uid://3it6fh07tirl
+8
View File
@@ -0,0 +1,8 @@
[gd_scene format=3 uid="uid://bmainmenu001"]
[ext_resource type="Script" path="res://main_menu.gd" id="1_script"]
[node name="MainMenu" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_script")
+17
View File
@@ -0,0 +1,17 @@
[gd_scene format=3 uid="uid://dygtdlkvo6ofl"]
[ext_resource type="Script" uid="uid://c4kj3i73wjdcj" path="res://ship_movement.gd" id="1_0e48y"]
[ext_resource type="Texture2D" uid="uid://td2tbmkm3r6u" path="res://images/ships/Example_ships/1.png" id="2_epypp"]
[sub_resource type="CircleShape2D" id="CircleShape2D_2uxl3"]
[node name="Player" type="CharacterBody2D" unique_id=1973248732]
script = ExtResource("1_0e48y")
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=188418093]
shape = SubResource("CircleShape2D_2uxl3")
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=2083904209]
texture = ExtResource("2_epypp")
[node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="." unique_id=1504818491]
+124
View File
@@ -0,0 +1,124 @@
extends CanvasLayer
var _root: Control
func _ready() -> void:
layer = 10
_build_ui()
_root.visible = false
func _input(event: InputEvent) -> void:
if event.is_action_pressed("toggle_pause"):
_toggle()
get_viewport().set_input_as_handled()
func _toggle() -> void:
_root.visible = not _root.visible
func _build_ui() -> void:
_root = Control.new()
_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(_root)
# Dim overlay — blocks mouse clicks reaching the game
var overlay := ColorRect.new()
overlay.color = Color(0.0, 0.0, 0.0, 0.58)
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
_root.add_child(overlay)
# Center panel (360 × 490)
var panel := Panel.new()
panel.set_anchors_preset(Control.PRESET_CENTER)
panel.offset_left = -180
panel.offset_top = -245
panel.offset_right = 180
panel.offset_bottom = 245
var ps := StyleBoxFlat.new()
ps.bg_color = Color(0.04, 0.07, 0.16, 0.97)
ps.set_corner_radius_all(10)
ps.set_border_width_all(1)
ps.border_color = Color(0.18, 0.38, 0.65, 0.9)
panel.add_theme_stylebox_override("panel", ps)
_root.add_child(panel)
var vb := VBoxContainer.new()
vb.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
vb.offset_left = 28
vb.offset_top = 28
vb.offset_right = -28
vb.offset_bottom = -28
vb.add_theme_constant_override("separation", 10)
panel.add_child(vb)
var title := Label.new()
title.text = "MENU"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 26)
title.add_theme_color_override("font_color", Color(0.4, 0.85, 1.0))
vb.add_child(title)
vb.add_child(_hsep())
var gap := Control.new()
gap.custom_minimum_size = Vector2(0, 4)
vb.add_child(gap)
_add_btn(vb, "RESUME", true).pressed.connect(_toggle)
_add_btn(vb, "SETTINGS", false)
_add_btn(vb, "SELECT TEAM", false)
vb.add_child(_hsep())
_add_btn(vb, "QUIT TO MENU", true).pressed.connect(_on_quit_to_menu)
_add_btn(vb, "QUIT TO DESKTOP", true).pressed.connect(_on_quit_to_desktop)
func _add_btn(parent: VBoxContainer, label_text: String, active: bool) -> Button:
var btn := Button.new()
btn.text = label_text
btn.custom_minimum_size = Vector2(0, 54)
btn.add_theme_font_size_override("font_size", 16)
if active:
btn.add_theme_color_override("font_color", Color(0.92, 0.95, 1.0))
var sn := _flat(Color(0.07, 0.13, 0.25, 1.0), 6)
var sh := _flat(Color(0.13, 0.24, 0.42, 1.0), 6)
sh.set_border_width_all(1)
sh.border_color = Color(0.3, 0.65, 1.0)
btn.add_theme_stylebox_override("normal", sn)
btn.add_theme_stylebox_override("hover", sh)
else:
btn.add_theme_color_override("font_color", Color(0.35, 0.40, 0.50))
var sn := _flat(Color(0.04, 0.06, 0.10, 1.0), 6)
btn.add_theme_stylebox_override("normal", sn)
btn.add_theme_stylebox_override("hover", sn)
parent.add_child(btn)
return btn
func _hsep() -> HSeparator:
var sep := HSeparator.new()
sep.add_theme_color_override("color", Color(0.18, 0.28, 0.45, 0.7))
return sep
func _flat(col: Color, radius: int = 0) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = col
s.set_corner_radius_all(radius)
return s
func _on_quit_to_menu() -> void:
get_tree().change_scene_to_file("res://main_menu.tscn")
func _on_quit_to_desktop() -> void:
get_tree().quit()
+1
View File
@@ -0,0 +1 @@
uid://lqjtib7jlqg0
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://cpausemenu01a"]
[ext_resource type="Script" path="res://pause_menu.gd" id="1_script"]
[node name="PauseMenu" type="CanvasLayer"]
script = ExtResource("1_script")
+68
View File
@@ -0,0 +1,68 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="spacewar"
run/main_scene="res://main_menu.tscn"
config/features=PackedStringArray("4.7", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
GameConfig="*res://game_config.gd"
[display]
window/size/viewport_width=1280
window/size/viewport_height=800
window/size/resizable=false
[input]
move_up={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
]
}
move_left={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
]
}
move_right={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
]
}
move_down={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
]
}
shoot={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
]
}
toggle_pause={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":27,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":6,"pressure":0.0,"pressed":false,"script":null)
]
}
[physics]
3d/physics_engine="Jolt Physics"
[rendering]
rendering_device/driver.windows="d3d12"
+283
View File
@@ -0,0 +1,283 @@
extends Control
var _selected_race: int = 0
var _selected_server: String = ""
var _name_input: LineEdit
var _join_button: Button
var _race_buttons: Array[Button] = []
var _server_btn: Button
func _ready() -> void:
_build_ui()
func _build_ui() -> void:
var bg := TextureRect.new()
bg.texture = load("res://images/background/skybox/1.png")
bg.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
bg.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(bg)
var overlay := ColorRect.new()
overlay.color = Color(0.02, 0.03, 0.1, 0.72)
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(overlay)
var header := _make_panel(0, 0, 1280, 60)
add_child(header)
var title_lbl := Label.new()
title_lbl.text = "S P A C E W A R — SERVER BROWSER"
title_lbl.anchor_left = 0.0
title_lbl.anchor_top = 0.0
title_lbl.anchor_right = 1.0
title_lbl.anchor_bottom = 1.0
title_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_lbl.add_theme_font_size_override("font_size", 26)
title_lbl.add_theme_color_override("font_color", Color(0.4, 0.85, 1.0))
header.add_child(title_lbl)
var left := _make_panel(10, 70, 745, 720)
add_child(left)
_build_server_list(left)
var right := _make_panel(765, 70, 505, 720)
add_child(right)
_build_player_setup(right)
func _make_panel(x: float, y: float, w: float, h: float) -> Panel:
var p := Panel.new()
p.anchor_left = 0.0
p.anchor_top = 0.0
p.anchor_right = 0.0
p.anchor_bottom = 0.0
p.offset_left = x
p.offset_top = y
p.offset_right = x + w
p.offset_bottom = y + h
var style := StyleBoxFlat.new()
style.bg_color = Color(0.04, 0.07, 0.16, 0.92)
style.set_corner_radius_all(8)
style.set_border_width_all(1)
style.border_color = Color(0.18, 0.38, 0.65, 0.9)
p.add_theme_stylebox_override("panel", style)
return p
func _vbox_in(parent: Control, margin: float = 16.0) -> VBoxContainer:
var vb := VBoxContainer.new()
vb.anchor_left = 0.0
vb.anchor_top = 0.0
vb.anchor_right = 1.0
vb.anchor_bottom = 1.0
vb.offset_left = margin
vb.offset_top = margin
vb.offset_right = -margin
vb.offset_bottom = -margin
vb.add_theme_constant_override("separation", 10)
parent.add_child(vb)
return vb
func _section_label(txt: String) -> Label:
var lbl := Label.new()
lbl.text = txt
lbl.add_theme_font_size_override("font_size", 18)
lbl.add_theme_color_override("font_color", Color(0.55, 0.82, 1.0))
return lbl
func _field_label(txt: String) -> Label:
var lbl := Label.new()
lbl.text = txt
lbl.add_theme_font_size_override("font_size", 14)
lbl.add_theme_color_override("font_color", Color(0.65, 0.72, 0.82))
return lbl
func _flat_box(color: Color, radius: int = 0) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = color
s.set_corner_radius_all(radius)
return s
func _build_server_list(parent: Panel) -> void:
var vb := _vbox_in(parent)
vb.add_child(_section_label("Available Servers"))
vb.add_child(HSeparator.new())
var col_hdr := HBoxContainer.new()
vb.add_child(col_hdr)
var h_name := _field_label("Server Name")
h_name.size_flags_horizontal = Control.SIZE_EXPAND_FILL
col_hdr.add_child(h_name)
var h_players := _field_label("Players")
h_players.custom_minimum_size = Vector2(110, 0)
h_players.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
col_hdr.add_child(h_players)
vb.add_child(HSeparator.new())
_server_btn = Button.new()
_server_btn.toggle_mode = true
_server_btn.text = " Trench Wars"
_server_btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
_server_btn.custom_minimum_size = Vector2(0, 54)
_server_btn.add_theme_font_size_override("font_size", 17)
_server_btn.add_theme_color_override("font_color", Color(0.92, 0.95, 1.0))
var sn := _flat_box(Color(0.07, 0.13, 0.25, 1.0), 4)
sn.content_margin_left = 12.0
var sh := _flat_box(Color(0.11, 0.21, 0.4, 1.0), 4)
sh.content_margin_left = 12.0
var sp := _flat_box(Color(0.09, 0.27, 0.52, 1.0), 4)
sp.set_border_width_all(2)
sp.border_color = Color(0.3, 0.65, 1.0, 1.0)
sp.content_margin_left = 12.0
_server_btn.add_theme_stylebox_override("normal", sn)
_server_btn.add_theme_stylebox_override("hover", sh)
_server_btn.add_theme_stylebox_override("pressed", sp)
_server_btn.pressed.connect(_on_server_selected.bind("trench_wars"))
vb.add_child(_server_btn)
# Player count label floated to the right of the server button row
# (separate non-interactive label overlaid via HBox trick)
var row_hb := HBoxContainer.new()
# We already added the button directly to vb; restructure: remove and re-add inside HBox
vb.remove_child(_server_btn)
vb.add_child(row_hb)
row_hb.add_child(_server_btn)
_server_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var pc_lbl := Label.new()
pc_lbl.text = "0 / 32"
pc_lbl.custom_minimum_size = Vector2(80, 0)
pc_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
pc_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
pc_lbl.add_theme_font_size_override("font_size", 14)
pc_lbl.add_theme_color_override("font_color", Color(0.5, 0.65, 0.8))
row_hb.add_child(pc_lbl)
func _build_player_setup(parent: Panel) -> void:
var vb := _vbox_in(parent, 20.0)
vb.add_theme_constant_override("separation", 12)
vb.add_child(_section_label("Player Setup"))
vb.add_child(HSeparator.new())
vb.add_child(_field_label("Player Name:"))
_name_input = LineEdit.new()
_name_input.placeholder_text = "Enter your callsign..."
_name_input.custom_minimum_size = Vector2(0, 44)
_name_input.add_theme_font_size_override("font_size", 16)
_name_input.text_changed.connect(_on_name_changed)
vb.add_child(_name_input)
var gap := Control.new()
gap.custom_minimum_size = Vector2(0, 6)
vb.add_child(gap)
vb.add_child(_field_label("Select Race:"))
var race_hb := HBoxContainer.new()
race_hb.custom_minimum_size = Vector2(0, 58)
race_hb.add_theme_constant_override("separation", 10)
vb.add_child(race_hb)
var race_cfg: Array = [
["Race 1", Color(0.25, 0.5, 1.0)],
["Race 2", Color(1.0, 0.45, 0.2)],
["Race 3", Color(0.25, 0.9, 0.4)],
]
for i in 3:
var btn := _make_race_button(race_cfg[i][0], race_cfg[i][1])
btn.pressed.connect(_on_race_selected.bind(i + 1))
race_hb.add_child(btn)
_race_buttons.append(btn)
var filler := Control.new()
filler.size_flags_vertical = Control.SIZE_EXPAND_FILL
vb.add_child(filler)
_join_button = Button.new()
_join_button.text = "JOIN GAME"
_join_button.custom_minimum_size = Vector2(0, 64)
_join_button.disabled = true
_join_button.add_theme_font_size_override("font_size", 22)
_join_button.add_theme_color_override("font_color", Color(0.35, 1.0, 0.45))
_join_button.add_theme_color_override("font_color_disabled", Color(0.3, 0.35, 0.3, 0.6))
var jn := _flat_box(Color(0.07, 0.28, 0.1, 1.0), 7)
var jh := _flat_box(Color(0.1, 0.42, 0.14, 1.0), 7)
var jd := _flat_box(Color(0.06, 0.09, 0.07, 0.5), 7)
_join_button.add_theme_stylebox_override("normal", jn)
_join_button.add_theme_stylebox_override("hover", jh)
_join_button.add_theme_stylebox_override("disabled", jd)
_join_button.pressed.connect(_on_join_pressed)
vb.add_child(_join_button)
func _make_race_button(label_text: String, col: Color) -> Button:
var btn := Button.new()
btn.text = label_text
btn.toggle_mode = true
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
btn.custom_minimum_size = Vector2(0, 58)
btn.add_theme_font_size_override("font_size", 15)
btn.add_theme_color_override("font_color", Color.WHITE)
var dark := Color(col.r * 0.22, col.g * 0.22, col.b * 0.22, 1.0)
var mid := Color(col.r * 0.38, col.g * 0.38, col.b * 0.38, 1.0)
var lite := Color(col.r * 0.5, col.g * 0.5, col.b * 0.5, 1.0)
var s_n := _flat_box(dark, 6)
var s_h := _flat_box(mid, 6)
var s_p := _flat_box(lite, 6)
s_p.set_border_width_all(2)
s_p.border_color = col
btn.add_theme_stylebox_override("normal", s_n)
btn.add_theme_stylebox_override("hover", s_h)
btn.add_theme_stylebox_override("pressed", s_p)
return btn
func _on_name_changed(_text: String) -> void:
_update_join()
func _on_race_selected(race: int) -> void:
_selected_race = race
for i in _race_buttons.size():
_race_buttons[i].button_pressed = (i + 1 == race)
_update_join()
func _on_server_selected(server: String) -> void:
_selected_server = server
_server_btn.button_pressed = true
_update_join()
func _update_join() -> void:
_join_button.disabled = (
_name_input.text.strip_edges().is_empty()
or _selected_race == 0
or _selected_server.is_empty()
)
func _on_join_pressed() -> void:
GameConfig.player_name = _name_input.text.strip_edges()
GameConfig.player_race = _selected_race
get_tree().change_scene_to_file("res://world.tscn")
+1
View File
@@ -0,0 +1 @@
uid://dg0sojl1wf8dh
+8
View File
@@ -0,0 +1,8 @@
[gd_scene format=3 uid="uid://doolmlvruun6o"]
[ext_resource type="Script" path="res://server_browser.gd" id="1_script"]
[node name="ServerBrowser" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_script")
+110
View File
@@ -0,0 +1,110 @@
extends CharacterBody2D
const BULLET_SCENE = preload("res://bullet.tscn")
var _fire_cooldown: float = 0.0
var health: int = 0
var _dead: bool = false
var _invincible: bool = false
var _invincible_timer: float = 0.0
func _ready() -> void:
if GameConfig.player_ship_path.is_empty():
visible = false
set_physics_process(false)
GameConfig.team_selected.connect(_on_team_selected)
else:
_init_player()
func _on_team_selected() -> void:
_init_player()
func _init_player() -> void:
var sprite := get_node_or_null("Sprite2D")
if sprite and not GameConfig.player_ship_path.is_empty():
sprite.texture = load(GameConfig.player_ship_path)
set_physics_process(true)
_respawn()
func _physics_process(delta: float) -> void:
if _dead:
return
# 1. INVINCIBILITY BLINK
if _invincible:
_invincible_timer -= delta
modulate.a = 0.3 if int(_invincible_timer * 8) % 2 == 0 else 1.0
if _invincible_timer <= 0.0:
_invincible = false
modulate.a = 1.0
# 2. ROTATION
if Input.is_action_pressed("move_right"):
rotation += GameConfig.ship_rotation_speed * delta
if Input.is_action_pressed("move_left"):
rotation -= GameConfig.ship_rotation_speed * delta
# 3. THRUST
if Input.is_action_pressed("move_up"):
velocity += Vector2.UP.rotated(rotation) * GameConfig.ship_thrust * delta
if Input.is_action_pressed("move_down"):
velocity += Vector2.DOWN.rotated(rotation) * GameConfig.ship_thrust * delta
velocity = velocity.limit_length(GameConfig.ship_max_speed)
move_and_slide()
# 4. SHOOT
_fire_cooldown -= delta
if Input.is_action_pressed("shoot") and _fire_cooldown <= 0.0:
_fire_cooldown = GameConfig.ship_fire_rate
var bullet = BULLET_SCENE.instantiate()
bullet.global_position = global_position + Vector2.UP.rotated(rotation) * 40.0
bullet.velocity = Vector2.UP.rotated(rotation) * GameConfig.bullet_speed + velocity
bullet.source = self
get_parent().add_child(bullet)
# 5. WALL COLLISION
var screen = get_viewport_rect().size
if global_position.x < 0 or global_position.x > screen.x:
velocity.x = 0
global_position.x = clamp(global_position.x, 0, screen.x)
take_damage(GameConfig.ship_wall_damage)
if global_position.y < 0 or global_position.y > screen.y:
velocity.y = 0
global_position.y = clamp(global_position.y, 0, screen.y)
take_damage(GameConfig.ship_wall_damage)
func take_damage(amount: int) -> void:
if _invincible or _dead:
return
health = max(0, health - amount)
_update_hud()
if health == 0:
_die()
func _die() -> void:
_dead = true
visible = false
velocity = Vector2.ZERO
await get_tree().create_timer(GameConfig.ship_respawn_delay).timeout
_respawn()
func _respawn() -> void:
var screen = get_viewport_rect().size
global_position = screen / 2.0
velocity = Vector2.ZERO
rotation = 0.0
health = GameConfig.ship_max_health
_dead = false
visible = true
_invincible = true
_invincible_timer = GameConfig.ship_invincibility_time
_update_hud()
func _update_hud() -> void:
var label = get_node_or_null("/root/World/HUD/HealthLabel")
if label:
label.text = "HP: %d / %d" % [health, GameConfig.ship_max_health]
+1
View File
@@ -0,0 +1 @@
uid://c4kj3i73wjdcj
+246
View File
@@ -0,0 +1,246 @@
extends CanvasLayer
const RACES := [
{"id": 1, "name": "TERRAN FEDERATION", "sub": "Speed and balanced firepower", "color": Color(0.30, 0.55, 1.00)},
{"id": 2, "name": "THE COLLECTIVE", "sub": "Heavy armor, brute force", "color": Color(1.00, 0.45, 0.18)},
{"id": 3, "name": "IRON ORDER", "sub": "Precision strike force", "color": Color(0.28, 0.90, 0.42)},
]
const SHIPS := [
{
"name": "WARBIRD",
"role": "All-Round Fighter",
"desc": "The most versatile ship in the fleet. Solid speed, reliable firepower, and enough armor to stay in the fight. Forgiving to fly and effective in any situation — the go-to choice when you're not sure what you're up against.",
"path": "res://images/ships/Example_ships/1.png",
},
{
"name": "JAVELIN",
"role": "Fast Interceptor",
"desc": "Built for speed above everything else. Light armor means you have to pick your fights carefully, but nothing can outrun you. Best used for hit-and-run strikes, hunting weakened targets, and escaping when things go wrong.",
"path": "res://images/ships/Example_ships/1B.png",
},
{
"name": "SPIDER",
"role": "Heavy Support",
"desc": "Slow but extremely durable. High-volume fire makes it devastating in sustained engagements. Anchors a team by holding zones and suppressing enemy movements. Stay near allies — alone you're a target, together you're a wall.",
"path": "res://images/ships/Example_ships/2a.png",
},
{
"name": "LANCASTER",
"role": "Stealth Bomber",
"desc": "Medium speed with high burst damage potential. Built for patience — get into position, strike hard, and fade before the enemy can respond. Excels at flanking and ambushes. Requires discipline to play but punishes mistakes severely.",
"path": "res://images/ships/Example_ships/3b.png",
},
]
# Row layout constants (screen is 1280×800)
const ROW_X1 := 120.0
const ROW_X2 := 1160.0
const ROW_H := 130.0
const ROW_GAP := 10.0
const ROW_START := 195.0
const IMG_W := 130.0
var _root: Control
var _content: Control
var _offered: Array = []
var _chosen_race: Dictionary = {}
func _ready() -> void:
layer = 20
_offered = _pick_two_races()
_build_root()
_show_race_selection()
func _pick_two_races() -> Array:
var pool := RACES.duplicate()
pool.shuffle()
return [pool[0], pool[1]]
func _build_root() -> void:
_root = Control.new()
_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(_root)
var overlay := ColorRect.new()
overlay.color = Color(0.0, 0.0, 0.0, 0.65)
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
_root.add_child(overlay)
func _swap_content() -> Control:
if _content:
_content.queue_free()
_content = Control.new()
_content.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_content.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.add_child(_content)
return _content
# ── Race selection ───────────────────────────────────────────────────────────
func _show_race_selection() -> void:
var c := _swap_content()
_label(c, "CHOOSE YOUR FACTION", 32, Color(0.42, 0.87, 1.0),
0, 105, 1280, 160, HORIZONTAL_ALIGNMENT_CENTER)
_label(c, "This match: %s vs %s" % [_offered[0].name, _offered[1].name],
13, Color(0.45, 0.58, 0.72),
0, 170, 1280, 200, HORIZONTAL_ALIGNMENT_CENTER)
var left_btn := _make_race_btn(_offered[0])
_place(left_btn, 150, 225, 590, 540)
left_btn.pressed.connect(_on_race_chosen.bind(_offered[0]))
c.add_child(left_btn)
var right_btn := _make_race_btn(_offered[1])
_place(right_btn, 690, 225, 1130, 540)
right_btn.pressed.connect(_on_race_chosen.bind(_offered[1]))
c.add_child(right_btn)
func _on_race_chosen(race: Dictionary) -> void:
_chosen_race = race
GameConfig.player_race = race.id
_show_ship_selection()
# ── Ship selection ───────────────────────────────────────────────────────────
func _show_ship_selection() -> void:
var c := _swap_content()
_label(c, "SELECT YOUR SHIP", 30, Color(0.42, 0.87, 1.0),
0, 90, 1280, 145, HORIZONTAL_ALIGNMENT_CENTER)
_label(c, "Playing as — " + _chosen_race.name, 14, _chosen_race.color,
0, 155, 1280, 185, HORIZONTAL_ALIGNMENT_CENTER)
for i in SHIPS.size():
var y := ROW_START + i * (ROW_H + ROW_GAP)
var btn := _make_ship_row(SHIPS[i], _chosen_race.color)
_place(btn, ROW_X1, y, ROW_X2, y + ROW_H)
btn.pressed.connect(_on_ship_chosen.bind(SHIPS[i]))
c.add_child(btn)
func _on_ship_chosen(ship: Dictionary) -> void:
GameConfig.player_ship_path = ship.path
GameConfig.team_selected.emit()
queue_free()
# ── Button builders ──────────────────────────────────────────────────────────
func _make_race_btn(race: Dictionary) -> Button:
var btn := Button.new()
btn.text = ""
var col: Color = race.color
var sn := _flat(Color(col.r * 0.14, col.g * 0.14, col.b * 0.14, 0.96), 12)
sn.set_border_width_all(2)
sn.border_color = Color(col.r * 0.38, col.g * 0.38, col.b * 0.38)
var sh := _flat(Color(col.r * 0.24, col.g * 0.24, col.b * 0.24, 0.96), 12)
sh.set_border_width_all(2)
sh.border_color = col
btn.add_theme_stylebox_override("normal", sn)
btn.add_theme_stylebox_override("hover", sh)
btn.add_theme_stylebox_override("pressed", sh)
var vb := VBoxContainer.new()
vb.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
vb.offset_left = 20; vb.offset_top = 24; vb.offset_right = -20; vb.offset_bottom = -24
vb.alignment = BoxContainer.ALIGNMENT_CENTER
vb.add_theme_constant_override("separation", 16)
vb.mouse_filter = Control.MOUSE_FILTER_IGNORE
btn.add_child(vb)
var name_lbl := Label.new()
name_lbl.text = race.name
name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_lbl.add_theme_font_size_override("font_size", 28)
name_lbl.add_theme_color_override("font_color", col)
name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(name_lbl)
var sep := HSeparator.new()
sep.add_theme_color_override("color", Color(col.r * 0.4, col.g * 0.4, col.b * 0.4))
sep.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(sep)
var sub_lbl := Label.new()
sub_lbl.text = race.sub
sub_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
sub_lbl.add_theme_font_size_override("font_size", 15)
sub_lbl.add_theme_color_override("font_color", Color(0.60, 0.70, 0.82))
sub_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(sub_lbl)
var hint := Label.new()
hint.text = "CLICK TO SELECT"
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
hint.add_theme_font_size_override("font_size", 11)
hint.add_theme_color_override("font_color", Color(col.r * 0.5, col.g * 0.5, col.b * 0.5))
hint.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(hint)
return btn
func _make_ship_row(ship: Dictionary, accent: Color) -> Button:
var btn := Button.new()
btn.text = ""
var sn := _flat(Color(0.06, 0.09, 0.17, 0.95), 8)
sn.set_border_width_all(1)
sn.border_color = Color(0.18, 0.28, 0.48, 0.6)
var sh := _flat(Color(0.10, 0.16, 0.28, 0.97), 8)
sh.set_border_width_all(2)
sh.border_color = accent
btn.add_theme_stylebox_override("normal", sn)
btn.add_theme_stylebox_override("hover", sh)
btn.add_theme_stylebox_override("pressed", sh)
var tex := TextureRect.new()
tex.texture = load(ship.path)
tex.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
tex.offset_left = 8; tex.offset_top = 6; tex.offset_right = -8; tex.offset_bottom = -6
tex.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
tex.expand_mode = TextureRect.EXPAND_FIT_HEIGHT_PROPORTIONAL
tex.mouse_filter = Control.MOUSE_FILTER_IGNORE
btn.add_child(tex)
return btn
# ── Helpers ──────────────────────────────────────────────────────────────────
func _label(parent: Control, text: String, font_size: int, color: Color,
x1: float, y1: float, x2: float, y2: float,
align: HorizontalAlignment = HORIZONTAL_ALIGNMENT_LEFT) -> void:
var lbl := Label.new()
lbl.text = text
lbl.horizontal_alignment = align
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
lbl.add_theme_font_size_override("font_size", font_size)
lbl.add_theme_color_override("font_color", color)
_place(lbl, x1, y1, x2, y2)
parent.add_child(lbl)
func _place(node: Control, x1: float, y1: float, x2: float, y2: float) -> void:
node.set_anchor_and_offset(SIDE_LEFT, 0.0, x1)
node.set_anchor_and_offset(SIDE_TOP, 0.0, y1)
node.set_anchor_and_offset(SIDE_RIGHT, 0.0, x2)
node.set_anchor_and_offset(SIDE_BOTTOM, 0.0, y2)
func _flat(col: Color, radius: int = 0) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = col
s.set_corner_radius_all(radius)
return s
+1
View File
@@ -0,0 +1 @@
uid://33672ukno060
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://dteamselect01"]
[ext_resource type="Script" path="res://team_select.gd" id="1_script"]
[node name="TeamSelect" type="CanvasLayer"]
script = ExtResource("1_script")
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
[gd_scene format=3 uid="uid://c77s15ns13p6y"]
[ext_resource type="PackedScene" uid="uid://dygtdlkvo6ofl" path="res://node_2d.tscn" id="1_f3sb7"]
[ext_resource type="Texture2D" uid="uid://d0i6jwqh131b1" path="res://images/background/skybox/1.png" id="1_fj7yv"]
[ext_resource type="PackedScene" uid="uid://cpausemenu01a" path="res://pause_menu.tscn" id="2_pm"]
[ext_resource type="PackedScene" uid="uid://dteamselect01" path="res://team_select.tscn" id="3_ts"]
[node name="World" type="Node2D" unique_id=1962020789]
[node name="TextureRect" type="TextureRect" parent="." unique_id=1177783968]
offset_right = 1280.0
offset_bottom = 800.0
texture = ExtResource("1_fj7yv")
[node name="Player" parent="." unique_id=2118863138 instance=ExtResource("1_f3sb7")]
[node name="HUD" type="CanvasLayer" parent="."]
[node name="HealthLabel" type="Label" parent="HUD"]
offset_left = 20.0
offset_top = 20.0
offset_right = 300.0
offset_bottom = 60.0
text = "HP: 100 / 100"
theme_override_font_sizes/font_size = 24
[node name="PauseMenu" parent="." instance=ExtResource("2_pm")]
[node name="TeamSelect" parent="." instance=ExtResource("3_ts")]