c0b5f421c5
- In-game chat: T=all, Y=team (race doubles as team), server-relayed and team-filtered via new ChatManager autoload; last 10 messages shown bottom-left (chat/chat_box.gd). - Bot fill for casual (bots/): keeps each of the match's 2 offered races at a minimum of 7 total humans+bots, spawning/despawning reactively as players join/leave. Bots always fly their race's fighter and use negative peer_ids so they ride the existing networked-ship stack (spawning, loadout sync, health/position sync, bullet attribution) with no special-casing. Casual matchmaking now forms with just 1 real player queued instead of waiting for a second (matchmaking-api/). - Borderless fullscreen with stretch scaling disabled instead of scaling the Steam-Deck-matched 1280x800 canvas up to fill PC monitors (which read as zoomed in) — PC monitors now reveal more world/HUD at native size instead. Reworked main_menu/team_select/chat_box to position via anchors relative to the real window instead of hardcoded coordinates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
145 lines
5.0 KiB
GDScript
145 lines
5.0 KiB
GDScript
extends Node2D
|
|
|
|
const MAP_SCENE: PackedScene = preload("res://world/maps/map_01.tscn")
|
|
const SHIP_SCENE: PackedScene = preload("res://ships/ship.tscn")
|
|
const BULLET_SCENE: PackedScene = preload("res://ships/bullet.tscn")
|
|
|
|
@onready var _map_container: Node2D = $MapContainer
|
|
@onready var _players: Node2D = $Players
|
|
@onready var _spawner: MultiplayerSpawner = $MultiplayerSpawner
|
|
@onready var _bullets: Node2D = $Bullets
|
|
@onready var _bullet_spawner: MultiplayerSpawner = $BulletSpawner
|
|
@onready var _team_select: TeamSelect = $TeamSelect
|
|
|
|
# Server-authoritative: the 2 races offered for this match, decided once and
|
|
# shared by every player — see TeamSelect._ready(), which either reads this
|
|
# directly (server's own instance) or requests it over RPC (remote clients).
|
|
var _offered_race_ids: Array = []
|
|
|
|
|
|
func _ready() -> void:
|
|
var map := MAP_SCENE.instantiate()
|
|
_map_container.add_child(map)
|
|
var background := map.get_node_or_null("Background") as TextureRect
|
|
if background:
|
|
GameConfig.world_bounds = Rect2(background.position, background.size)
|
|
_build_tile_collisions(map)
|
|
|
|
_spawner.spawn_function = _spawn_ship
|
|
_bullet_spawner.spawn_function = _spawn_bullet
|
|
NetworkManager.peer_joined.connect(_on_peer_joined)
|
|
NetworkManager.peer_left.connect(_on_peer_left)
|
|
|
|
if multiplayer.is_server():
|
|
spawn_peer(multiplayer.get_unique_id())
|
|
for peer_id in multiplayer.get_peers():
|
|
spawn_peer(peer_id)
|
|
|
|
|
|
# Picks the match's 2 offered races once and caches them, so every caller
|
|
# (the server's own TeamSelect and every remote client's request) converges
|
|
# on the same pair regardless of call order. Also kicks off bot fill for
|
|
# those 2 races — see bots/bot_manager.gd.
|
|
func decide_offered_races() -> Array:
|
|
if _offered_race_ids.is_empty():
|
|
var ids := range(1, TeamSelect.RACES.size() + 1)
|
|
ids.shuffle()
|
|
_offered_race_ids = [ids[0], ids[1]]
|
|
# Deferred: this can run from TeamSelect._ready(), which (as World's
|
|
# child) fires before World's own _ready() — before _players/_spawner
|
|
# are assigned. Deferring runs it after the whole scene's _ready
|
|
# cascade finishes, once World is actually spawn-ready.
|
|
BotManager.call_deferred("on_match_start", self, _offered_race_ids)
|
|
return _offered_race_ids
|
|
|
|
|
|
@rpc("any_peer", "call_remote", "reliable")
|
|
func request_offered_races() -> void:
|
|
if not multiplayer.is_server():
|
|
return
|
|
var sender_id := multiplayer.get_remote_sender_id()
|
|
_deliver_offered_races.rpc_id(sender_id, decide_offered_races())
|
|
|
|
|
|
@rpc("authority", "call_remote", "reliable")
|
|
func _deliver_offered_races(race_ids: Array) -> void:
|
|
_team_select.offer_races(race_ids)
|
|
|
|
|
|
# Spawns (or returns the already-spawned) ship for a given peer_id — real
|
|
# (positive) or bot (negative, see bots/bot_manager.gd). Replicates to every
|
|
# client automatically via MultiplayerSpawner.
|
|
func spawn_peer(peer_id: int) -> Node:
|
|
if _players.has_node(str(peer_id)):
|
|
return _players.get_node(str(peer_id))
|
|
return _spawner.spawn(peer_id)
|
|
|
|
|
|
func despawn_peer(peer_id: int) -> void:
|
|
var ship := _players.get_node_or_null(str(peer_id))
|
|
if ship:
|
|
ship.queue_free()
|
|
|
|
|
|
func _spawn_ship(peer_id: int) -> Node:
|
|
var ship := SHIP_SCENE.instantiate()
|
|
ship.name = str(peer_id)
|
|
ship.peer_id = peer_id
|
|
return ship
|
|
|
|
|
|
# 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.
|
|
func request_bullet_spawn(source_peer_id: int, pos: Vector2, vel: Vector2) -> void:
|
|
if not multiplayer.is_server():
|
|
return
|
|
_bullet_spawner.spawn({"peer_id": source_peer_id, "pos": pos, "vel": vel})
|
|
|
|
|
|
func _spawn_bullet(data: Dictionary) -> Node:
|
|
var bullet := BULLET_SCENE.instantiate()
|
|
bullet.global_position = data.pos
|
|
bullet.velocity = data.vel
|
|
bullet.source_peer_id = data.peer_id
|
|
return bullet
|
|
|
|
|
|
func _on_peer_joined(peer_id: int) -> void:
|
|
if multiplayer.is_server():
|
|
spawn_peer(peer_id)
|
|
|
|
|
|
func _on_peer_left(peer_id: int) -> void:
|
|
if not multiplayer.is_server():
|
|
return
|
|
despawn_peer(peer_id)
|
|
|
|
|
|
# Tiles have no collision shapes of their own, so spawn one StaticBody2D per
|
|
# occupied cell. Asteroids get round hitboxes to roughly match their sprites.
|
|
# Walls only bounce the ship; asteroids also deal impact damage.
|
|
func _build_tile_collisions(map: Node) -> void:
|
|
for layer in map.find_children("*", "TileMapLayer", true, false):
|
|
var is_asteroid := layer.name == "Asteroids"
|
|
var group := "environment_hazard" if is_asteroid else "environment_wall"
|
|
_add_layer_colliders(layer, is_asteroid, group)
|
|
|
|
|
|
func _add_layer_colliders(layer: TileMapLayer, use_circle: bool, group: String) -> void:
|
|
var tile_size := Vector2(layer.tile_set.tile_size)
|
|
for cell in layer.get_used_cells():
|
|
var body := StaticBody2D.new()
|
|
body.position = layer.map_to_local(cell)
|
|
body.add_to_group(group)
|
|
var shape := CollisionShape2D.new()
|
|
if use_circle:
|
|
var circle := CircleShape2D.new()
|
|
circle.radius = min(tile_size.x, tile_size.y) * 0.5
|
|
shape.shape = circle
|
|
else:
|
|
var rect := RectangleShape2D.new()
|
|
rect.size = tile_size
|
|
shape.shape = rect
|
|
body.add_child(shape)
|
|
layer.add_child(body)
|