Pawn bodies split out, character select redesigned, and started the dreaded process of correcting the netcode. Players can connect to a shared room, character select, and start a match. They appear correctly differentiated with cameras and map icons that follow them independently. Jankiness with trap ownership is present as is stuff connected to animation. Unit tests for all network functionality required.

This commit is contained in:
2026-03-10 01:41:15 -04:00
parent 984567cf96
commit d128501f7c
52 changed files with 1179 additions and 929 deletions

View File

@@ -7,6 +7,16 @@ enum Modes{
VS_MAN
}
var pawns : Dictionary = {
"A" : load("res://data/pawns/a.tres"),
"B" : load("res://data/pawns/b.tres"),
"C" : load("res://data/pawns/c.tres"),
"D" : load("res://data/pawns/d.tres"),
"E" : load("res://data/pawns/e.tres"),
"F" : load("res://data/pawns/f.tres")
}
const blinder_template = preload("res://templates/blinder.tscn")
const one_shot_template = preload("res://templates/one_shot.tscn")
@@ -15,13 +25,14 @@ const vs_com_level = preload("res://scenes/test_level.tscn")
const vs_man_level = preload("res://scenes/manufactory.tscn")
const pawn_select_scene = preload("res://scenes/character_select.tscn")
var cmd_args : Dictionary = {}
var player : PawnController
var hud : HUD
var level : Level
var mode : Modes = Modes.STORY
var multiplayer_game : bool = false
var num_players : int = 1
var pawns_selected : Dictionary[int, PawnBaseData] = {}
var pawns_selected : Dictionary[int, StringName] = {}
var level_synced : Dictionary[int, bool] = {}
var net_test : bool #TODO: Remove later
@@ -30,16 +41,21 @@ func _ready() -> void:
for arg in OS.get_cmdline_args():
if arg.begins_with("--"):
var flag = arg.lstrip("--")
if flag.contains("="):
var key_val = flag.split("=")
args[key_val[0]]=key_val[1]
args[flag]=true
if args.has("net_test"):
net_test = true
if args.has("instance_id"):
get_tree().root.title = "Net Gunner (Instance: %s)" % args.instance_id
cmd_args = args
func setup_player(pc : PawnController) -> void:
var is_local = false
Game.level.pawns[pc.id] = pc
if pc.id == Multiplayer.id:
is_local = true
level.pawns[pc.id] = pc
if is_local:
player = pc
if hud:
@@ -49,10 +65,11 @@ func is_multiplayer() -> bool:
return multiplayer_game
func start_level(pawns : Dictionary[int, PawnBaseData]) -> void:
func start_level(pawns : Dictionary[int, StringName]) -> void:
pawns_selected = pawns
if !Multiplayer.is_host():
return
pawns_selected = pawns
for i in Multiplayer.players.values():
level_synced[i] = false
level_synced[1] = true
@@ -117,7 +134,6 @@ func level_spawned(peer_id : int) -> void:
level_synced[peer_id] = true
check_level_ready()
func check_level_ready() -> void:
var ready : bool = true
for id in level_synced:

View File

@@ -34,11 +34,11 @@ func become_host() -> void:
print("Hosting Room...")
var room_id = await server_peer.room_connected
print("Connected to room: ", server_peer.room_id)
server_peer.get_rooms()
#server_peer.get_rooms()
print("GETTING ROOMS!")
var rooms = await server_peer.rooms_received
#var rooms = await server_peer.rooms_received
print("GOT ROOMS!")
print(rooms)
#print(rooms)
players[handle] = 1
player_readiness[1] = false

View File

@@ -1,13 +1,6 @@
extends Control
class_name CharacterSelect extends Control
const pawns : Array[PawnBaseData] = [
preload("res://data/pawns/van_reily.tres"),
preload("res://data/pawns/lou_riche.tres"),
preload("res://data/pawns/tico.tres"),
preload("res://data/pawns/john_bishous.tres"),
preload("res://data/pawns/abdoll_relin.tres"),
preload("res://data/pawns/tenrou_ugetsu.tres")
]
const p_colors : Array[Color] = [
Color.ROYAL_BLUE,
@@ -16,34 +9,53 @@ const p_colors : Array[Color] = [
const selector_template = preload("res://templates/pawn_selector.tscn")
@onready var pawn_displays : Array[PawnDisplay] = [
%P1PawnDisplay,
%P2PawnDisplay
]
const pawn_pick_portrait_template = preload("res://templates/pawn_pick_portrait.tscn")
var selector_wait : float = 0
var selected : int = -1
var selectors : Dictionary[int,PawnSelector] = {}
var displays : Dictionary[int, PawnDisplay] = {}
var portraits : Dictionary[Vector2i, PawnPickPortrait] = {}
var announce_tween : Tween = null
@onready var announcer : AudioStreamPlayer = %Announcer
@onready var selector_start : Control = %SelectorStart
@onready var switch_sound : AudioStreamPlayer = %SwitchSound
@onready var select_sound : AudioStreamPlayer = %SelectSound
@onready var portrait_grid : GridContainer = %PawnPickPortraits
@onready var pawn_displays : Control = %PawnDisplays
func _ready() -> void:
var i : int = 0
var idx : Vector2i = Vector2i.ZERO
for pawn in Game.pawns.values():
var pp : PawnPickPortrait = pawn_pick_portrait_template.instantiate()
portrait_grid.add_child(pp)
pp.setup(pawn, i)
portraits[idx] = pp
i+=1
idx.x += 1
if idx.x >= portrait_grid.columns:
idx.y += 1
idx.x -= portrait_grid.columns
Multiplayer.waiting = true
var plist
if Game.is_multiplayer():
plist = Multiplayer.players.values()
for i in range(Game.num_players):
displays[plist[i]] = pawn_displays[i]
change_display(plist[i], 0)
for id in range(Game.num_players):
var display : PawnDisplay = pawn_displays.get_child(id)
displays[plist[id]] = display
display.visible = true
portraits[Vector2i.ZERO].num_players += 1
change_display(plist[id], 0)
else:
displays[1] = pawn_displays[0]
displays[1] = pawn_displays.get_child(0)
displays[1].visible = true
change_display(1, 0)
portraits[Vector2i.ZERO].num_players += 1
Multiplayer.all_ready.connect(add_selector, CONNECT_ONE_SHOT)
Multiplayer.set_player_ready.rpc(Multiplayer.id)
#Set up each pawn on bottom
@@ -67,12 +79,12 @@ func _process(delta : float) -> void:
if !selectors.has(Multiplayer.id):
return
var locked = true
var pawns_selected : Dictionary[int, PawnBaseData] = {}
var pawns_selected : Dictionary[int, StringName] = {}
for selector : PawnSelector in selectors.values():
if !selector.selected:
locked = false
else:
pawns_selected[selector.player_id] = pawns[selector.selection]
pawns_selected[selector.player_id] = portraits[selector.selection].pawn.name
if locked:
Game.start_level(pawns_selected)
@@ -88,25 +100,25 @@ func _process(delta : float) -> void:
input_vector = Input.get_vector("west","east","north","south")
var move_dir : Vector2 = Vector2.ZERO
var move_dir : Vector2i = Vector2.ZERO
if input_vector.x < 0:
move_dir.x -= 1
if input_vector.x > 0:
move_dir.x += 1
if move_dir.x != 0:
move_selector.rpc(selector.player_id, wrapi(selector.selection + sign(move_dir.x),0, len(pawns)))
move_dir = Vector2i((input_vector + .499 * sign(input_vector)).round())
if move_dir.length_squared() != 0:
var selection : Vector2i = selector.selection + move_dir
selection.x = wrapi(selection.x, 0, portrait_grid.columns)
selection.y = wrapi(selection.y, 0, ceili(float(portrait_grid.get_child_count()) / portrait_grid.columns))
move_selector.rpc(selector.player_id, selection)
selector_wait = 0.25
@rpc("any_peer","call_local","reliable")
func move_selector(peer_id : int, selection : int) -> void:
func move_selector(peer_id : int, selection : Vector2i) -> void:
var selector : PawnSelector = selectors[peer_id]
selector.selection = selection
change_display(peer_id, selector.selection)
var v = Vector2(175 * selector.selection, 0)
selector.position = Vector2(175 * selector.selection, 0)
portraits[selector.selection].num_players -= 1
selector.selection = selection
portraits[selector.selection].num_players += 1
change_display(peer_id, portraits[selector.selection].pawn_idx)
var v = Vector2(154 * selector.selection.x, 154 * selector.selection.y)
selector.position = v
switch_sound.play()
if announce_tween != null and announce_tween.is_running():
announce_tween.stop()
@@ -114,7 +126,7 @@ func move_selector(peer_id : int, selection : int) -> void:
announcer.stop()
announce_tween = create_tween()
announce_tween.tween_interval(.25)
announcer.stream = pawns[selection].name_audio
announcer.stream = portraits[selection].pawn.name_audio
announce_tween.tween_callback(announcer.play)
@rpc("any_peer","call_local","reliable")
@@ -126,9 +138,10 @@ func lock_selector(peer_id : int) -> void:
func change_display(player : int, selection: int) -> void:
var pd : PawnDisplay = displays[player]
pd.set_pawn_name(pawns[selection].name)
pd.set_portrait(pawns[selection].portrait)
pd.set_hacks(pawns[selection].starting_hacks)
var pawn : PawnBaseData = Game.pawns.values()[selection]
pd.set_pawn_name(pawn.name)
pd.set_portrait(pawn.portrait)
pd.set_hacks(pawn.starting_hacks)
func _on_selector_start_child_entered_tree(node: Node) -> void:

View File

@@ -108,7 +108,6 @@ func _on_reveal_timeout() -> void:
func _ready() -> void:
var owns_hack = hack_owner == Multiplayer.id
print("Setup Hack " + name)
material.set_shader_parameter("glow_color", Color.YELLOW if owns_hack else Color.RED)
icon.texture = hack_icons[type]
model.visible = owns_hack

View File

@@ -24,7 +24,7 @@ const camera_offset = Vector3(0, 10, 5.25)
@onready var cameras_node : Node3D = %Cameras
@onready var map_markers_node : Node3D = %MapMarkers
@onready var spawn_points : Array[Node] = $PawnSpawner.get_children()
@onready var multiplayer_level_setup : MultiplayerLevelSetup = %MultiplayerLevelSetup
@export var difficulty : int = 1
@@ -34,6 +34,7 @@ var cameras : Dictionary[int, PawnCamera] = {}
var map_markers : Dictionary[Object, MapMarker] = {}
func _ready() -> void:
process_mode=Node.PROCESS_MODE_DISABLED
Game.level = self
if Game.mode == Game.Modes.STORY:
setup()
@@ -43,20 +44,45 @@ func setup() -> void:
if Multiplayer.is_host():
spawn_players()
@rpc("authority")
func spawn_players() -> void:
if Game.is_multiplayer():
spawn_players_multiplayer()
else:
spawn_players_singleplayer()
func spawn_players_singleplayer() -> void:
var pc : PawnController = pawn_controller.instantiate()
pawns[1] = pc
pawns_node.add_child(pc,true)
var position : Vector3
if(spawn_points == null
or len(spawn_points) <= 1):
position = Vector3(0,0,0)
else:
position = spawn_points[0].global_position
pc.global_position = position
pc.setup(1, Game.pawns_selected[1])
add_pawn_camera(pc)
cameras[1].register_pawn(1)
process_mode=Node.PROCESS_MODE_INHERIT
func spawn_players_multiplayer() -> void:
var count = 0
multiplayer_level_setup.set_players_unready(Multiplayer.players.values())
multiplayer_level_setup.set_pawns_expected.rpc(Game.pawns_selected.size())
for key in Game.pawns_selected:
var pc : PawnController = pawn_controller.instantiate()
var pd = Game.pawns_selected[key]
pawns[key] = pc
pawns_node.add_child(pc,true)
var hacks : Array = []
for hack : HackSet in pd.starting_hacks:
var dict = {
"type":hack.type,
"qty":hack.qty
}
hacks.append(dict)
await multiplayer_level_setup.all_players_ready
for key in pawns:
var pc = pawns[key]
var position : Vector3
if(spawn_points == null
or len(spawn_points) <= count):
@@ -66,10 +92,23 @@ func spawn_players() -> void:
position = Vector3(3,0,0)
else:
position = spawn_points[count].global_position
pc.setup.rpc(key,hacks,position)
add_pawn_camera(pc)
count += 1
pc.global_position = position
pc.setup.rpc(key, Game.pawns_selected[key])
multiplayer_level_setup.set_players_unready(Multiplayer.players.values())
multiplayer_level_setup.set_cameras_expected.rpc(Game.pawns_selected.size())
for pc in pawns.values():
add_pawn_camera(pc)
await multiplayer_level_setup.all_players_ready
setup_cameras()
start_level.rpc()
@rpc("authority", "call_local")
func start_level() -> void:
process_mode=Node.PROCESS_MODE_INHERIT
func is_square_detected(crd) -> bool:
return marker_layer.get_cell_item(crd + Vector3i(0,-1,0)) != GridMap.INVALID_CELL_ITEM
@@ -88,15 +127,21 @@ func detect_square(crd : Vector3i, mark : bool) -> bool:
hack.reveal()
return true
@rpc("any_peer", "call_local", "reliable")
func add_pawn_camera(pawn : PawnController) -> void:
if Multiplayer.is_host():
var camera : PawnCamera = camera_template.instantiate()
camera.position = pawn.global_position + camera_offset
cameras_node.add_child(camera,true)
camera.register_pawn.rpc(pawn.id)
cameras[pawn.id] = camera
func setup_cameras() -> void:
var camera_list = cameras_node.get_children()
var idx = 0
for pawn_id in pawns:
camera_list[idx].register_pawn.rpc(pawn_id)
cameras[pawn_id] = camera_list[idx]
idx += 1
func add_hack(hack : Hack, crd : Vector3i) -> void:
hack.square = crd

View File

@@ -1,19 +0,0 @@
extends Level
#const pawn_controller_template = preload("res://templates/pawn_controller.tscn")
#
#@onready var player_spawner = %Spawner
#func _ready() -> void:
#print("LEVEL LOADING, ID: " + str(Multiplayer.id) + " with " + str(Multiplayer.players.size()) + " keys recognized")
#Game.level = self
#
#func setup() -> void:
#if Multiplayer.is_host():
#spawn_players()
#
#func spawn_players() -> void:
#for key in Game.pawns_selected:
#var pc : PawnController = pawn_controller_template.instantiate()
#var pd = Game.pawns_selected[key]
#player_spawner.add_pawn(pc, key)
#pc.setup(key,pd.starting_hacks)

View File

@@ -0,0 +1,62 @@
class_name MultiplayerLevelSetup extends Node
var players_ready : Dictionary[int, bool] = {}
var pawns_expected : int = 1
var cameras_expected : int = 1
signal all_pawns_spawned()
signal all_cameras_spawned()
signal all_players_ready()
@rpc("authority", "reliable")
func set_pawns_expected(num : int) -> void:
all_pawns_spawned.connect(_on_all_pawns_spawned, CONNECT_ONE_SHOT)
print("(Instance %s) Expecting %d Pawns" % [Game.cmd_args.instance_id, num])
pawns_expected = num
func set_players_unready(list : Array) -> void:
players_ready = {}
for id in list:
players_ready[id] = false
players_ready[1] = true
func check_all_ready() -> bool:
for id in players_ready:
if !players_ready[id]:
return false
return true
func _on_all_pawns_spawned() -> void:
player_ready.rpc_id(1,Multiplayer.id)
func _on_all_cameras_spawned() -> void:
player_ready.rpc_id(1,Multiplayer.id)
@rpc("any_peer", "reliable")
func player_ready(id : int) -> void:
print("(Instance %s) Player %d notified ready" % [Game.cmd_args.instance_id, id])
players_ready[id] = true
if(check_all_ready()):
print("(Instance %s) ALL PLAYERS READY!!" % [Game.cmd_args.instance_id])
all_players_ready.emit()
@rpc("authority", "reliable")
func set_cameras_expected(num : int) -> void:
all_cameras_spawned.connect(_on_all_cameras_spawned, CONNECT_ONE_SHOT)
cameras_expected = num
func _on_pawn_spawner_spawned(node: Node) -> void:
print("(Instance %s) Spawned %d Pawn" % [Game.cmd_args.instance_id, node.id])
pawns_expected -= 1
if pawns_expected == 0:
print("(Instance %s) ALL PAWNS SPAWNED!" % [Game.cmd_args.instance_id])
all_pawns_spawned.emit()
func _on_camera_spawner_spawned(node: Node) -> void:
cameras_expected -= 1
if cameras_expected == 0:
all_cameras_spawned.emit()

View File

@@ -0,0 +1 @@
uid://b5npi1ys4lnf4

View File

@@ -0,0 +1,18 @@
extends Node3D
func _enter_tree() -> void:
Game.pawns_selected[1] = Game.pawns.keys()[0]
Multiplayer.client_added.connect(_on_client_added)
#SETUP HOST:
if !Game.net_test:
var handle = "Host - P1"
Multiplayer.handle = handle
#Validate entries
Multiplayer.become_host()
func _on_client_added(handle : String, id : int) -> void:
pass

View File

@@ -13,6 +13,9 @@ class_name PawnBody extends Node3D
@onready var walk_sound : AudioStreamPlayer3D = $WalkSound
@onready var footstep_timer : Timer = $FootstepTimer
signal shooting()
signal reloading()
@rpc("call_local")
func play_animation(anim_name : String) -> void:
anim_player.play(anim_name)

View File

@@ -4,6 +4,7 @@ class_name PawnCamera extends Camera3D
var target
var player_offset : Vector3
@export var player_id = 1
@export var decay = 0.9 # How quickly the shaking stops [0, 1].
@export var max_offset = Vector2(2, 1.5) # Maximum hor/ver shake in pixels.

View File

@@ -15,9 +15,10 @@ const hack_template = preload("res://templates/hack.tscn")
const uninstall_hack_modal = preload("res://templates/uninstall_hack_modal.tscn")
const decompile_hack_modal = preload("res://templates/decompile_hack_modal.tscn")
const range_sphere_template = preload("res://templates/range_sphere.tscn")
@export var pawn_name : StringName
@export var speed : float = 10
@onready var body : PawnBody = $PawnBody
@onready var body : PawnBody
@onready var input : PawnInput = $PawnInput
@onready var data : PawnLevelData = $Data
@onready var hack_sound : AudioStreamPlayer3D = $HackSound
@@ -28,7 +29,7 @@ const range_sphere_template = preload("res://templates/range_sphere.tscn")
@onready var reload_sound : AudioStreamPlayer3D = $PawnBody/ReloadSound
@onready var detect_icon : Sprite3D = $DetectIcon
var id : int = 1
@export var id : int = 1
@export var state : State
var button_actions : Dictionary[int, String]
@@ -62,6 +63,8 @@ var max_ammo = 5
var combat_target
var meleeing : bool = false
var shooting : bool = false
var reloading : bool = false
var take_shot : bool = false
var flinch : float = 0
@@ -95,6 +98,8 @@ func _exit_tree() -> void:
#Game.level.evaluate_outcome()
#Game.evaluate
func _physics_process(delta: float) -> void:
if attack_timer > 0:
attack_timer -= delta
@@ -155,10 +160,11 @@ func _physics_process(delta: float) -> void:
body.play_footsteps(lerp(.78, .33, dir.length()))
else:
body.stop_footsteps()
else:
elif body != null:
body.stop_footsteps()
body.set_animation_parameter("parameters/Motion/blend_position", dir.length())
body.set_animation_parameter("parameters/Crouch/blend_position", dir.length())
if body != null:
body.set_animation_parameter("parameters/Motion/blend_position", dir.length())
body.set_animation_parameter("parameters/Crouch/blend_position", dir.length())
State.KNOCKUP:
if is_on_floor():
knockdown(facing)
@@ -197,6 +203,20 @@ func _physics_process(delta: float) -> void:
elif state == State.NORMAL:
update_actions()
@rpc("call_local", "reliable")
func setup_pawn_body(pd : PawnBaseData) -> void:
set_pawn_body(pd.pawn_body.instantiate())
func set_pawn_body(pb : PawnBody) -> void:
if body != null:
body.queue_free()
body = pb
pb.reloading.connect(reload_ranged)
pb.shooting.connect(fire_ranged)
add_child(body)
struggling.connect(body._on_struggle_changed)
func attack() -> void:
if attack_timer > 0:
return
@@ -205,22 +225,25 @@ func attack() -> void:
attack_timer = melee_recovery_time
else:
if ammo <= 0:
reload_sound.play()
ammo = max_ammo
attack_timer = ranged_reload_time
ammo_changed.emit(ammo, max_ammo)
reloading = true
return
ammo-=1
ammo_changed.emit(ammo, max_ammo)
attack_timer = ranged_recovery_time
shooting == true
shooting = true
take_shot = true
if combat_target != null:
var v = body.global_position.direction_to(combat_target.global_position)
v.y = 0
body.look_at(body.global_position - v)
func reload_ranged() -> void:
reloading = false
reload_sound.play()
ammo = max_ammo
attack_timer = ranged_reload_time
ammo_changed.emit(ammo, max_ammo)
func update_actions() -> void:
if attack_timer <= 0 and shooting and !input.is_action_pressed("attack"):
shooting = false
@@ -297,6 +320,9 @@ func update_poison(delta : float) -> void:
func fire_ranged() -> void:
var shot = body.projectile_template.instantiate()
var tdir : Vector3 = Vector3.ZERO
take_shot = false
ammo-=1
ammo_changed.emit(ammo, max_ammo)
shot.speed = projectile_speed
tdir = body.ranged_point.global_position.direction_to(combat_target.global_position) if combat_target else facing
shot.direction = tdir
@@ -490,15 +516,15 @@ func clear_detect_region() -> void:
detect_squares = {}
@rpc("authority", "call_local", "reliable")
func setup(id : int, hacks : Array, pos : Vector3) -> void:
func setup(id : int, pawn : StringName) -> void:
self.id = id
self.global_position = pos
var base_data : PawnBaseData = Game.pawns[pawn]
var hacklist : Array[PawnLevelData.HackData] = []
for hack in hacks:
for hack in base_data.starting_hacks:
hacklist.append(PawnLevelData.HackData.new(hack.type, hack.qty, hack.qty))
$Data.hacks = hacklist
input.set_multiplayer_authority(id)
struggling.connect(body._on_struggle_changed)
setup_pawn_body(base_data)
Game.setup_player(self)
func uninstall_hack_at(square) -> void:

View File

@@ -0,0 +1,25 @@
class_name PawnPickPortrait extends Panel
@onready var portrait : TextureRect = %Portrait
@onready var cover : Control = %Cover
var pawn : PawnBaseData
var pawn_idx : int
var _players : int = 0
var num_players : int :
get:
return _players
set(value):
_players = value
if cover != null:
cover.visible = (_players == 0)
func _ready() -> void:
pass
func setup(data : PawnBaseData, idx : int) -> void:
pawn = data
pawn_idx = idx
if portrait != null:
portrait.texture = data.portrait

View File

@@ -0,0 +1 @@
uid://dqpoa5x8u3r3h

View File

@@ -2,7 +2,7 @@ class_name PawnSelector extends TextureRect
@onready var anim_player : AnimationPlayer = $AnimationPlayer
@export var player_id : int
@export var selection : int = 0
@export var selection : Vector2i = Vector2.ZERO
@export var selected : bool
func setup(id : int, color : Color) -> void:

View File

@@ -3,6 +3,6 @@ extends Node3D
@onready var level : Level = $Level
func _ready() -> void:
Game.pawns_selected[1] = load("res://data/pawns/van_reily.tres")
Game.pawns_selected[1] = "a"
level.setup()