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>
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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. Small–medium 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
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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 |
|
||||
@@ -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 |
|
||||
@@ -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)
|
||||
@@ -0,0 +1,4 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
@@ -0,0 +1,2 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,3 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dho8dpmkj8bdu
|
||||
@@ -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)
|
||||
|
After Width: | Height: | Size: 23 KiB |
@@ -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
|
||||
@@ -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 = ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdh612na2digh
|
||||
@@ -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 |
@@ -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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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.
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
@@ -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")
|
||||
@@ -0,0 +1 @@
|
||||
uid://3it6fh07tirl
|
||||
@@ -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")
|
||||
@@ -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]
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://lqjtib7jlqg0
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
@@ -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")
|
||||
@@ -0,0 +1 @@
|
||||
uid://dg0sojl1wf8dh
|
||||
@@ -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")
|
||||
@@ -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]
|
||||
@@ -0,0 +1 @@
|
||||
uid://c4kj3i73wjdcj
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://33672ukno060
|
||||
@@ -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")
|
||||
@@ -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")]
|
||||