Add engine flames, damage numbers, nameplates, music, and combat/collision fixes
Feature work (CLAUDE.md items 17-18): - Race roster art rework (Apex/ISN/ORC) plus per-ship engine-flame sprites that swap in on thrust - Floating damage-number VFX, per-ship nameplates, energy bar rework - Background music manager and per-race ship SFX Bugfixes/tuning from live testing: - No friendly fire: bullets stop on a teammate's hull but deal no damage (bullet.gd), decided by race via PlayerRegistry - Ships no longer physically block each other (own collision layer, ship.tscn/bullet.tscn) -- fixes ramming/parking trolling and the stuck-on- corpse bug after a kill - Explosions ghost any ship's sprite passing through instead of blocking it (effects/explosion.gd's GhostArea) - Fixed two "flushing physics queries" crashes from the above: explosion spawn and death/respawn collision toggling are now deferred (ship_movement.gd) - Dead ships stop blocking bullets during their respawn delay - Bots fly a mixed roster (1 Tank + 2 Gunner + rest Fighter per team) instead of always the Fighter (bot_manager.gd) - Camera zoom knob, larger/longer-lived damage numbers and nameplate text, red (not yellow) enemy name color for better contrast Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@@ -38,6 +38,8 @@ README for how to run/extend it.
|
|||||||
16. **Real game-server pool (self-registration + heartbeat + stale sweep)** — `NetworkManager.host_server()` now calls the new `MatchmakingClient.register_server()` once on boot and then every `HEARTBEAT_INTERVAL` (8s) via a `Timer`, reporting live `PlayerRegistry.players.size()`/`MAX_PLAYERS` — item 15's demo-seeded rows are no longer the only thing populating `GET /servers`; a real server registering on the same `(ip, port, mode)` as a demo row just takes it over in place, no special-casing needed. `--server-ip=` is a new cmdline arg for the IP a hosted server advertises (defaults to loopback for local dev — see the reachability note already in `matchmaking-api/README.md` about `DEV_SEED_SERVER_IP`, same constraint applies here). API-side, `POST /servers/register` now computes `status` from `player_count`/`max_players` (`full` vs `available`) instead of always writing `available`, and a new background loop (`sweep_stale_servers` in `app/routers/servers.py`, run every 5s from `main.py`'s lifespan) marks any server `offline` once its `last_heartbeat` exceeds `server_stale_seconds` (20s) — catches a crashed/killed server that never got to deregister cleanly, so a dead server doesn't sit in the list looking joinable forever. `menu/server_select.gd`'s connect button now also blocks (with a message) on `status == "full"`, not just `"offline"`.
|
16. **Real game-server pool (self-registration + heartbeat + stale sweep)** — `NetworkManager.host_server()` now calls the new `MatchmakingClient.register_server()` once on boot and then every `HEARTBEAT_INTERVAL` (8s) via a `Timer`, reporting live `PlayerRegistry.players.size()`/`MAX_PLAYERS` — item 15's demo-seeded rows are no longer the only thing populating `GET /servers`; a real server registering on the same `(ip, port, mode)` as a demo row just takes it over in place, no special-casing needed. `--server-ip=` is a new cmdline arg for the IP a hosted server advertises (defaults to loopback for local dev — see the reachability note already in `matchmaking-api/README.md` about `DEV_SEED_SERVER_IP`, same constraint applies here). API-side, `POST /servers/register` now computes `status` from `player_count`/`max_players` (`full` vs `available`) instead of always writing `available`, and a new background loop (`sweep_stale_servers` in `app/routers/servers.py`, run every 5s from `main.py`'s lifespan) marks any server `offline` once its `last_heartbeat` exceeds `server_stale_seconds` (20s) — catches a crashed/killed server that never got to deregister cleanly, so a dead server doesn't sit in the list looking joinable forever. `menu/server_select.gd`'s connect button now also blocks (with a message) on `status == "full"`, not just `"offline"`.
|
||||||
17. **Race roster replaced (Terran/Mechanos/Vorg → Apex Dynamics/Inner Sphere Navy/Outer Rim Collective)** — `team_select.gd`'s `RACES` now points at the 3 factions from `overview/racesclasses.md`'s pivot, each with a 3-ship Fighter/Gunner/Tank roster (Lancet/Pulsar/Sovereign, Patriot/Barrage/Behemoth, Rail-Jack/Scrap-Spitter/Iron-Clad) instead of the old 5-ship Interceptor/Gunship/Bomber/Support/Heavy set; `SPEED_BY_ROLE` shrunk to match. Art was extracted from 3 user-provided concept sheets (originally dropped at `assets/images/ship/`, now cropped per-ship into `assets/images/ships/apex|isn|orc/` with a `source/` copy of each sheet, same convention as the old race folders) — background removed via flood-fill + largest-connected-component matting (plain background for Apex, starfield for ORC, grid-lined UI panels for ISN, each needing different thresholding). ISN and ORC's source art was drawn nose-*sideways*; both were rotated 90° before saving since this project's ship rotation convention is nose-up at `rotation = 0` (`Vector2.UP.rotated(rotation)` in `ship_movement.gd`) — Apex's source art was already nose-up. `scale` values were computed with the same per-role target-on-screen-height normalization the old roster used (Fighter/Gunner/Tank targets reuse the old Interceptor/Gunship/Heavy heights, ~52.5/58.5/82.5px) so ship sizes read consistently across factions. Verified by hosting a real server and screenshotting bot fill flying with the new sprites — no load errors, transparency and nose-up orientation both correct. The old `terran/mech/vorg` asset folders are left on disk but unreferenced (no code points at them); not deleted since that wasn't asked for.
|
17. **Race roster replaced (Terran/Mechanos/Vorg → Apex Dynamics/Inner Sphere Navy/Outer Rim Collective)** — `team_select.gd`'s `RACES` now points at the 3 factions from `overview/racesclasses.md`'s pivot, each with a 3-ship Fighter/Gunner/Tank roster (Lancet/Pulsar/Sovereign, Patriot/Barrage/Behemoth, Rail-Jack/Scrap-Spitter/Iron-Clad) instead of the old 5-ship Interceptor/Gunship/Bomber/Support/Heavy set; `SPEED_BY_ROLE` shrunk to match. Art was extracted from 3 user-provided concept sheets (originally dropped at `assets/images/ship/`, now cropped per-ship into `assets/images/ships/apex|isn|orc/` with a `source/` copy of each sheet, same convention as the old race folders) — background removed via flood-fill + largest-connected-component matting (plain background for Apex, starfield for ORC, grid-lined UI panels for ISN, each needing different thresholding). ISN and ORC's source art was drawn nose-*sideways*; both were rotated 90° before saving since this project's ship rotation convention is nose-up at `rotation = 0` (`Vector2.UP.rotated(rotation)` in `ship_movement.gd`) — Apex's source art was already nose-up. `scale` values were computed with the same per-role target-on-screen-height normalization the old roster used (Fighter/Gunner/Tank targets reuse the old Interceptor/Gunship/Heavy heights, ~52.5/58.5/82.5px) so ship sizes read consistently across factions. Verified by hosting a real server and screenshotting bot fill flying with the new sprites — no load errors, transparency and nose-up orientation both correct. The old `terran/mech/vorg` asset folders are left on disk but unreferenced (no code points at them); not deleted since that wasn't asked for.
|
||||||
|
|
||||||
|
18. **Engine-flame sprite swap on thrust** — 6 of item 17's 9 ships (ISN's Patriot/Barrage/Behemoth, ORC's Rail-Jack/Scrap-Spitter/Iron-Clad) gained a second `_flame.png` sprite alongside their idle one, cropped from the same concept sheets' flame-frame columns (ISN's sheet has a clean idle/flame pair per ship; ORC's only unambiguous case was Rail-Jack's brighter 3rd-column flame, so Scrap-Spitter/Iron-Clad's flame PNGs are just copies of their idle sprite for now — their sheet's other columns are damage/weapon-fire poses, not more exhaust). Behemoth's flame was composited from two separate sheet crops (clean idle hull + a flame-only crop of just its thrusters) rather than cropped directly, since the sheet's flame frame has a large red shield-ability graphic overlapping the hull that isn't exhaust. Apex's 3 ships (Lancet/Pulsar/Sovereign) have no flame art — their concept sheet never drew a thrust pose — so they're untouched and always show their one sprite. Wired into gameplay via a new optional `flame_path` per ship in `team_select.gd`'s `RACES`, threaded through `PlayerRegistry` (`submit_local_loadout`/`register_bot`/the loadout RPCs, new `ship_flame_path` field, default `""`) so bots and remote peers all get it too, not just the local owner. `ship_movement.gd`'s `_set_thrust_sprite()` swaps the `Sprite2D` texture based on whether the up/down thrust key is *currently held*, not current speed — this game's movement has no drag, so a first cut keyed off `velocity.length()` left the flame on long after letting off the gas since a released ship keeps drifting at speed. The owner predicts this locally the instant it samples input (`_owner_tick`); the server does the same for bots/itself (`_server_tick`) and broadcasts its authoritative value as a new `thrusting` bool on the existing `_receive_state` RPC so every other peer's view of a ship — which never sees raw input, only replicated state — stays in sync too, with no separate RPC needed. Ships with no `flame_path` (Apex) just skip the check entirely (`_flame_texture == null` short-circuits).
|
||||||
|
|
||||||
## Current Tasks
|
## Current Tasks
|
||||||
|
|
||||||
- [ ] Asteroids and environment hazards
|
- [ ] Asteroids and environment hazards
|
||||||
|
|||||||
|
After Width: | Height: | Size: 176 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://csymmghcpcpue"
|
||||||
|
path="res://.godot/imported/70fb9114-c6c8-459c-ad2f-8111f30c9b82.jpeg-7e809ec090d7c262b7af7abf69bc7fa3.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/banners/70fb9114-c6c8-459c-ad2f-8111f30c9b82.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/70fb9114-c6c8-459c-ad2f-8111f30c9b82.jpeg-7e809ec090d7c262b7af7abf69bc7fa3.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: 197 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://do26o8h5m5hb8"
|
||||||
|
path="res://.godot/imported/ed212cd5-530a-4520-ba00-23795b40f32c.jpeg-c98f9e50e9255c5b98318bf225959231.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/banners/ed212cd5-530a-4520-ba00-23795b40f32c.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/ed212cd5-530a-4520-ba00-23795b40f32c.jpeg-c98f9e50e9255c5b98318bf225959231.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: 164 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://7enw50oma8ny"
|
||||||
|
path="res://.godot/imported/f6790c6b-65b5-4701-ad12-75db071f4969.jpeg-ee0c086fa7093a1da46de3e84860fc21.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/banners/f6790c6b-65b5-4701-ad12-75db071f4969.jpeg"
|
||||||
|
dest_files=["res://.godot/imported/f6790c6b-65b5-4701-ad12-75db071f4969.jpeg-ee0c086fa7093a1da46de3e84860fc21.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
|
||||||
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cbvtvq4i1b6mj"
|
||||||
|
path="res://.godot/imported/barrage_flame.png-dc31f37d25c43c1dd680a3d87a9d51ce.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/ships/isn/barrage_flame.png"
|
||||||
|
dest_files=["res://.godot/imported/barrage_flame.png-dc31f37d25c43c1dd680a3d87a9d51ce.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
|
||||||
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 109 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://jjqmov8jp2gn"
|
||||||
|
path="res://.godot/imported/behemoth_flame.png-c0e05b4a3fe0f18416e728d95489a5c4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/ships/isn/behemoth_flame.png"
|
||||||
|
dest_files=["res://.godot/imported/behemoth_flame.png-c0e05b4a3fe0f18416e728d95489a5c4.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
|
||||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dkdx0l7j5niek"
|
||||||
|
path="res://.godot/imported/patriot_flame.png-9ca141f355519db996c8cde162fafa5f.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/ships/isn/patriot_flame.png"
|
||||||
|
dest_files=["res://.godot/imported/patriot_flame.png-9ca141f355519db996c8cde162fafa5f.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: 91 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dh6xgxgv31wxb"
|
||||||
|
path="res://.godot/imported/iron_clad_flame.png-920e3b9df5be762900ddeb1e02f1c4fe.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/ships/orc/iron_clad_flame.png"
|
||||||
|
dest_files=["res://.godot/imported/iron_clad_flame.png-920e3b9df5be762900ddeb1e02f1c4fe.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: 33 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dqjwq05h8bvo"
|
||||||
|
path="res://.godot/imported/rail_jack_flame.png-adcdcd3322d05f20100b740587d9ca87.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/ships/orc/rail_jack_flame.png"
|
||||||
|
dest_files=["res://.godot/imported/rail_jack_flame.png-adcdcd3322d05f20100b740587d9ca87.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: 51 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bfp4jaegntjnw"
|
||||||
|
path="res://.godot/imported/scrap_spitter_flame.png-4dde0769583adec98b69041adc88e9d5.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/images/ships/orc/scrap_spitter_flame.png"
|
||||||
|
dest_files=["res://.godot/imported/scrap_spitter_flame.png-4dde0769583adec98b69041adc88e9d5.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,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://di0k3q7l1cqic"
|
||||||
|
path="res://.godot/imported/MyVeryOwnDeadShip.ogg-2887b4e03d42491161284b1a1f82465a.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sound/menu/MyVeryOwnDeadShip.ogg"
|
||||||
|
dest_files=["res://.godot/imported/MyVeryOwnDeadShip.ogg-2887b4e03d42491161284b1a1f82465a.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://fvoe3ofyvk0"
|
||||||
|
path="res://.godot/imported/apex_1.wav-04224be7c4d888cfa8ad7540e351b885.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sound/ships/apex/apex_1.wav"
|
||||||
|
dest_files=["res://.godot/imported/apex_1.wav-04224be7c4d888cfa8ad7540e351b885.sample"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
force/8_bit=false
|
||||||
|
force/mono=false
|
||||||
|
force/max_rate=false
|
||||||
|
force/max_rate_hz=44100
|
||||||
|
edit/trim=false
|
||||||
|
edit/normalize=false
|
||||||
|
edit/loop_mode=0
|
||||||
|
edit/loop_begin=0
|
||||||
|
edit/loop_end=-1
|
||||||
|
compress/mode=2
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://trblktkshml2"
|
||||||
|
path="res://.godot/imported/isn_1.wav-debf57fe39a72ee924b79a5dd14ab830.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sound/ships/isn/isn_1.wav"
|
||||||
|
dest_files=["res://.godot/imported/isn_1.wav-debf57fe39a72ee924b79a5dd14ab830.sample"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
force/8_bit=false
|
||||||
|
force/mono=false
|
||||||
|
force/max_rate=false
|
||||||
|
force/max_rate_hz=44100
|
||||||
|
edit/trim=false
|
||||||
|
edit/normalize=false
|
||||||
|
edit/loop_mode=0
|
||||||
|
edit/loop_begin=0
|
||||||
|
edit/loop_end=-1
|
||||||
|
compress/mode=2
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://d2jvn7p3wll04"
|
||||||
|
path="res://.godot/imported/orc_1.wav-0e7d27aca8a1e64998f822086458e7b2.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sound/ships/orc/orc_1.wav"
|
||||||
|
dest_files=["res://.godot/imported/orc_1.wav-0e7d27aca8a1e64998f822086458e7b2.sample"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
force/8_bit=false
|
||||||
|
force/mono=false
|
||||||
|
force/max_rate=false
|
||||||
|
force/max_rate_hz=44100
|
||||||
|
edit/trim=false
|
||||||
|
edit/normalize=false
|
||||||
|
edit/loop_mode=0
|
||||||
|
edit/loop_begin=0
|
||||||
|
edit/loop_end=-1
|
||||||
|
compress/mode=2
|
||||||
@@ -10,22 +10,34 @@ var ship_rotation_speed: float = 1.0
|
|||||||
var ship_rotation_ramp_delay: float = 0.15
|
var ship_rotation_ramp_delay: float = 0.15
|
||||||
var ship_rotation_speed_held: float = 2.0
|
var ship_rotation_speed_held: float = 2.0
|
||||||
|
|
||||||
# Shooting
|
# Shooting — damage/energy cost/bullet count all scale per-role (Fighter/
|
||||||
|
# Gunner/Tank, see menu/team_select.gd's RACES "role" field); Gunner and Tank
|
||||||
|
# fire their bullets in a side-by-side pair instead of Fighter's single shot
|
||||||
|
# (see ship_movement.gd's _server_process_shoot()).
|
||||||
var ship_fire_rate: float = 0.15
|
var ship_fire_rate: float = 0.15
|
||||||
var bullet_speed: float = 450.0
|
var bullet_speed: float = 450.0
|
||||||
|
var ship_bullet_damage_by_role: Dictionary = {"Fighter": 100, "Gunner": 25, "Tank": 50}
|
||||||
|
var ship_bullet_count_by_role: Dictionary = {"Fighter": 1, "Gunner": 2, "Tank": 2}
|
||||||
|
var bullet_side_spacing: float = 14.0
|
||||||
|
var bullet_max_range: float = 1500.0
|
||||||
|
|
||||||
|
# Per-shot sound effect (menu/team_select.gd's RACES "sound_path" field, one
|
||||||
|
# shared clip per faction) — max_distance is in world/pixel units, same as
|
||||||
|
# AudioStreamPlayer2D expects, so only ships within this radius of the shot
|
||||||
|
# actually hear it (see world.gd's _play_shoot_sound()).
|
||||||
|
var ship_shoot_sound_max_distance: float = 500.0
|
||||||
|
|
||||||
# Health & respawn
|
# Health & respawn
|
||||||
var ship_max_health: int = 100
|
var ship_max_health: int = 100
|
||||||
var ship_respawn_delay: float = 3.0
|
var ship_respawn_delay: float = 3.0
|
||||||
var ship_invincibility_time: float = 2.0
|
var ship_invincibility_time: float = 2.0
|
||||||
var bullet_damage: int = 40
|
|
||||||
|
|
||||||
# Energy (see ships/ship_movement.gd) — max energy scales by ship role
|
# Energy (see ships/ship_movement.gd) — max energy scales by ship role
|
||||||
# (Fighter/Gunner/Tank, see menu/team_select.gd's RACES ship "role" field);
|
# (Fighter/Gunner/Tank, see menu/team_select.gd's RACES ship "role" field);
|
||||||
# regen is a flat rate regardless of role, so bigger ships take longer to
|
# regen is a flat rate regardless of role, so bigger ships take longer to
|
||||||
# top off from empty than smaller ones.
|
# top off from empty than smaller ones.
|
||||||
var ship_max_energy_by_role: Dictionary = {"Fighter": 100.0, "Gunner": 150.0, "Tank": 250.0}
|
var ship_max_energy_by_role: Dictionary = {"Fighter": 100.0, "Gunner": 150.0, "Tank": 250.0}
|
||||||
var bullet_energy_cost: float = 75.0
|
var ship_bullet_energy_cost_by_role: Dictionary = {"Fighter": 75.0, "Gunner": 25.0, "Tank": 50.0}
|
||||||
var ship_energy_regen_rate: float = 100.0 / 2.5 # 100 energy per 2.5s
|
var ship_energy_regen_rate: float = 100.0 / 2.5 # 100 energy per 2.5s
|
||||||
|
|
||||||
# Environment collision (asteroids only) — bounce-back and impact damage scale with impact speed
|
# Environment collision (asteroids only) — bounce-back and impact damage scale with impact speed
|
||||||
@@ -38,6 +50,11 @@ var ship_collision_damage_scale: float = 0.064
|
|||||||
# balanced per-role values, just scales the end result uniformly.
|
# balanced per-role values, just scales the end result uniformly.
|
||||||
var ship_scale_factor: float = 0.75
|
var ship_scale_factor: float = 0.75
|
||||||
|
|
||||||
|
# How much of the sprite's own texture bounds its collision capsule covers
|
||||||
|
# (see ship_movement.gd's _update_collision_shape()) — under 1.0 to stay
|
||||||
|
# inside the hull's soft/anti-aliased edge instead of the full texture rect.
|
||||||
|
var ship_hitbox_scale: float = 0.85
|
||||||
|
|
||||||
# Explosion VFX (effects/explosion.gd) — one shared knob to resize every
|
# Explosion VFX (effects/explosion.gd) — one shared knob to resize every
|
||||||
# explosion in the game at once; explosion_fps controls playback speed of the
|
# explosion in the game at once; explosion_fps controls playback speed of the
|
||||||
# 12-frame animation (see assets/images/effects/explosion/). Each frame is
|
# 12-frame animation (see assets/images/effects/explosion/). Each frame is
|
||||||
@@ -48,6 +65,35 @@ var ship_scale_factor: float = 0.75
|
|||||||
var explosion_scale: float = 0.75
|
var explosion_scale: float = 0.75
|
||||||
var explosion_fps: float = 24.0 # 60 ticks / 24.0 fps = 2.5s total
|
var explosion_fps: float = 24.0 # 60 ticks / 24.0 fps = 2.5s total
|
||||||
|
|
||||||
|
# Damage-number VFX (effects/damage_number.gd) — floating "(amount)" combat
|
||||||
|
# text shown only to the attacker when their shot lands, not broadcast to
|
||||||
|
# every peer like the kill feed (see ship_movement.gd's
|
||||||
|
# _notify_damage_dealt()/_show_damage_number()).
|
||||||
|
var damage_number_rise_distance: float = 36.0
|
||||||
|
var damage_number_duration: float = 1.4
|
||||||
|
var damage_number_font_size: int = 34
|
||||||
|
var damage_number_color: Color = Color(0.95, 0.15, 0.15)
|
||||||
|
|
||||||
|
# Shared white/teammate vs. red/enemy color scheme -- used by both the
|
||||||
|
# top-left roster (hud/player_list.gd) and in-world ship nameplates
|
||||||
|
# (ships/ship_movement.gd) so the two always agree. Was a yellow enemy_color
|
||||||
|
# that read too close to white/teammate at a glance; red is unambiguous.
|
||||||
|
var team_color: Color = Color(0.92, 0.95, 1.0)
|
||||||
|
var enemy_color: Color = Color(0.95, 0.25, 0.25)
|
||||||
|
|
||||||
|
# In-world ship nameplate (ships/ship_movement.gd) -- offset from ship center
|
||||||
|
# so it sits at the bottom-right of the sprite rather than directly under it
|
||||||
|
# (keeps it clear of the engine flame and doesn't read as a health-bar-like
|
||||||
|
# element stacked straight below).
|
||||||
|
var ship_nameplate_offset: Vector2 = Vector2(26, 24)
|
||||||
|
var ship_nameplate_font_size: int = 18
|
||||||
|
|
||||||
|
# One shared knob for the local player's view distance -- Camera2D.zoom below
|
||||||
|
# 1.0 shows more world per screen pixel (zoomed out), above 1.0 shows less
|
||||||
|
# (zoomed in). Applied once in ship_movement.gd's _ready() to the owner's own
|
||||||
|
# Camera2D only; doesn't affect any other peer's view.
|
||||||
|
var camera_zoom: float = 0.85
|
||||||
|
|
||||||
# Locally-remembered callsign, pre-fills the name field on the menu.
|
# Locally-remembered callsign, pre-fills the name field on the menu.
|
||||||
# Race/ship/speed are per-peer now — see PlayerRegistry.
|
# Race/ship/speed are per-peer now — see PlayerRegistry.
|
||||||
var player_name: String = ""
|
var player_name: String = ""
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# Background music for the pre-game screens (main menu, server select, and
|
||||||
|
# the in-world "LOADING MATCH..." screen up to a player's first-ever ship
|
||||||
|
# pick). One shared autoload — rather than a player per-scene — so the track
|
||||||
|
# keeps playing seamlessly across scene changes instead of restarting on
|
||||||
|
# every menu->menu transition (main_menu.tscn/server_select.tscn are swapped
|
||||||
|
# in whole via change_scene_to_file, which frees everything in the old scene).
|
||||||
|
const MENU_MUSIC: AudioStream = preload("res://assets/sound/menu/MyVeryOwnDeadShip.ogg")
|
||||||
|
const FADE_TIME := 1.2
|
||||||
|
const PLAY_VOLUME_DB := -8.0
|
||||||
|
const FADE_OUT_VOLUME_DB := -40.0
|
||||||
|
|
||||||
|
var _player: AudioStreamPlayer
|
||||||
|
var _fade_tween: Tween
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_player = AudioStreamPlayer.new()
|
||||||
|
_player.stream = MENU_MUSIC
|
||||||
|
_player.volume_db = PLAY_VOLUME_DB
|
||||||
|
add_child(_player)
|
||||||
|
_player.finished.connect(_player.play) # loop the track
|
||||||
|
|
||||||
|
|
||||||
|
# Idempotent — safe to call from every pre-game screen's _ready() without
|
||||||
|
# restarting the track if it's already playing (or resuming from a fade the
|
||||||
|
# player interrupted, e.g. quitting straight back to the menu mid-fade-out).
|
||||||
|
func play_menu_music() -> void:
|
||||||
|
if _fade_tween and _fade_tween.is_running():
|
||||||
|
_fade_tween.kill()
|
||||||
|
_player.volume_db = PLAY_VOLUME_DB
|
||||||
|
if not _player.playing:
|
||||||
|
_player.play()
|
||||||
|
|
||||||
|
|
||||||
|
func stop_menu_music() -> void:
|
||||||
|
if not _player.playing:
|
||||||
|
return
|
||||||
|
if _fade_tween:
|
||||||
|
_fade_tween.kill()
|
||||||
|
_fade_tween = create_tween()
|
||||||
|
_fade_tween.tween_property(_player, "volume_db", FADE_OUT_VOLUME_DB, FADE_TIME)
|
||||||
|
_fade_tween.tween_callback(_player.stop)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b3igqhi4jnw8h
|
||||||
@@ -17,7 +17,7 @@ signal player_joined(peer_id: int)
|
|||||||
# race is already overwritten in `players`.
|
# race is already overwritten in `players`.
|
||||||
signal race_changed(peer_id: int, old_race: int, new_race: int)
|
signal race_changed(peer_id: int, old_race: int, new_race: int)
|
||||||
|
|
||||||
var players: Dictionary = {} # peer_id -> {name, race, ship_path, ship_scale, ship_speed_factor, role}
|
var players: Dictionary = {} # peer_id -> {name, race, ship_path, ship_scale, ship_speed_factor, role, ship_flame_path, ship_sound_path}
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -32,18 +32,18 @@ func get_info(peer_id: int) -> Dictionary:
|
|||||||
return players.get(peer_id, {})
|
return players.get(peer_id, {})
|
||||||
|
|
||||||
|
|
||||||
func submit_local_loadout(player_name: String, race: int, ship_path: String, ship_scale: float, ship_speed_factor: float, role: String) -> void:
|
func submit_local_loadout(player_name: String, race: int, ship_path: String, ship_scale: float, ship_speed_factor: float, role: String, ship_flame_path: String = "", ship_sound_path: String = "") -> void:
|
||||||
submit_loadout.rpc(player_name, race, ship_path, ship_scale, ship_speed_factor, role)
|
submit_loadout.rpc(player_name, race, ship_path, ship_scale, ship_speed_factor, role, ship_flame_path, ship_sound_path)
|
||||||
|
|
||||||
|
|
||||||
# Server-only: registers a bot's loadout the same way a real player's
|
# Server-only: registers a bot's loadout the same way a real player's
|
||||||
# submit_loadout would (minus the backfill, since a fresh bot has nothing to
|
# submit_loadout would (minus the backfill, since a fresh bot has nothing to
|
||||||
# learn), broadcasting to every connected peer so bots render identically to
|
# learn), broadcasting to every connected peer so bots render identically to
|
||||||
# human ships on every client. See bots/bot_manager.gd.
|
# human ships on every client. See bots/bot_manager.gd.
|
||||||
func register_bot(peer_id: int, bot_name: String, race: int, ship_path: String, ship_scale: float, ship_speed_factor: float, role: String) -> void:
|
func register_bot(peer_id: int, bot_name: String, race: int, ship_path: String, ship_scale: float, ship_speed_factor: float, role: String, ship_flame_path: String = "", ship_sound_path: String = "") -> void:
|
||||||
for pid in multiplayer.get_peers():
|
for pid in multiplayer.get_peers():
|
||||||
_receive_loadout.rpc_id(pid, peer_id, bot_name, race, ship_path, ship_scale, ship_speed_factor, role)
|
_receive_loadout.rpc_id(pid, peer_id, bot_name, race, ship_path, ship_scale, ship_speed_factor, role, ship_flame_path, ship_sound_path)
|
||||||
_store(peer_id, bot_name, race, ship_path, ship_scale, ship_speed_factor, role)
|
_store(peer_id, bot_name, race, ship_path, ship_scale, ship_speed_factor, role, ship_flame_path, ship_sound_path)
|
||||||
|
|
||||||
|
|
||||||
func unregister_bot(peer_id: int) -> void:
|
func unregister_bot(peer_id: int) -> void:
|
||||||
@@ -54,32 +54,32 @@ func unregister_bot(peer_id: int) -> void:
|
|||||||
# the new loadout to everyone else, and backfills the newcomer with everyone
|
# the new loadout to everyone else, and backfills the newcomer with everyone
|
||||||
# else's already-known loadout.
|
# else's already-known loadout.
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func submit_loadout(player_name: String, race: int, ship_path: String, ship_scale: float, ship_speed_factor: float, role: String) -> void:
|
func submit_loadout(player_name: String, race: int, ship_path: String, ship_scale: float, ship_speed_factor: float, role: String, ship_flame_path: String = "", ship_sound_path: String = "") -> void:
|
||||||
var sender_id := multiplayer.get_remote_sender_id()
|
var sender_id := multiplayer.get_remote_sender_id()
|
||||||
if sender_id == 0:
|
if sender_id == 0:
|
||||||
sender_id = multiplayer.get_unique_id()
|
sender_id = multiplayer.get_unique_id()
|
||||||
|
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
_store(sender_id, player_name, race, ship_path, ship_scale, ship_speed_factor, role)
|
_store(sender_id, player_name, race, ship_path, ship_scale, ship_speed_factor, role, ship_flame_path, ship_sound_path)
|
||||||
return
|
return
|
||||||
|
|
||||||
for peer_id in multiplayer.get_peers():
|
for peer_id in multiplayer.get_peers():
|
||||||
if peer_id != sender_id:
|
if peer_id != sender_id:
|
||||||
_receive_loadout.rpc_id(peer_id, sender_id, player_name, race, ship_path, ship_scale, ship_speed_factor, role)
|
_receive_loadout.rpc_id(peer_id, sender_id, player_name, race, ship_path, ship_scale, ship_speed_factor, role, ship_flame_path, ship_sound_path)
|
||||||
for peer_id in players:
|
for peer_id in players:
|
||||||
if peer_id != sender_id:
|
if peer_id != sender_id:
|
||||||
var info: Dictionary = players[peer_id]
|
var info: Dictionary = players[peer_id]
|
||||||
_receive_loadout.rpc_id(sender_id, peer_id, info.name, info.race, info.ship_path, info.ship_scale, info.ship_speed_factor, info.role)
|
_receive_loadout.rpc_id(sender_id, peer_id, info.name, info.race, info.ship_path, info.ship_scale, info.ship_speed_factor, info.role, info.get("ship_flame_path", ""), info.get("ship_sound_path", ""))
|
||||||
|
|
||||||
_store(sender_id, player_name, race, ship_path, ship_scale, ship_speed_factor, role)
|
_store(sender_id, player_name, race, ship_path, ship_scale, ship_speed_factor, role, ship_flame_path, ship_sound_path)
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "reliable")
|
@rpc("authority", "call_remote", "reliable")
|
||||||
func _receive_loadout(peer_id: int, player_name: String, race: int, ship_path: String, ship_scale: float, ship_speed_factor: float, role: String) -> void:
|
func _receive_loadout(peer_id: int, player_name: String, race: int, ship_path: String, ship_scale: float, ship_speed_factor: float, role: String, ship_flame_path: String = "", ship_sound_path: String = "") -> void:
|
||||||
_store(peer_id, player_name, race, ship_path, ship_scale, ship_speed_factor, role)
|
_store(peer_id, player_name, race, ship_path, ship_scale, ship_speed_factor, role, ship_flame_path, ship_sound_path)
|
||||||
|
|
||||||
|
|
||||||
func _store(peer_id: int, player_name: String, race: int, ship_path: String, ship_scale: float, ship_speed_factor: float, role: String) -> void:
|
func _store(peer_id: int, player_name: String, race: int, ship_path: String, ship_scale: float, ship_speed_factor: float, role: String, ship_flame_path: String = "", ship_sound_path: String = "") -> void:
|
||||||
var old_race: int = players.get(peer_id, {}).get("race", 0)
|
var old_race: int = players.get(peer_id, {}).get("race", 0)
|
||||||
players[peer_id] = {
|
players[peer_id] = {
|
||||||
"name": player_name,
|
"name": player_name,
|
||||||
@@ -88,6 +88,8 @@ func _store(peer_id: int, player_name: String, race: int, ship_path: String, shi
|
|||||||
"ship_scale": ship_scale,
|
"ship_scale": ship_scale,
|
||||||
"ship_speed_factor": ship_speed_factor,
|
"ship_speed_factor": ship_speed_factor,
|
||||||
"role": role,
|
"role": role,
|
||||||
|
"ship_flame_path": ship_flame_path,
|
||||||
|
"ship_sound_path": ship_sound_path,
|
||||||
}
|
}
|
||||||
print("[PlayerRegistry] peer %d loadout: %s" % [peer_id, players[peer_id]])
|
print("[PlayerRegistry] peer %d loadout: %s" % [peer_id, players[peer_id]])
|
||||||
loadout_updated.emit(peer_id)
|
loadout_updated.emit(peer_id)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func _reconcile_race(race_id: int) -> void:
|
|||||||
var desired: int = max(0, GameConfig.bot_min_team_size - humans)
|
var desired: int = max(0, GameConfig.bot_min_team_size - humans)
|
||||||
var current: Array = _bots_by_race.get(race_id, [])
|
var current: Array = _bots_by_race.get(race_id, [])
|
||||||
while current.size() < desired:
|
while current.size() < desired:
|
||||||
current.append(_spawn_bot(race_id))
|
current.append(_spawn_bot(race_id, current.size()))
|
||||||
while current.size() > desired:
|
while current.size() > desired:
|
||||||
_despawn_bot(current.pop_back())
|
_despawn_bot(current.pop_back())
|
||||||
_bots_by_race[race_id] = current
|
_bots_by_race[race_id] = current
|
||||||
@@ -76,15 +76,16 @@ func _count_humans(race_id: int) -> int:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
func _spawn_bot(race_id: int) -> int:
|
func _spawn_bot(race_id: int, slot_index: int) -> int:
|
||||||
var bot_id := _next_bot_id
|
var bot_id := _next_bot_id
|
||||||
_next_bot_id -= 1
|
_next_bot_id -= 1
|
||||||
|
|
||||||
var race := _find_race(race_id)
|
var race := _find_race(race_id)
|
||||||
var ship_def: Dictionary = race.ships[0] # bots always fly the race's fighter (Interceptor)
|
var ship_def: Dictionary = _bot_ship_def(race, slot_index)
|
||||||
PlayerRegistry.register_bot(
|
PlayerRegistry.register_bot(
|
||||||
bot_id, BotNames.random_name(), race_id,
|
bot_id, BotNames.random_name(), race_id,
|
||||||
ship_def.path, ship_def.scale, ship_def.speed_factor, ship_def.role
|
ship_def.path, ship_def.scale, ship_def.speed_factor, ship_def.role,
|
||||||
|
ship_def.get("flame_path", ""), ship_def.get("sound_path", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
var ship_node := _world.spawn_peer(bot_id) as Ship
|
var ship_node := _world.spawn_peer(bot_id) as Ship
|
||||||
@@ -93,6 +94,27 @@ func _spawn_bot(race_id: int) -> int:
|
|||||||
return bot_id
|
return bot_id
|
||||||
|
|
||||||
|
|
||||||
|
# Bots used to always fly the race's Fighter (ships[0]); now the first-spawned
|
||||||
|
# bot on a team flies its Tank, the next two fly its Gunner, and every bot
|
||||||
|
# after that flies its Fighter -- so a fully bot-filled team always has at
|
||||||
|
# least 1 Tank + 2 Gunner "backbone" instead of reading as an all-Fighter
|
||||||
|
# swarm. Keyed off slot_index (this bot's position within its race's bot
|
||||||
|
# array, before it's appended -- see _reconcile_race()) rather than the
|
||||||
|
# team's current size, so growing/shrinking the roster never reshuffles an
|
||||||
|
# already-spawned bot's ship: _reconcile_race() only ever adds at the tail
|
||||||
|
# and removes from the tail (pop_back()), so the Tank/Gunner bots spawned
|
||||||
|
# into the earliest slots are always the last ones cut as humans join.
|
||||||
|
# Relies on team_select.gd's RACES ship order being [Fighter, Gunner, Tank]
|
||||||
|
# for every faction.
|
||||||
|
func _bot_ship_def(race: Dictionary, slot_index: int) -> Dictionary:
|
||||||
|
if slot_index == 0:
|
||||||
|
return race.ships[2] # Tank
|
||||||
|
elif slot_index <= 2:
|
||||||
|
return race.ships[1] # Gunner
|
||||||
|
else:
|
||||||
|
return race.ships[0] # Fighter
|
||||||
|
|
||||||
|
|
||||||
func _despawn_bot(bot_id: int) -> void:
|
func _despawn_bot(bot_id: int) -> void:
|
||||||
_world.despawn_peer(bot_id)
|
_world.despawn_peer(bot_id)
|
||||||
PlayerRegistry.unregister_bot(bot_id)
|
PlayerRegistry.unregister_bot(bot_id)
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
extends Label
|
||||||
|
class_name DamageNumber
|
||||||
|
|
||||||
|
# Floating "(amount)" combat text shown only to the attacker when their shot
|
||||||
|
# lands (see ships/ship_movement.gd's _notify_damage_dealt()) — purely
|
||||||
|
# cosmetic and spawned locally on whichever peer it's told to appear for,
|
||||||
|
# same "no networked node needed" approach as effects/explosion.gd.
|
||||||
|
var amount: int = 0
|
||||||
|
|
||||||
|
const BOX_SIZE := Vector2(80, 24)
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
text = "(%d)" % amount
|
||||||
|
add_theme_font_size_override("font_size", GameConfig.damage_number_font_size)
|
||||||
|
add_theme_color_override("font_color", GameConfig.damage_number_color)
|
||||||
|
add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.8))
|
||||||
|
add_theme_constant_override("outline_size", 3)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
custom_minimum_size = BOX_SIZE
|
||||||
|
size = BOX_SIZE
|
||||||
|
# global_position was set to the hit point by the spawner; center the box
|
||||||
|
# on it instead of growing down-right from it.
|
||||||
|
global_position -= BOX_SIZE / 2.0
|
||||||
|
|
||||||
|
var tween := create_tween()
|
||||||
|
tween.set_parallel(true)
|
||||||
|
tween.tween_property(self, "position:y", position.y - GameConfig.damage_number_rise_distance, GameConfig.damage_number_duration)
|
||||||
|
tween.tween_property(self, "modulate:a", 0.0, GameConfig.damage_number_duration)
|
||||||
|
tween.chain().tween_callback(queue_free)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cj8fpi6biykj1
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://effects/damage_number.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="DamageNumber" type="Label"]
|
||||||
|
script = ExtResource("1")
|
||||||
@@ -5,6 +5,14 @@ class_name Explosion
|
|||||||
# one shared knob for resizing every explosion in the game at once).
|
# one shared knob for resizing every explosion in the game at once).
|
||||||
@export var scale_override: float = 0.0
|
@export var scale_override: float = 0.0
|
||||||
|
|
||||||
|
# Ships no longer collide with each other (see ship.tscn's collision layers),
|
||||||
|
# but flying straight through an explosion's sprite still looked wrong, so
|
||||||
|
# GhostArea (a child Area2D, scaled along with this node) hides any ship's
|
||||||
|
# sprite while it overlaps and restores it on exit. Tracked here so a ship
|
||||||
|
# still inside when this explosion despawns gets un-ghosted rather than
|
||||||
|
# stuck invisible.
|
||||||
|
var _ghosted_ships: Array = []
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var s: float = scale_override if scale_override > 0.0 else GameConfig.explosion_scale
|
var s: float = scale_override if scale_override > 0.0 else GameConfig.explosion_scale
|
||||||
@@ -12,3 +20,25 @@ func _ready() -> void:
|
|||||||
sprite_frames.set_animation_speed("explode", GameConfig.explosion_fps)
|
sprite_frames.set_animation_speed("explode", GameConfig.explosion_fps)
|
||||||
play("explode")
|
play("explode")
|
||||||
animation_finished.connect(queue_free)
|
animation_finished.connect(queue_free)
|
||||||
|
|
||||||
|
var ghost_area := get_node("GhostArea") as Area2D
|
||||||
|
ghost_area.body_entered.connect(_on_body_entered)
|
||||||
|
ghost_area.body_exited.connect(_on_body_exited)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node) -> void:
|
||||||
|
if body.has_method("set_ghosted"):
|
||||||
|
body.set_ghosted(true)
|
||||||
|
_ghosted_ships.append(body)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_body_exited(body: Node) -> void:
|
||||||
|
if body.has_method("set_ghosted"):
|
||||||
|
body.set_ghosted(false)
|
||||||
|
_ghosted_ships.erase(body)
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
for ship in _ghosted_ships:
|
||||||
|
if is_instance_valid(ship):
|
||||||
|
ship.set_ghosted(false)
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
[ext_resource type="Texture2D" path="res://assets/images/effects/explosion/explosion_11.png" id="12_expl11"]
|
[ext_resource type="Texture2D" path="res://assets/images/effects/explosion/explosion_11.png" id="12_expl11"]
|
||||||
[ext_resource type="Texture2D" path="res://assets/images/effects/explosion/explosion_12.png" id="13_expl12"]
|
[ext_resource type="Texture2D" path="res://assets/images/effects/explosion/explosion_12.png" id="13_expl12"]
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id="CircleShape2D_expl01"]
|
||||||
|
radius = 45.0
|
||||||
|
|
||||||
[sub_resource type="SpriteFrames" id="SpriteFrames_expl01"]
|
[sub_resource type="SpriteFrames" id="SpriteFrames_expl01"]
|
||||||
animations = [{
|
animations = [{
|
||||||
"frames": [{
|
"frames": [{
|
||||||
@@ -63,3 +66,10 @@ sprite_frames = SubResource("SpriteFrames_expl01")
|
|||||||
animation = &"explode"
|
animation = &"explode"
|
||||||
autoplay = ""
|
autoplay = ""
|
||||||
script = ExtResource("1_expl01")
|
script = ExtResource("1_expl01")
|
||||||
|
|
||||||
|
[node name="GhostArea" type="Area2D" parent="."]
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 2
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="GhostArea"]
|
||||||
|
shape = SubResource("CircleShape2D_expl01")
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
extends CanvasLayer
|
extends CanvasLayer
|
||||||
|
|
||||||
# Blue "mirrored" energy bar, top-middle of the screen: the filled region is
|
# "Mirrored" stat bar, top-middle of the screen: the filled region is
|
||||||
# centered in the bar's full extent, so it shrinks toward the middle from
|
# centered in the bar's full extent, so it shrinks toward the middle from
|
||||||
# both edges as energy drains, instead of draining left-to-right like a
|
# both edges as the stat drains, instead of draining left-to-right like a
|
||||||
# normal bar. Driven by Ship._update_hud() calling set_fill() with the local
|
# normal bar. Driven by Ship._update_hud() calling set_fill() with a
|
||||||
# player's energy/max_energy fraction (see ships/ship_movement.gd).
|
# health/max_health or energy/max_energy fraction (see ships/ship_movement.gd).
|
||||||
|
# Reused for both the HealthBar and EnergyBar instances in world.tscn — fill
|
||||||
|
# color and vertical stacking position are per-instance overrides of the
|
||||||
|
# exports below (health above energy).
|
||||||
|
|
||||||
const BAR_WIDTH := 300.0
|
const BAR_WIDTH := 300.0
|
||||||
const BAR_HEIGHT := 14.0
|
const BAR_HEIGHT := 14.0
|
||||||
const MARGIN_TOP := 20.0
|
|
||||||
const COLOR_BG := Color(0.15, 0.18, 0.25, 0.6)
|
const COLOR_BG := Color(0.15, 0.18, 0.25, 0.6)
|
||||||
const COLOR_FILL := Color(0.25, 0.55, 1.0, 0.95)
|
|
||||||
|
@export var margin_top: float = 20.0
|
||||||
|
@export var fill_color: Color = Color(0.25, 0.55, 1.0, 0.95)
|
||||||
|
|
||||||
var _fill: ColorRect
|
var _fill: ColorRect
|
||||||
|
|
||||||
@@ -29,8 +33,8 @@ func _build_ui() -> void:
|
|||||||
container.anchor_bottom = 0.0
|
container.anchor_bottom = 0.0
|
||||||
container.offset_left = -BAR_WIDTH / 2.0
|
container.offset_left = -BAR_WIDTH / 2.0
|
||||||
container.offset_right = BAR_WIDTH / 2.0
|
container.offset_right = BAR_WIDTH / 2.0
|
||||||
container.offset_top = MARGIN_TOP
|
container.offset_top = margin_top
|
||||||
container.offset_bottom = MARGIN_TOP + BAR_HEIGHT
|
container.offset_bottom = margin_top + BAR_HEIGHT
|
||||||
container.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
container.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
add_child(container)
|
add_child(container)
|
||||||
|
|
||||||
@@ -44,7 +48,7 @@ func _build_ui() -> void:
|
|||||||
container.add_child(bg)
|
container.add_child(bg)
|
||||||
|
|
||||||
_fill = ColorRect.new()
|
_fill = ColorRect.new()
|
||||||
_fill.color = COLOR_FILL
|
_fill.color = fill_color
|
||||||
_fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
_fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
container.add_child(_fill)
|
container.add_child(_fill)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dx6kcvxxca00e
|
||||||
@@ -2,18 +2,15 @@ extends CanvasLayer
|
|||||||
|
|
||||||
# Top-left roster of every connected player (human + bot). White entries are
|
# Top-left roster of every connected player (human + bot). White entries are
|
||||||
# on the local player's team (same race — races double as teams, see
|
# on the local player's team (same race — races double as teams, see
|
||||||
# TeamSelect), yellow entries are the enemy team. Bots (negative peer_id, see
|
# TeamSelect), red entries are the enemy team. Bots (negative peer_id, see
|
||||||
# bots/bot_manager.gd) get a trailing " (b)". Rebuilt whenever
|
# bots/bot_manager.gd) get a trailing " (b)". Rebuilt whenever
|
||||||
# PlayerRegistry's roster changes rather than polled every frame.
|
# PlayerRegistry's roster changes rather than polled every frame.
|
||||||
|
|
||||||
const MARGIN_X := 16.0
|
const MARGIN_X := 16.0
|
||||||
const MARGIN_TOP := 90.0 # below HUD/HealthBar + HUD/EnergyBar, which occupy y 20-74
|
const MARGIN_TOP := 90.0
|
||||||
const WIDTH := 220.0
|
const WIDTH := 220.0
|
||||||
const HEIGHT := 260.0
|
const HEIGHT := 260.0
|
||||||
|
|
||||||
const TEAM_COLOR := Color(0.92, 0.95, 1.0)
|
|
||||||
const ENEMY_COLOR := Color(0.95, 0.85, 0.25)
|
|
||||||
|
|
||||||
var _list: RichTextLabel
|
var _list: RichTextLabel
|
||||||
|
|
||||||
|
|
||||||
@@ -85,7 +82,7 @@ func _refresh() -> void:
|
|||||||
for peer_id in peer_ids:
|
for peer_id in peer_ids:
|
||||||
var info: Dictionary = PlayerRegistry.players[peer_id]
|
var info: Dictionary = PlayerRegistry.players[peer_id]
|
||||||
var is_teammate: bool = info.get("race", 0) == local_race
|
var is_teammate: bool = info.get("race", 0) == local_race
|
||||||
var color := TEAM_COLOR if is_teammate else ENEMY_COLOR
|
var color := GameConfig.team_color if is_teammate else GameConfig.enemy_color
|
||||||
var name_text := _escape_bbcode(String(info.get("name", "")))
|
var name_text := _escape_bbcode(String(info.get("name", "")))
|
||||||
if peer_id < 0:
|
if peer_id < 0:
|
||||||
name_text += " (b)"
|
name_text += " (b)"
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
extends Control
|
|
||||||
|
|
||||||
# Simple left-to-right stat bar for the top-left HUD (HealthBar/EnergyBar in
|
|
||||||
# world.tscn's HUD node) — fill shrinks from the right as the stat depletes.
|
|
||||||
# Contrast hud/energy_bar.gd's top-middle bar, which drains from both edges
|
|
||||||
# toward the center instead.
|
|
||||||
|
|
||||||
@export var fill_color: Color = Color(0.3, 0.85, 0.35, 0.95)
|
|
||||||
@export var bg_color: Color = Color(0.12, 0.14, 0.18, 0.6)
|
|
||||||
|
|
||||||
var _fill: ColorRect
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
|
|
||||||
var bg := ColorRect.new()
|
|
||||||
bg.color = bg_color
|
|
||||||
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
||||||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
add_child(bg)
|
|
||||||
|
|
||||||
_fill = ColorRect.new()
|
|
||||||
_fill.color = fill_color
|
|
||||||
_fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
_fill.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
||||||
add_child(_fill)
|
|
||||||
set_fill(1.0)
|
|
||||||
|
|
||||||
|
|
||||||
# frac in [0, 1]; anchor_right (not an explicit pixel width) so the fill
|
|
||||||
# tracks this control's actual size regardless of window width.
|
|
||||||
func set_fill(frac: float) -> void:
|
|
||||||
if _fill == null:
|
|
||||||
return
|
|
||||||
_fill.anchor_left = 0.0
|
|
||||||
_fill.anchor_right = clampf(frac, 0.0, 1.0)
|
|
||||||
_fill.offset_left = 0.0
|
|
||||||
_fill.offset_right = 0.0
|
|
||||||
@@ -36,6 +36,7 @@ const NAV_GAP := 4.0
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_build_ui()
|
_build_ui()
|
||||||
|
MusicManager.play_menu_music()
|
||||||
NetworkManager.connection_succeeded.connect(_on_connected)
|
NetworkManager.connection_succeeded.connect(_on_connected)
|
||||||
NetworkManager.connection_failed.connect(_on_connect_failed)
|
NetworkManager.connection_failed.connect(_on_connect_failed)
|
||||||
MatchmakingClient.match_found.connect(_on_match_found)
|
MatchmakingClient.match_found.connect(_on_match_found)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ var _ping_probes: Array = [] # {udp: PacketPeerUDP, start_msec: int, done: bool
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_build_ui()
|
_build_ui()
|
||||||
|
MusicManager.play_menu_music()
|
||||||
NetworkManager.connection_succeeded.connect(_on_connected)
|
NetworkManager.connection_succeeded.connect(_on_connected)
|
||||||
NetworkManager.connection_failed.connect(_on_connect_failed)
|
NetworkManager.connection_failed.connect(_on_connect_failed)
|
||||||
_refresh_servers()
|
_refresh_servers()
|
||||||
|
|||||||
@@ -18,16 +18,19 @@ const RACES := [
|
|||||||
"name": "LANCET", "role": "Fighter",
|
"name": "LANCET", "role": "Fighter",
|
||||||
"desc": "A single, high-precision bolt of blue coherent light. Built to poke from range and vanish before anything can return fire — the sharpest shot in the fleet, if you can land it.",
|
"desc": "A single, high-precision bolt of blue coherent light. Built to poke from range and vanish before anything can return fire — the sharpest shot in the fleet, if you can land it.",
|
||||||
"path": "res://assets/images/ships/apex/lancet.png", "scale": 0.296, "speed_factor": SPEED_BY_ROLE.Fighter,
|
"path": "res://assets/images/ships/apex/lancet.png", "scale": 0.296, "speed_factor": SPEED_BY_ROLE.Fighter,
|
||||||
|
"sound_path": "res://assets/sound/ships/apex/apex_1.wav",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "PULSAR", "role": "Gunner",
|
"name": "PULSAR", "role": "Gunner",
|
||||||
"desc": "A wide, rapid-fire fan of low-damage green plasma pulses. Trades precision for volume — spray enough bolts and something connects.",
|
"desc": "A wide, rapid-fire fan of low-damage green plasma pulses. Trades precision for volume — spray enough bolts and something connects.",
|
||||||
"path": "res://assets/images/ships/apex/pulsar.png", "scale": 0.373, "speed_factor": SPEED_BY_ROLE.Gunner,
|
"path": "res://assets/images/ships/apex/pulsar.png", "scale": 0.373, "speed_factor": SPEED_BY_ROLE.Gunner,
|
||||||
|
"sound_path": "res://assets/sound/ships/apex/apex_1.wav",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "SOVEREIGN", "role": "Tank",
|
"name": "SOVEREIGN", "role": "Tank",
|
||||||
"desc": "Homing micro-missiles alongside dual heavy plasma batteries. Slow and telegraphed, but its lock-on missiles punish anyone who tries to just outrun it.",
|
"desc": "Homing micro-missiles alongside dual heavy plasma batteries. Slow and telegraphed, but its lock-on missiles punish anyone who tries to just outrun it.",
|
||||||
"path": "res://assets/images/ships/apex/sovereign.png", "scale": 0.348, "speed_factor": SPEED_BY_ROLE.Tank,
|
"path": "res://assets/images/ships/apex/sovereign.png", "scale": 0.348, "speed_factor": SPEED_BY_ROLE.Tank,
|
||||||
|
"sound_path": "res://assets/sound/ships/apex/apex_1.wav",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -39,16 +42,22 @@ const RACES := [
|
|||||||
"name": "PATRIOT", "role": "Fighter",
|
"name": "PATRIOT", "role": "Fighter",
|
||||||
"desc": "A single, hard-hitting high-caliber explosive shell. Standard-issue and dependable — a clean, punishing hit if you land it.",
|
"desc": "A single, hard-hitting high-caliber explosive shell. Standard-issue and dependable — a clean, punishing hit if you land it.",
|
||||||
"path": "res://assets/images/ships/isn/patriot.png", "scale": 0.504, "speed_factor": SPEED_BY_ROLE.Fighter,
|
"path": "res://assets/images/ships/isn/patriot.png", "scale": 0.504, "speed_factor": SPEED_BY_ROLE.Fighter,
|
||||||
|
"sound_path": "res://assets/sound/ships/isn/isn_1.wav",
|
||||||
|
"flame_path": "res://assets/images/ships/isn/patriot_flame.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "BARRAGE", "role": "Gunner",
|
"name": "BARRAGE", "role": "Gunner",
|
||||||
"desc": "A relentless quad-barrel spray of fast, low-damage light bullets. Built for sustained suppressing fire rather than any one killing blow.",
|
"desc": "A relentless quad-barrel spray of fast, low-damage light bullets. Built for sustained suppressing fire rather than any one killing blow.",
|
||||||
"path": "res://assets/images/ships/isn/barrage.png", "scale": 0.516, "speed_factor": SPEED_BY_ROLE.Gunner,
|
"path": "res://assets/images/ships/isn/barrage.png", "scale": 0.516, "speed_factor": SPEED_BY_ROLE.Gunner,
|
||||||
|
"sound_path": "res://assets/sound/ships/isn/isn_1.wav",
|
||||||
|
"flame_path": "res://assets/images/ships/isn/barrage_flame.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "BEHEMOTH", "role": "Tank",
|
"name": "BEHEMOTH", "role": "Tank",
|
||||||
"desc": "Heavy tracking torpedoes plus massive physical deck-guns. The navy's flagship-killer — slow to turn, but nothing stands in its way for long.",
|
"desc": "Heavy tracking torpedoes plus massive physical deck-guns. The navy's flagship-killer — slow to turn, but nothing stands in its way for long.",
|
||||||
"path": "res://assets/images/ships/isn/behemoth.png", "scale": 0.331, "speed_factor": SPEED_BY_ROLE.Tank,
|
"path": "res://assets/images/ships/isn/behemoth.png", "scale": 0.331, "speed_factor": SPEED_BY_ROLE.Tank,
|
||||||
|
"sound_path": "res://assets/sound/ships/isn/isn_1.wav",
|
||||||
|
"flame_path": "res://assets/images/ships/isn/behemoth_flame.png",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -60,16 +69,22 @@ const RACES := [
|
|||||||
"name": "RAIL-JACK", "role": "Fighter",
|
"name": "RAIL-JACK", "role": "Fighter",
|
||||||
"desc": "A single, heavy magnetic railgun slug. Repurposed mining hardware — one slug, but it hits like a dropped asteroid.",
|
"desc": "A single, heavy magnetic railgun slug. Repurposed mining hardware — one slug, but it hits like a dropped asteroid.",
|
||||||
"path": "res://assets/images/ships/orc/rail_jack.png", "scale": 0.492, "speed_factor": SPEED_BY_ROLE.Fighter,
|
"path": "res://assets/images/ships/orc/rail_jack.png", "scale": 0.492, "speed_factor": SPEED_BY_ROLE.Fighter,
|
||||||
|
"sound_path": "res://assets/sound/ships/orc/orc_1.wav",
|
||||||
|
"flame_path": "res://assets/images/ships/orc/rail_jack_flame.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "SCRAP-SPITTER", "role": "Gunner",
|
"name": "SCRAP-SPITTER", "role": "Gunner",
|
||||||
"desc": "A rotary scrap-cannon spraying rapid, low-damage metal shrapnel. Loud, dirty, and built to keep the pressure on.",
|
"desc": "A rotary scrap-cannon spraying rapid, low-damage metal shrapnel. Loud, dirty, and built to keep the pressure on.",
|
||||||
"path": "res://assets/images/ships/orc/scrap_spitter.png", "scale": 0.462, "speed_factor": SPEED_BY_ROLE.Gunner,
|
"path": "res://assets/images/ships/orc/scrap_spitter.png", "scale": 0.462, "speed_factor": SPEED_BY_ROLE.Gunner,
|
||||||
|
"sound_path": "res://assets/sound/ships/orc/orc_1.wav",
|
||||||
|
"flame_path": "res://assets/images/ships/orc/scrap_spitter_flame.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "IRON-CLAD", "role": "Tank",
|
"name": "IRON-CLAD", "role": "Tank",
|
||||||
"desc": "Unguided explosive rockets plus continuous vulcan machine-gun fire. Bolted-together and brutal — built to grind a fight down, not finesse it.",
|
"desc": "Unguided explosive rockets plus continuous vulcan machine-gun fire. Bolted-together and brutal — built to grind a fight down, not finesse it.",
|
||||||
"path": "res://assets/images/ships/orc/iron_clad.png", "scale": 0.495, "speed_factor": SPEED_BY_ROLE.Tank,
|
"path": "res://assets/images/ships/orc/iron_clad.png", "scale": 0.495, "speed_factor": SPEED_BY_ROLE.Tank,
|
||||||
|
"sound_path": "res://assets/sound/ships/orc/orc_1.wav",
|
||||||
|
"flame_path": "res://assets/images/ships/orc/iron_clad_flame.png",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -331,8 +346,14 @@ func _show_ship_selection() -> void:
|
|||||||
|
|
||||||
func _on_ship_chosen(ship: Dictionary) -> void:
|
func _on_ship_chosen(ship: Dictionary) -> void:
|
||||||
PlayerRegistry.submit_local_loadout(
|
PlayerRegistry.submit_local_loadout(
|
||||||
GameConfig.player_name, _chosen_race.id, ship.path, ship.scale, ship.speed_factor, ship.role
|
GameConfig.player_name, _chosen_race.id, ship.path, ship.scale, ship.speed_factor, ship.role,
|
||||||
|
ship.get("flame_path", ""), ship.get("sound_path", "")
|
||||||
)
|
)
|
||||||
|
# Only the mandatory pre-spawn pick should end the menu music — a
|
||||||
|
# mid-match reopen (pause menu SELECT TEAM) leaves it alone since it's
|
||||||
|
# already stopped by this point (see _reopenable).
|
||||||
|
if not _reopenable:
|
||||||
|
MusicManager.stop_menu_music()
|
||||||
_reopenable = true
|
_reopenable = true
|
||||||
_close()
|
_close()
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ MatchmakingClient="*res://autoload/matchmaking_client.gd"
|
|||||||
ChatManager="*res://autoload/chat_manager.gd"
|
ChatManager="*res://autoload/chat_manager.gd"
|
||||||
KillFeedManager="*res://autoload/kill_feed_manager.gd"
|
KillFeedManager="*res://autoload/kill_feed_manager.gd"
|
||||||
BotManager="*res://bots/bot_manager.gd"
|
BotManager="*res://bots/bot_manager.gd"
|
||||||
|
MusicManager="*res://autoload/music_manager.gd"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
window/size/viewport_width=1280
|
window/size/viewport_width=1280
|
||||||
window/size/viewport_height=800
|
window/size/viewport_height=800
|
||||||
window/size/mode=3
|
window/size/mode=3
|
||||||
window/stretch/mode="disabled"
|
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ extends Area2D
|
|||||||
# Set by World._spawn_bullet() before this node enters the tree.
|
# Set by World._spawn_bullet() before this node enters the tree.
|
||||||
var velocity: Vector2 = Vector2.ZERO
|
var velocity: Vector2 = Vector2.ZERO
|
||||||
var source_peer_id: int = 0
|
var source_peer_id: int = 0
|
||||||
|
var damage: int = 100
|
||||||
|
|
||||||
|
var _spawn_pos: Vector2
|
||||||
|
|
||||||
# Only the server detects hits and despawns bullets — that's replicated to
|
# Only the server detects hits and despawns bullets — that's replicated to
|
||||||
# every client automatically since bullets are spawned via BulletSpawner.
|
# every client automatically since bullets are spawned via BulletSpawner.
|
||||||
# Movement itself stays local on every peer (deterministic constant-velocity
|
# Movement itself stays local on every peer (deterministic constant-velocity
|
||||||
# simulation from the replicated spawn state, no further sync needed).
|
# simulation from the replicated spawn state, no further sync needed).
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_spawn_pos = global_position
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
body_entered.connect(_on_body_entered)
|
body_entered.connect(_on_body_entered)
|
||||||
|
|
||||||
@@ -18,10 +22,21 @@ func _process(delta: float) -> void:
|
|||||||
var bounds = GameConfig.world_bounds.grow(64.0)
|
var bounds = GameConfig.world_bounds.grow(64.0)
|
||||||
if not bounds.has_point(global_position):
|
if not bounds.has_point(global_position):
|
||||||
queue_free()
|
queue_free()
|
||||||
|
elif global_position.distance_to(_spawn_pos) > GameConfig.bullet_max_range:
|
||||||
|
queue_free()
|
||||||
|
|
||||||
func _on_body_entered(body: Node) -> void:
|
func _on_body_entered(body: Node) -> void:
|
||||||
if body.get("peer_id") == source_peer_id:
|
var target_peer_id = body.get("peer_id")
|
||||||
|
if target_peer_id == source_peer_id:
|
||||||
return
|
return
|
||||||
if body.has_method("take_damage"):
|
# Friendly fire is blocked, not ignored -- the bullet still stops dead on
|
||||||
body.take_damage(GameConfig.bullet_damage, source_peer_id)
|
# a teammate's hull (queue_free below), it just deals no damage. Only a
|
||||||
|
# real target (has take_damage, i.e. a Ship) is team-checked; hazards/
|
||||||
|
# walls have neither peer_id nor take_damage and fall straight through
|
||||||
|
# to queue_free(), same as before.
|
||||||
|
if target_peer_id != null and body.has_method("take_damage"):
|
||||||
|
var source_race: int = PlayerRegistry.get_info(source_peer_id).get("race", -1)
|
||||||
|
var target_race: int = PlayerRegistry.get_info(target_peer_id).get("race", -2)
|
||||||
|
if source_race != target_race:
|
||||||
|
body.take_damage(damage, source_peer_id, global_position)
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
radius = 4.0
|
radius = 4.0
|
||||||
|
|
||||||
[node name="Bullet" type="Area2D"]
|
[node name="Bullet" type="Area2D"]
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 3
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
[gd_scene format=3 uid="uid://dygtdlkvo6ofl"]
|
[gd_scene format=3 uid="uid://dygtdlkvo6ofl"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://c4kj3i73wjdcj" path="res://ships/ship_movement.gd" id="1_0e48y"]
|
[ext_resource type="Script" uid="uid://c4kj3i73wjdcj" path="res://ships/ship_movement.gd" id="1_0e48y"]
|
||||||
[ext_resource type="Texture2D" path="res://assets/images/ships/terran/warframe.png" id="2_epypp"]
|
|
||||||
|
|
||||||
[sub_resource type="CircleShape2D" id="CircleShape2D_2uxl3"]
|
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_2uxl3"]
|
||||||
|
radius = 20.0
|
||||||
|
height = 40.0
|
||||||
|
|
||||||
[node name="Player" type="CharacterBody2D" unique_id=1973248732]
|
[node name="Player" type="CharacterBody2D" unique_id=1973248732]
|
||||||
|
collision_layer = 2
|
||||||
|
collision_mask = 1
|
||||||
script = ExtResource("1_0e48y")
|
script = ExtResource("1_0e48y")
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=188418093]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=188418093]
|
||||||
shape = SubResource("CircleShape2D_2uxl3")
|
shape = SubResource("CapsuleShape2D_2uxl3")
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=2083904209]
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=2083904209]
|
||||||
scale = Vector2(0.229, 0.229)
|
scale = Vector2(0.229, 0.229)
|
||||||
texture = ExtResource("2_epypp")
|
|
||||||
|
|
||||||
[node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="." unique_id=1504818491]
|
[node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="." unique_id=1504818491]
|
||||||
|
|
||||||
@@ -22,3 +24,6 @@ current = false
|
|||||||
rotating = false
|
rotating = false
|
||||||
position_smoothing_enabled = true
|
position_smoothing_enabled = true
|
||||||
position_smoothing_speed = 8.0
|
position_smoothing_speed = 8.0
|
||||||
|
|
||||||
|
[node name="NamePlate" type="Label" parent="."]
|
||||||
|
top_level = true
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ extends CharacterBody2D
|
|||||||
class_name Ship
|
class_name Ship
|
||||||
|
|
||||||
const EXPLOSION_SCENE: PackedScene = preload("res://effects/explosion.tscn")
|
const EXPLOSION_SCENE: PackedScene = preload("res://effects/explosion.tscn")
|
||||||
|
const DAMAGE_NUMBER_SCENE: PackedScene = preload("res://effects/damage_number.tscn")
|
||||||
|
|
||||||
# Set by World._spawn_ship() before this node enters the tree.
|
# Set by World._spawn_ship() before this node enters the tree.
|
||||||
var peer_id: int = 0
|
var peer_id: int = 0
|
||||||
@@ -19,6 +20,7 @@ var _turn_hold_time: float = 0.0
|
|||||||
var health: int = 0
|
var health: int = 0
|
||||||
var energy: float = 0.0
|
var energy: float = 0.0
|
||||||
var max_energy: float = 100.0
|
var max_energy: float = 100.0
|
||||||
|
var _role: String = "Fighter"
|
||||||
var _dead: bool = false
|
var _dead: bool = false
|
||||||
|
|
||||||
# Last-hit attribution for the kill feed (see autoload/kill_feed_manager.gd).
|
# Last-hit attribution for the kill feed (see autoload/kill_feed_manager.gd).
|
||||||
@@ -30,9 +32,25 @@ var _invincible: bool = false
|
|||||||
var _invincible_timer: float = 0.0
|
var _invincible_timer: float = 0.0
|
||||||
var _ship_speed_factor: float = 1.0
|
var _ship_speed_factor: float = 1.0
|
||||||
|
|
||||||
|
# Idle (no-flame) vs engine-flame sprite swap — see _update_thrust_visual().
|
||||||
|
# _flame_texture stays null for ships with no flame_path (e.g. Apex), which
|
||||||
|
# just keeps _update_thrust_visual() from ever touching the sprite.
|
||||||
|
var _idle_texture: Texture2D
|
||||||
|
var _flame_texture: Texture2D
|
||||||
|
var _thrusting: bool = false
|
||||||
|
|
||||||
|
# Reference-counted so overlapping explosions (or a ship lingering across two
|
||||||
|
# blasts) can't have one explosion's exit re-reveal a ship another explosion
|
||||||
|
# is still covering -- see effects/explosion.gd's GhostArea.
|
||||||
|
var _ghost_count: int = 0
|
||||||
|
|
||||||
var _is_owner: bool = false
|
var _is_owner: bool = false
|
||||||
var _fixed_delta: float = 1.0 / 60.0
|
var _fixed_delta: float = 1.0 / 60.0
|
||||||
|
|
||||||
|
# Bottom-right nameplate — see _refresh_nameplate() and ship.tscn's
|
||||||
|
# top_level Label child.
|
||||||
|
var _nameplate: Label
|
||||||
|
|
||||||
# Owner-side prediction: inputs sent to the server but not yet acknowledged,
|
# Owner-side prediction: inputs sent to the server but not yet acknowledged,
|
||||||
# replayed on top of the server's authoritative state on correction.
|
# replayed on top of the server's authoritative state on correction.
|
||||||
const MAX_PENDING_INPUTS := 180
|
const MAX_PENDING_INPUTS := 180
|
||||||
@@ -59,6 +77,18 @@ func _ready() -> void:
|
|||||||
_is_owner = peer_id == multiplayer.get_unique_id()
|
_is_owner = peer_id == multiplayer.get_unique_id()
|
||||||
add_to_group("ships") # lets BotAI find nearby targets without a World reference
|
add_to_group("ships") # lets BotAI find nearby targets without a World reference
|
||||||
|
|
||||||
|
# top_level (set in ship.tscn) keeps this from inheriting the ship's own
|
||||||
|
# rotation, so the name always reads upright regardless of facing.
|
||||||
|
_nameplate = get_node_or_null("NamePlate") as Label
|
||||||
|
if _nameplate:
|
||||||
|
_nameplate.visible = false
|
||||||
|
_nameplate.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_nameplate.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_nameplate.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
_nameplate.add_theme_font_size_override("font_size", GameConfig.ship_nameplate_font_size)
|
||||||
|
_nameplate.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
|
||||||
|
_nameplate.add_theme_constant_override("outline_size", 3)
|
||||||
|
|
||||||
# Only ever claim the camera for the local owner. Whichever Camera2D
|
# Only ever claim the camera for the local owner. Whichever Camera2D
|
||||||
# enters the SceneTree first auto-claims the viewport's active camera
|
# enters the SceneTree first auto-claims the viewport's active camera
|
||||||
# regardless of its own `current` value (Godot's built-in "first camera
|
# regardless of its own `current` value (Godot's built-in "first camera
|
||||||
@@ -71,6 +101,7 @@ func _ready() -> void:
|
|||||||
if _is_owner:
|
if _is_owner:
|
||||||
var camera := get_node_or_null("Camera2D") as Camera2D
|
var camera := get_node_or_null("Camera2D") as Camera2D
|
||||||
if camera:
|
if camera:
|
||||||
|
camera.zoom = Vector2.ONE * GameConfig.camera_zoom
|
||||||
camera.call_deferred("make_current")
|
camera.call_deferred("make_current")
|
||||||
|
|
||||||
PlayerRegistry.loadout_updated.connect(_on_loadout_updated)
|
PlayerRegistry.loadout_updated.connect(_on_loadout_updated)
|
||||||
@@ -85,21 +116,76 @@ func _ready() -> void:
|
|||||||
func _on_loadout_updated(updated_peer_id: int) -> void:
|
func _on_loadout_updated(updated_peer_id: int) -> void:
|
||||||
if updated_peer_id == peer_id:
|
if updated_peer_id == peer_id:
|
||||||
_init_player(PlayerRegistry.get_info(peer_id))
|
_init_player(PlayerRegistry.get_info(peer_id))
|
||||||
|
elif updated_peer_id == PlayerRegistry.get_local_id():
|
||||||
|
# The local viewer's own race changed (mid-match team swap) -- every
|
||||||
|
# other ship's teammate/enemy color depends on that comparison too,
|
||||||
|
# not just this ship's own info.
|
||||||
|
_refresh_nameplate()
|
||||||
|
|
||||||
|
|
||||||
func _init_player(info: Dictionary) -> void:
|
func _init_player(info: Dictionary) -> void:
|
||||||
var sprite := get_node_or_null("Sprite2D")
|
var sprite := get_node_or_null("Sprite2D") as Sprite2D
|
||||||
var ship_path: String = info.get("ship_path", "")
|
var ship_path: String = info.get("ship_path", "")
|
||||||
if sprite and not ship_path.is_empty():
|
if sprite and not ship_path.is_empty():
|
||||||
sprite.texture = load(ship_path)
|
_idle_texture = load(ship_path)
|
||||||
sprite.scale = Vector2.ONE * info.get("ship_scale", 1.0) * GameConfig.ship_scale_factor
|
var flame_path: String = info.get("ship_flame_path", "")
|
||||||
|
_flame_texture = load(flame_path) if not flame_path.is_empty() else null
|
||||||
|
sprite.texture = _idle_texture
|
||||||
|
_thrusting = false
|
||||||
|
var total_scale: float = info.get("ship_scale", 1.0) * GameConfig.ship_scale_factor
|
||||||
|
sprite.scale = Vector2.ONE * total_scale
|
||||||
|
_update_collision_shape(_idle_texture, total_scale)
|
||||||
_ship_speed_factor = info.get("ship_speed_factor", 1.0)
|
_ship_speed_factor = info.get("ship_speed_factor", 1.0)
|
||||||
max_energy = GameConfig.ship_max_energy_by_role.get(info.get("role", "Fighter"), 100.0)
|
_role = info.get("role", "Fighter")
|
||||||
|
max_energy = GameConfig.ship_max_energy_by_role.get(_role, 100.0)
|
||||||
|
_refresh_nameplate()
|
||||||
set_physics_process(true)
|
set_physics_process(true)
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
_server_respawn()
|
_server_respawn()
|
||||||
|
|
||||||
|
|
||||||
|
# Bottom-right-of-ship name label — text/color only change on a loadout
|
||||||
|
# change (this ship's or the local viewer's own, see _on_loadout_updated()),
|
||||||
|
# not every frame; position/visibility instead track the ship every physics
|
||||||
|
# tick (see _physics_process()) since those change constantly.
|
||||||
|
func _refresh_nameplate() -> void:
|
||||||
|
if _nameplate == null:
|
||||||
|
return
|
||||||
|
var info := PlayerRegistry.get_info(peer_id)
|
||||||
|
var name_text: String = info.get("name", "")
|
||||||
|
if peer_id < 0:
|
||||||
|
name_text += " (b)" # bots -- see bots/bot_manager.gd's negative peer_id trick
|
||||||
|
_nameplate.text = name_text
|
||||||
|
var local_race: int = PlayerRegistry.get_info(PlayerRegistry.get_local_id()).get("race", 0)
|
||||||
|
var is_teammate: bool = info.get("race", 0) == local_race
|
||||||
|
_nameplate.add_theme_color_override("font_color", GameConfig.team_color if is_teammate else GameConfig.enemy_color)
|
||||||
|
|
||||||
|
|
||||||
|
# Sizes the hitbox to the actual sprite being flown, in the same scale-space
|
||||||
|
# as the visual (see call site above) — every ship previously shared one
|
||||||
|
# fixed-radius CircleShape2D (Godot's 10px default) regardless of its actual
|
||||||
|
# on-screen size, which is a fraction of any ship's real silhouette (ships
|
||||||
|
# render 40-110px tall depending on role). That left the nose and outer
|
||||||
|
# edges of every ship un-collidable, since only a tiny circle near the
|
||||||
|
# center actually registered hits. A CapsuleShape2D tracks a ship's own
|
||||||
|
# rotation for free (it's a child of this CharacterBody2D) and its
|
||||||
|
# height-along-local-Y shape matches this project's nose-up-at-rotation-0
|
||||||
|
# sprite convention (ship_movement.gd's Vector2.UP-based movement) far
|
||||||
|
# better than a circle for the elongated Fighter/Gunner hulls. Shrunk by
|
||||||
|
# GameConfig.ship_hitbox_scale off the full texture bounds since the source
|
||||||
|
# art has some anti-aliased/soft-edge margin outside the solid hull.
|
||||||
|
func _update_collision_shape(texture: Texture2D, total_scale: float) -> void:
|
||||||
|
var collision := get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||||
|
if collision == null:
|
||||||
|
return
|
||||||
|
var capsule := collision.shape as CapsuleShape2D
|
||||||
|
if capsule == null:
|
||||||
|
return
|
||||||
|
var size := texture.get_size() * total_scale * GameConfig.ship_hitbox_scale
|
||||||
|
capsule.radius = size.x / 2.0
|
||||||
|
capsule.height = maxf(size.y, size.x)
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
if _dead:
|
if _dead:
|
||||||
return
|
return
|
||||||
@@ -120,11 +206,44 @@ func _physics_process(delta: float) -> void:
|
|||||||
else:
|
else:
|
||||||
_remote_tick(delta)
|
_remote_tick(delta)
|
||||||
|
|
||||||
|
if _nameplate:
|
||||||
|
_nameplate.visible = visible
|
||||||
|
_nameplate.global_position = global_position + GameConfig.ship_nameplate_offset
|
||||||
|
|
||||||
|
|
||||||
|
# Swaps the sprite between idle/flame textures based on whether the up/down
|
||||||
|
# thrust key is *currently held* — not current speed, since this game's
|
||||||
|
# movement has no drag and a released ship keeps drifting at speed, which
|
||||||
|
# would otherwise leave the flame on long after letting off the gas. The
|
||||||
|
# owner predicts this locally the instant it samples input (_owner_tick);
|
||||||
|
# the server does the same for bots/itself (_server_tick) and broadcasts its
|
||||||
|
# authoritative value in _receive_state so every other peer's view of a ship
|
||||||
|
# (which never sees raw input, only replicated state) stays in sync too.
|
||||||
|
func _set_thrust_sprite(active: bool) -> void:
|
||||||
|
if _flame_texture == null or active == _thrusting:
|
||||||
|
return
|
||||||
|
_thrusting = active
|
||||||
|
var sprite := get_node_or_null("Sprite2D") as Sprite2D
|
||||||
|
if sprite:
|
||||||
|
sprite.texture = _flame_texture if active else _idle_texture
|
||||||
|
|
||||||
|
|
||||||
|
# Purely local/visual -- called by any Explosion whose GhostArea this ship's
|
||||||
|
# (layer-2) hull overlaps, on every peer independently. Never touches the
|
||||||
|
# networked `visible` property (that's server-authoritative via
|
||||||
|
# _receive_state/_apply_death/_apply_respawn), just the sprite underneath it.
|
||||||
|
func set_ghosted(active: bool) -> void:
|
||||||
|
_ghost_count = maxi(0, _ghost_count + (1 if active else -1))
|
||||||
|
var sprite := get_node_or_null("Sprite2D") as Sprite2D
|
||||||
|
if sprite:
|
||||||
|
sprite.visible = _ghost_count == 0
|
||||||
|
|
||||||
|
|
||||||
# ── Owner client: predict locally, send input to server ─────────────────────
|
# ── Owner client: predict locally, send input to server ─────────────────────
|
||||||
|
|
||||||
func _owner_tick(delta: float) -> void:
|
func _owner_tick(delta: float) -> void:
|
||||||
var input := _sample_local_input()
|
var input := _sample_local_input()
|
||||||
|
_set_thrust_sprite(input.get("up", false) or input.get("down", false))
|
||||||
_input_tick += 1
|
_input_tick += 1
|
||||||
_simulate_step(delta, input, true)
|
_simulate_step(delta, input, true)
|
||||||
|
|
||||||
@@ -135,7 +254,7 @@ func _owner_tick(delta: float) -> void:
|
|||||||
|
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "unreliable")
|
@rpc("authority", "call_remote", "unreliable")
|
||||||
func _receive_state(tick: int, pos: Vector2, rot: float, vel: Vector2, hp: int, nrg: float, shown: bool) -> void:
|
func _receive_state(tick: int, pos: Vector2, rot: float, vel: Vector2, hp: int, nrg: float, shown: bool, thrusting: bool) -> void:
|
||||||
# Piggyback current health/energy AND visibility on every state broadcast
|
# Piggyback current health/energy AND visibility on every state broadcast
|
||||||
# (not just the one-shot _apply_health/_apply_death/_apply_respawn RPCs)
|
# (not just the one-shot _apply_health/_apply_death/_apply_respawn RPCs)
|
||||||
# so a peer that connects after another ship already spawned/respawned
|
# so a peer that connects after another ship already spawned/respawned
|
||||||
@@ -158,6 +277,7 @@ func _receive_state(tick: int, pos: Vector2, rot: float, vel: Vector2, hp: int,
|
|||||||
_interp_to_rot = rot
|
_interp_to_rot = rot
|
||||||
_interp_elapsed = 0.0
|
_interp_elapsed = 0.0
|
||||||
velocity = vel
|
velocity = vel
|
||||||
|
_set_thrust_sprite(thrusting)
|
||||||
|
|
||||||
|
|
||||||
func _reconcile(server_tick: int, server_pos: Vector2, server_rot: float, server_vel: Vector2) -> void:
|
func _reconcile(server_tick: int, server_pos: Vector2, server_rot: float, server_vel: Vector2) -> void:
|
||||||
@@ -195,11 +315,17 @@ func _server_tick(delta: float) -> void:
|
|||||||
_last_processed_tick = entry.tick
|
_last_processed_tick = entry.tick
|
||||||
input = _last_remote_input
|
input = _last_remote_input
|
||||||
|
|
||||||
|
# Updates this server's own local view of the sprite too (matters for a
|
||||||
|
# listen-server that's also rendering, e.g. hosting-and-playing) — for
|
||||||
|
# every other connected peer, _receive_state below is what carries this.
|
||||||
|
var thrusting: bool = input.get("up", false) or input.get("down", false)
|
||||||
|
_set_thrust_sprite(thrusting)
|
||||||
|
|
||||||
_simulate_step(delta, input, true)
|
_simulate_step(delta, input, true)
|
||||||
energy = minf(max_energy, energy + GameConfig.ship_energy_regen_rate * delta)
|
energy = minf(max_energy, energy + GameConfig.ship_energy_regen_rate * delta)
|
||||||
_server_process_shoot(delta, input)
|
_server_process_shoot(delta, input)
|
||||||
|
|
||||||
_receive_state.rpc(_last_processed_tick, global_position, rotation, velocity, health, energy, visible)
|
_receive_state.rpc(_last_processed_tick, global_position, rotation, velocity, health, energy, visible, thrusting)
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_remote", "unreliable")
|
@rpc("any_peer", "call_remote", "unreliable")
|
||||||
@@ -301,24 +427,32 @@ func _apply_collision_bounce(previous_velocity: Vector2, apply_side_effects: boo
|
|||||||
|
|
||||||
# Server-only: decides whether this tick's queued input fires a shot, using
|
# Server-only: decides whether this tick's queued input fires a shot, using
|
||||||
# its own authoritative cooldown (never trust a client's fire rate). Requests
|
# its own authoritative cooldown (never trust a client's fire rate). Requests
|
||||||
# World spawn the bullet, which replicates it to every peer.
|
# World spawn the bullet(s), which replicate to every peer. Damage/energy
|
||||||
|
# cost/bullet count all scale by role (Fighter fires 1 bullet, Gunner/Tank
|
||||||
|
# fire a side-by-side pair — see GameConfig.ship_bullet_count_by_role).
|
||||||
func _server_process_shoot(delta: float, input: Dictionary) -> void:
|
func _server_process_shoot(delta: float, input: Dictionary) -> void:
|
||||||
_fire_cooldown -= delta
|
_fire_cooldown -= delta
|
||||||
if input.get("shoot", false) and _fire_cooldown <= 0.0 and energy >= GameConfig.bullet_energy_cost:
|
var cost: float = GameConfig.ship_bullet_energy_cost_by_role.get(_role, 75.0)
|
||||||
|
if input.get("shoot", false) and _fire_cooldown <= 0.0 and energy >= cost:
|
||||||
_fire_cooldown = GameConfig.ship_fire_rate
|
_fire_cooldown = GameConfig.ship_fire_rate
|
||||||
energy -= GameConfig.bullet_energy_cost
|
energy -= cost
|
||||||
var world := get_node_or_null("/root/World")
|
var world := get_node_or_null("/root/World")
|
||||||
if world:
|
if world:
|
||||||
world.request_bullet_spawn(
|
var damage: int = GameConfig.ship_bullet_damage_by_role.get(_role, 100)
|
||||||
peer_id,
|
var count: int = GameConfig.ship_bullet_count_by_role.get(_role, 1)
|
||||||
global_position + Vector2.UP.rotated(rotation) * 40.0,
|
var forward := Vector2.UP.rotated(rotation)
|
||||||
Vector2.UP.rotated(rotation) * GameConfig.bullet_speed + velocity
|
var right := Vector2.RIGHT.rotated(rotation)
|
||||||
)
|
var base_pos := global_position + forward * 40.0
|
||||||
|
var bullet_vel := forward * GameConfig.bullet_speed + velocity
|
||||||
|
var spacing := GameConfig.bullet_side_spacing
|
||||||
|
var start := -spacing * (count - 1) / 2.0
|
||||||
|
for i in count:
|
||||||
|
world.request_bullet_spawn(peer_id, base_pos + right * (start + i * spacing), bullet_vel, damage)
|
||||||
|
|
||||||
|
|
||||||
# ── Health / death — server-authoritative, broadcast to every peer ──────────
|
# ── Health / death — server-authoritative, broadcast to every peer ──────────
|
||||||
|
|
||||||
func take_damage(amount: int, source_peer_id: int = 0) -> void:
|
func take_damage(amount: int, source_peer_id: int = 0, hit_pos: Vector2 = Vector2.ZERO) -> void:
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
return
|
return
|
||||||
if _invincible or _dead:
|
if _invincible or _dead:
|
||||||
@@ -326,6 +460,7 @@ func take_damage(amount: int, source_peer_id: int = 0) -> void:
|
|||||||
_last_damage_source = source_peer_id
|
_last_damage_source = source_peer_id
|
||||||
health = max(0, health - amount)
|
health = max(0, health - amount)
|
||||||
_apply_health.rpc(health)
|
_apply_health.rpc(health)
|
||||||
|
_notify_damage_dealt(source_peer_id, amount, hit_pos if hit_pos != Vector2.ZERO else global_position)
|
||||||
if health == 0:
|
if health == 0:
|
||||||
_die()
|
_die()
|
||||||
|
|
||||||
@@ -336,6 +471,34 @@ func _apply_health(new_health: int) -> void:
|
|||||||
_update_hud()
|
_update_hud()
|
||||||
|
|
||||||
|
|
||||||
|
# Floating damage-number feedback for the attacker only -- unlike the kill
|
||||||
|
# feed (autoload/kill_feed_manager.gd), this is per-shooter combat info, not
|
||||||
|
# match-wide, so it's a targeted rpc_id() rather than a broadcast .rpc().
|
||||||
|
# Bots (negative peer_id) and hazard/wall damage (peer_id 0, see
|
||||||
|
# _apply_collision_bounce()) have no client to show this to. The server's own
|
||||||
|
# peer id (when hosting-and-playing) takes the direct local-call path instead
|
||||||
|
# of rpc_id()'ing itself, same pattern as world.gd's decide_offered_races().
|
||||||
|
func _notify_damage_dealt(source_peer_id: int, amount: int, pos: Vector2) -> void:
|
||||||
|
if source_peer_id <= 0:
|
||||||
|
return
|
||||||
|
if source_peer_id == multiplayer.get_unique_id():
|
||||||
|
_spawn_damage_number(pos, amount)
|
||||||
|
else:
|
||||||
|
_show_damage_number.rpc_id(source_peer_id, amount, pos)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable")
|
||||||
|
func _show_damage_number(amount: int, pos: Vector2) -> void:
|
||||||
|
_spawn_damage_number(pos, amount)
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_damage_number(pos: Vector2, amount: int) -> void:
|
||||||
|
var number := DAMAGE_NUMBER_SCENE.instantiate()
|
||||||
|
number.global_position = pos
|
||||||
|
number.amount = amount
|
||||||
|
get_parent().add_child(number)
|
||||||
|
|
||||||
|
|
||||||
func _die() -> void:
|
func _die() -> void:
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
return
|
return
|
||||||
@@ -351,15 +514,37 @@ func _apply_death() -> void:
|
|||||||
visible = false
|
visible = false
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
_spawn_explosion()
|
_spawn_explosion()
|
||||||
|
# Otherwise the invisible corpse -- still fully solid for the whole
|
||||||
|
# ship_respawn_delay window -- keeps blocking bullets fired "through" the
|
||||||
|
# explosion at its position, even though live ships already ghost through
|
||||||
|
# it visually (see effects/explosion.gd's GhostArea).
|
||||||
|
_set_collision_enabled(false)
|
||||||
|
# _physics_process()'s nameplate sync never runs while _dead, so hide it
|
||||||
|
# here explicitly instead of leaving it stuck at the death position.
|
||||||
|
if _nameplate:
|
||||||
|
_nameplate.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
# Deferred since _apply_death/_apply_respawn can run from inside a bullet's
|
||||||
|
# body_entered physics callback -- same "Can't change state while flushing
|
||||||
|
# queries" constraint _spawn_explosion's call_deferred works around above.
|
||||||
|
func _set_collision_enabled(enabled: bool) -> void:
|
||||||
|
var collision := get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||||
|
if collision:
|
||||||
|
collision.set_deferred("disabled", not enabled)
|
||||||
|
|
||||||
|
|
||||||
# Purely cosmetic, so it's spawned locally by every peer off the already
|
# Purely cosmetic, so it's spawned locally by every peer off the already
|
||||||
# broadcast _apply_death RPC above rather than needing its own networked
|
# broadcast _apply_death RPC above rather than needing its own networked
|
||||||
# spawner (contrast bullets/ships, which are gameplay-authoritative).
|
# spawner (contrast bullets/ships, which are gameplay-authoritative).
|
||||||
|
# call_deferred here since _apply_death (and this) can run from inside a
|
||||||
|
# bullet's body_entered physics callback -- adding a node with its own
|
||||||
|
# collider (the explosion's GhostArea) synchronously mid-physics-step trips
|
||||||
|
# Godot's "Can't change state while flushing queries" error.
|
||||||
func _spawn_explosion() -> void:
|
func _spawn_explosion() -> void:
|
||||||
var explosion := EXPLOSION_SCENE.instantiate()
|
var explosion := EXPLOSION_SCENE.instantiate()
|
||||||
explosion.global_position = global_position
|
explosion.global_position = global_position
|
||||||
get_parent().add_child(explosion)
|
get_parent().call_deferred("add_child", explosion)
|
||||||
|
|
||||||
|
|
||||||
func _server_respawn() -> void:
|
func _server_respawn() -> void:
|
||||||
@@ -376,6 +561,7 @@ func _apply_respawn(pos: Vector2) -> void:
|
|||||||
_dead = false
|
_dead = false
|
||||||
_last_damage_source = 0
|
_last_damage_source = 0
|
||||||
visible = true
|
visible = true
|
||||||
|
_set_collision_enabled(true)
|
||||||
_invincible = true
|
_invincible = true
|
||||||
_invincible_timer = GameConfig.ship_invincibility_time
|
_invincible_timer = GameConfig.ship_invincibility_time
|
||||||
|
|
||||||
@@ -418,12 +604,9 @@ func _team_spawn_group() -> String:
|
|||||||
func _update_hud() -> void:
|
func _update_hud() -> void:
|
||||||
if not _is_owner:
|
if not _is_owner:
|
||||||
return
|
return
|
||||||
var health_bar = get_node_or_null("/root/World/HUD/HealthBar")
|
var health_bar = get_node_or_null("/root/World/HealthBar")
|
||||||
if health_bar and health_bar.has_method("set_fill"):
|
if health_bar and health_bar.has_method("set_fill"):
|
||||||
health_bar.set_fill(float(health) / GameConfig.ship_max_health if GameConfig.ship_max_health > 0 else 0.0)
|
health_bar.set_fill(float(health) / GameConfig.ship_max_health if GameConfig.ship_max_health > 0 else 0.0)
|
||||||
var hud_energy_bar = get_node_or_null("/root/World/HUD/EnergyBar")
|
|
||||||
if hud_energy_bar and hud_energy_bar.has_method("set_fill"):
|
|
||||||
hud_energy_bar.set_fill(energy / max_energy if max_energy > 0.0 else 0.0)
|
|
||||||
var energy_bar = get_node_or_null("/root/World/EnergyBar")
|
var energy_bar = get_node_or_null("/root/World/EnergyBar")
|
||||||
if energy_bar and energy_bar.has_method("set_fill"):
|
if energy_bar and energy_bar.has_method("set_fill"):
|
||||||
energy_bar.set_fill(energy / max_energy if max_energy > 0.0 else 0.0)
|
energy_bar.set_fill(energy / max_energy if max_energy > 0.0 else 0.0)
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ func spawn_peer(peer_id: int) -> Node:
|
|||||||
func despawn_peer(peer_id: int) -> void:
|
func despawn_peer(peer_id: int) -> void:
|
||||||
var ship := _players.get_node_or_null(str(peer_id))
|
var ship := _players.get_node_or_null(str(peer_id))
|
||||||
if ship:
|
if ship:
|
||||||
|
# Stops _server_tick() from emitting any further _receive_state
|
||||||
|
# broadcasts for this ship the instant despawn is requested — without
|
||||||
|
# this, a state packet already in flight on the unreliable channel can
|
||||||
|
# still reach a client after that client has already processed this
|
||||||
|
# node's despawn replication, causing a "Node not found" RPC error.
|
||||||
|
ship.set_physics_process(false)
|
||||||
ship.queue_free()
|
ship.queue_free()
|
||||||
|
|
||||||
|
|
||||||
@@ -90,10 +96,12 @@ func _spawn_ship(peer_id: int) -> Node:
|
|||||||
|
|
||||||
# Called by a Ship node running on the server when its authoritative fire
|
# Called by a Ship node running on the server when its authoritative fire
|
||||||
# cooldown allows a shot. Replicated to every peer via the bullet spawner.
|
# cooldown allows a shot. Replicated to every peer via the bullet spawner.
|
||||||
func request_bullet_spawn(source_peer_id: int, pos: Vector2, vel: Vector2) -> void:
|
# damage is decided by the firing ship's role (see
|
||||||
|
# ship_movement.gd's _server_process_shoot()), not a flat global.
|
||||||
|
func request_bullet_spawn(source_peer_id: int, pos: Vector2, vel: Vector2, damage: int) -> void:
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
return
|
return
|
||||||
_bullet_spawner.spawn({"peer_id": source_peer_id, "pos": pos, "vel": vel})
|
_bullet_spawner.spawn({"peer_id": source_peer_id, "pos": pos, "vel": vel, "damage": damage})
|
||||||
|
|
||||||
|
|
||||||
func _spawn_bullet(data: Dictionary) -> Node:
|
func _spawn_bullet(data: Dictionary) -> Node:
|
||||||
@@ -101,9 +109,30 @@ func _spawn_bullet(data: Dictionary) -> Node:
|
|||||||
bullet.global_position = data.pos
|
bullet.global_position = data.pos
|
||||||
bullet.velocity = data.vel
|
bullet.velocity = data.vel
|
||||||
bullet.source_peer_id = data.peer_id
|
bullet.source_peer_id = data.peer_id
|
||||||
|
bullet.damage = data.damage
|
||||||
|
_play_shoot_sound(data.peer_id, data.pos)
|
||||||
return bullet
|
return bullet
|
||||||
|
|
||||||
|
|
||||||
|
# Runs once per peer (this spawn_function itself is what MultiplayerSpawner
|
||||||
|
# replicates, so every client fires this locally, not just the server) —
|
||||||
|
# each peer's own ship's Camera2D is that peer's audio listener, so
|
||||||
|
# AudioStreamPlayer2D's max_distance naturally limits who hears the shot to
|
||||||
|
# ships within GameConfig.ship_shoot_sound_max_distance, with no need to
|
||||||
|
# track which peers are "in range" server-side.
|
||||||
|
func _play_shoot_sound(source_peer_id: int, pos: Vector2) -> void:
|
||||||
|
var sound_path: String = PlayerRegistry.get_info(source_peer_id).get("ship_sound_path", "")
|
||||||
|
if sound_path.is_empty():
|
||||||
|
return
|
||||||
|
var player := AudioStreamPlayer2D.new()
|
||||||
|
player.stream = load(sound_path)
|
||||||
|
player.global_position = pos
|
||||||
|
player.max_distance = GameConfig.ship_shoot_sound_max_distance
|
||||||
|
player.finished.connect(player.queue_free)
|
||||||
|
add_child(player)
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
|
||||||
func _on_peer_joined(peer_id: int) -> void:
|
func _on_peer_joined(peer_id: int) -> void:
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
spawn_peer(peer_id)
|
spawn_peer(peer_id)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
[ext_resource type="PackedScene" uid="uid://cminimap0001" path="res://hud/mini_map.tscn" id="8_mmap"]
|
[ext_resource type="PackedScene" uid="uid://cminimap0001" path="res://hud/mini_map.tscn" id="8_mmap"]
|
||||||
[ext_resource type="PackedScene" path="res://hud/ping_display.tscn" id="9_ping"]
|
[ext_resource type="PackedScene" path="res://hud/ping_display.tscn" id="9_ping"]
|
||||||
[ext_resource type="PackedScene" path="res://hud/energy_bar.tscn" id="10_ebar"]
|
[ext_resource type="PackedScene" path="res://hud/energy_bar.tscn" id="10_ebar"]
|
||||||
[ext_resource type="Script" path="res://hud/stat_bar.gd" id="11_statbar"]
|
|
||||||
|
|
||||||
[node name="World" type="Node2D" unique_id=1962020789]
|
[node name="World" type="Node2D" unique_id=1962020789]
|
||||||
script = ExtResource("4_world")
|
script = ExtResource("4_world")
|
||||||
@@ -26,24 +25,6 @@ spawn_path = NodePath("../Players")
|
|||||||
[node name="BulletSpawner" type="MultiplayerSpawner" parent="."]
|
[node name="BulletSpawner" type="MultiplayerSpawner" parent="."]
|
||||||
spawn_path = NodePath("../Bullets")
|
spawn_path = NodePath("../Bullets")
|
||||||
|
|
||||||
[node name="HUD" type="CanvasLayer" parent="."]
|
|
||||||
|
|
||||||
[node name="HealthBar" type="Control" parent="HUD"]
|
|
||||||
offset_left = 20.0
|
|
||||||
offset_top = 20.0
|
|
||||||
offset_right = 300.0
|
|
||||||
offset_bottom = 44.0
|
|
||||||
script = ExtResource("11_statbar")
|
|
||||||
fill_color = Color(0.3, 0.85, 0.35, 0.95)
|
|
||||||
|
|
||||||
[node name="EnergyBar" type="Control" parent="HUD"]
|
|
||||||
offset_left = 20.0
|
|
||||||
offset_top = 50.0
|
|
||||||
offset_right = 300.0
|
|
||||||
offset_bottom = 74.0
|
|
||||||
script = ExtResource("11_statbar")
|
|
||||||
fill_color = Color(0.25, 0.55, 1.0, 0.95)
|
|
||||||
|
|
||||||
[node name="PauseMenu" parent="." instance=ExtResource("2_pm")]
|
[node name="PauseMenu" parent="." instance=ExtResource("2_pm")]
|
||||||
|
|
||||||
[node name="TeamSelect" parent="." instance=ExtResource("3_ts")]
|
[node name="TeamSelect" parent="." instance=ExtResource("3_ts")]
|
||||||
@@ -58,4 +39,10 @@ fill_color = Color(0.25, 0.55, 1.0, 0.95)
|
|||||||
|
|
||||||
[node name="PingDisplay" parent="." instance=ExtResource("9_ping")]
|
[node name="PingDisplay" parent="." instance=ExtResource("9_ping")]
|
||||||
|
|
||||||
|
[node name="HealthBar" parent="." instance=ExtResource("10_ebar")]
|
||||||
|
margin_top = 20.0
|
||||||
|
fill_color = Color(0.3, 0.85, 0.35, 0.95)
|
||||||
|
|
||||||
[node name="EnergyBar" parent="." instance=ExtResource("10_ebar")]
|
[node name="EnergyBar" parent="." instance=ExtResource("10_ebar")]
|
||||||
|
margin_top = 40.0
|
||||||
|
fill_color = Color(0.25, 0.55, 1.0, 0.95)
|
||||||
|
|||||||