Files
client/spacewar/world/world.gd
T
anekdotin c0b5f421c5 Add in-game chat, bot fill for casual, and borderless fullscreen
- 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>
2026-07-13 17:02:12 -04:00

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)