4
0
Fork 0

Changes for backporting.

This commit is contained in:
Nekojimi 2025-06-21 14:35:32 +01:00
parent 37ce953fb0
commit 90011bd75f
29 changed files with 425 additions and 54 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://difcchtl3dx04"]
[ext_resource type="Script" path="res://scripts/player.gd" id="1_b212b"]
[ext_resource type="Script" uid="uid://ch1ox42pq5wl7" path="res://scripts/player.gd" id="1_b212b"]
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_v03xq"]
properties/0/path = NodePath(".:session_id")

65
scripts/announcer.gd Normal file
View File

@ -0,0 +1,65 @@
extends Node
class_name Announcer
@onready var announcer_audio_player: AudioStreamPlayer = $"AnnouncerAudioPlayer"
@onready var animation: AnimationPlayer = $"../AnimationPlayer"
@export_group("Announcer", "announcer")
@export_subgroup("Text", "announcer_text")
@export var announcer_text_wpm: float = 150.0
@export var announcer_text_linger_ratio: float = 1.0
@export_subgroup("Lines", "announcer_lines")
@export var announcer_lines_intro: Array[AnnouncerLine] = []
@export var announcer_lines_start_game: Array[AnnouncerLine] = []
@export var announcer_lines_waiting_for_players: Array[AnnouncerLine] = []
@export var announcer_lines_team_handout_pre: Array[AnnouncerLine] = []
@export var announcer_lines_team_handout_post: Array[AnnouncerLine] = []
@export var announcer_lines_role_handout_pre: Array[AnnouncerLine] = []
@export var announcer_lines_role_handout_post: Array[AnnouncerLine] = []
signal finished()
func _ready() -> void:
announcer_audio_player.connect("finished", func(): finished.emit())
func announce_line_by_id(id: String, context: Dictionary = {}) -> void:
var lines: Array[AnnouncerLine] = []
lines.assign(get_indexed(NodePath("announcer_lines_" + id)))
if !lines.is_empty():
announce_line(lines.pick_random(), context)
func announce_text(text: String, context: Dictionary = {}) -> void:
var line: AnnouncerLine = AnnouncerLine.new()
line.text = text
await announce_line(line, context)
func announce_random_line(lines: Array[AnnouncerLine], context: Dictionary = {}) -> void:
await announce_line(lines.pick_random(), context)
func announce_line(line: AnnouncerLine, context: Dictionary = {}) -> void:
if line == null:
return
$"../SubtitlesLabel".text = line.text.format(context)
animation.play("subtitles_appear")
await animation.animation_finished
var duration: float = 1.0
if line.audio == null:
line.audio = await $"../TTS".get_tts_audio(line.text.format(context))
if line.audio != null:
duration = line.audio.get_length()
announcer_audio_player.stream = line.audio
announcer_audio_player.play(0.0)
else:
var word_count = line.text.count(" ")+1
duration = word_count * (60.0/150.0)
animation.play("subtitles_display", -1, 1.0/duration)
await animation.animation_finished
var linger_time: float = duration * announcer_text_linger_ratio
await get_tree().create_timer(linger_time).timeout
animation.play("subtitles_disappear")
await animation.animation_finished
func is_speaking():
return announcer_audio_player.playing

View File

@ -0,0 +1,7 @@
extends Resource
class_name AnnouncerLine
@export_multiline var text: String
@export var audio: AudioStream
@export var interruptable: bool = true
@export var stop_time: bool = false

View File

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

View File

@ -2,6 +2,7 @@ extends Control
@export var multiplayer_port: int = 2481
@export var reconnection_delay: float = 5
@export var auto_setup: bool = false
var server_ip: String = "127.0.0.1"
@ -15,6 +16,10 @@ var session_id: int = -1
var searching_for_player: bool = false
signal local_player_created(player: Player)
@onready var players: Players = $"GameState/Players"
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
server_ip = get_server_ip()
@ -25,7 +30,7 @@ func _ready() -> void:
reconnect()
$"../Players".connect("player_joined", on_player_joined)
players.connect("player_joined", on_player_joined)
func _process(delta: float) -> void:
if !connected and !connecting:
@ -34,10 +39,15 @@ func _process(delta: float) -> void:
reconnection_timer = 0
reconnect()
if searching_for_player:
for player in $"../Players".get_children():
for player in players.get_children():
if player.session_id == session_id:
print("Client %d found player object %s" % [session_id, player])
if auto_setup:
player.player_name = "Bot %d" % session_id
player.player_pronouns = "they/them"
searching_for_player = false
$PromptManager.player = player
local_player_created.emit(player)
#$PromptManager.player = player
func reconnect() -> void:
var peer: MultiplayerPeer = WebSocketMultiplayerPeer.new()
@ -92,7 +102,7 @@ func get_session_id() -> int:
func join_game(session_id: int):
searching_for_player = true
var player : Player = $"../Players".find_player_by_session_id(session_id)
var player : Player = players.find_player_by_session_id(session_id)
#player.claim_authority(session_id)
func on_player_joined(player: Player):

1
scripts/client.gd.uid Normal file
View File

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

View File

@ -38,6 +38,7 @@ func _ready() -> void:
start_caddy()
start_multiplayer()
func _process(delta: float) -> void:
check_caddy_timer += delta

View File

@ -5,34 +5,124 @@ class_name Player
@export var player_name: String = ""
@export var player_pronouns: String = ""
@export var current_prompt: Dictionary = {}:
var game_state: GameState = null
var waiting_actions: Dictionary = {}
var prompt_queue: Array[Prompt] = []
var message_context: Dictionary = {}
var current_prompt: Prompt:
set(val):
if val != current_prompt:
current_prompt = val
prompt_changed.emit(current_prompt)
var playing: bool = false:
set(val):
if val != playing:
playing = val
playing_status_changed.emit(playing)
signal prompt_changed(prompt: Dictionary)
signal prompt_answered(answer: String)
signal prompt_changed(prompt: Prompt)
signal prompt_answered(prompt: Prompt, answer: String)
signal prompt_queue_finished()
signal playing_status_changed(playing: bool)
signal message_spoken(message: String)
@rpc("any_peer", "call_local", "reliable")
func claim_authority(peer_id: int) -> void:
set_multiplayer_authority(peer_id, true)
#@rpc("any_peer", "call_local", "reliable")
#func claim_authority(peer_id: int) -> void:
#set_multiplayer_authority(peer_id, true)
func send_prompt(prompt: Dictionary) -> void:
_send_prompt.rpc(JSON.stringify(prompt))
pass
func send_message(message: String, additional_context: Dictionary = {}) -> void:
var prompt: Prompt = Prompt.create(message, Prompt.PromptType.CONFIRMATION)\
.with_context(message_context)\
.with_context(additional_context)
send_prompt(prompt)
func add_message_context(key: String, value: String) -> void:
message_context[key] = value
func add_message_context_dict(context: Dictionary) -> void:
message_context.merge(context, true)
func get_pronoun_context_dict(prefix: String = "") -> Dictionary:
var ret: Dictionary = {}
var they: String
var them: String
var their: String
var theirs: String
match player_pronouns:
"he/him":
they = "he"
them = "him"
their = "his"
theirs = "his"
"she/her":
they = "she"
them = "her"
their = "her"
theirs = "hers"
_:
they = "they"
them = "them"
their = "their"
theirs = "theirs"
ret[prefix+"they"] = they
ret[prefix+"them"] = them
ret[prefix+"their"] = their
ret[prefix+"theirs"] = theirs
ret[prefix+"themself"] = them+"self"
return ret
func get_context_dict(prefix: String) -> Dictionary:
var ret = get_pronoun_context_dict(prefix)
ret[prefix+"name"] = player_name.capitalize()
ret[prefix+"name's"] = player_name.capitalize() + "'s"
return ret
func send_action(action: Action, immediate: bool = false) -> void:
waiting_actions[action.prompt.id] = action
#connect("prompt_answered", action.prompt_answered.bind(self), CONNECT_ONE_SHOT)
send_prompt(action.prompt)
func send_prompt(prompt: Prompt) -> void:
if prompt != null:
_send_prompt.rpc(JSON.stringify(prompt.save()))
else:
_send_prompt.rpc("{}")
@rpc("any_peer", "call_local", "reliable")
func _send_prompt(json_prompt: String) -> void:
pass
if !current_prompt.is_empty():
submit_prompt_answer.rpc(1,"")
current_prompt = JSON.parse_string(json_prompt)
print("Player %d prompt received: \"%s\"" % [session_id, current_prompt["text"]])
@rpc("any_peer", "call_remote", "reliable")
func submit_prompt_answer(answer: String) -> void:
#current_prompt = null
current_prompt.clear()
prompt_answered.emit(answer)
print("Player %d answer received: \"%s\"" % [session_id, answer])
print("Peer %d Player %d prompt received: %s" % [multiplayer.get_unique_id(),session_id, json_prompt])
var prompt: Prompt = Prompt.load(JSON.parse_string(json_prompt))
if current_prompt != null:
if current_prompt.interruptable:
submit_prompt_answer.rpc(current_prompt.id,"")
current_prompt = null
else:
prompt_queue.append(prompt)
if current_prompt == null:
current_prompt = prompt
if current_prompt != null:
current_prompt.context["my_name"] = name
#print("Peer %d Player %d prompt received: \"%s\" context: %s" % [multiplayer.get_unique_id(),session_id, current_prompt.text, current_prompt.context])
@rpc("any_peer", "call_local", "reliable")
func submit_prompt_answer(prompt_id: int, answer: String) -> void:
print("Peer %d Player %d answer received: \"%s\"" % [multiplayer.get_unique_id(),session_id, answer])
prompt_answered.emit(current_prompt, answer)
if waiting_actions.has(prompt_id):
var action: Action = waiting_actions.get(prompt_id)
action.prompt_answered(answer,self)
waiting_actions.erase(prompt_id)
if prompt_queue.is_empty():
current_prompt = null
prompt_queue_finished.emit()
else:
current_prompt = prompt_queue.pop_front()
func speak(message: String) -> void:
message_spoken.emit(message)
func is_busy() -> bool:
return current_prompt != null or !prompt_queue.is_empty()

1
scripts/player.gd.uid Normal file
View File

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

View File

@ -1,27 +1,61 @@
extends Node
class_name Players
@export var player_scene: PackedScene = preload("res://objects/player.tscn")
@export var bot_scene: PackedScene = preload("res://objects/bot.tscn")
@export var bot_count: int = 7
@export var bot_test_mode = false
@onready var game_state: GameState = get_parent() as GameState
signal player_joined(player: Player)
signal player_left(player: Player)
static var instance: Players = null
func _ready() -> void:
connect("child_entered_tree", on_child_added)
if instance == null:
instance = self
await get_tree().create_timer(0.5).timeout
for i in range(bot_count):
create_bot(i)
#bot.player_name = "Bot %d" % i
#bot.player_pronouns = "they/them"
@rpc("any_peer", "call_remote", "reliable")
func create_player_for_session_id(session_id: int) -> void:
func clear_all_players():
for child in get_children():
child.queue_free()
@rpc("any_peer", "call_remote", "reliable")
func create_player_for_session_id(session_id: int) -> Player:
for child in get_children():
var player: Player = child as Player
if player == null:
continue
if player.session_id == session_id:
return
return player
var player: Player = player_scene.instantiate() as Player
player.name = "Player %d" % session_id
player.session_id = session_id
player.game_state = game_state
print("Created player object for session ID %d" % session_id)
add_child(player)
return player
@rpc("any_peer", "call_remote", "reliable")
func create_bot(bot_id: int) -> Bot:
var bot: Bot = bot_scene.instantiate() as Bot
bot.session_id = bot_id
bot.game_state = game_state
bot.player_name = "Bot %d" % bot_id
bot.test_mode = bot_test_mode
add_child(bot)
return bot
func find_player_by_session_id(session_id: int) -> Player:
for child in get_children():
@ -40,8 +74,28 @@ func find_player_by_session_id(session_id: int) -> Player:
if player.session_id == session_id:
return player
return null
func find_player_by_name(name: String) -> Player:
for player in get_players():
if player.player_name == name:
return player
return null
func get_players() -> Array[Player]:
var ret: Array[Player]
for child in get_children():
if child is Player:
ret.append(child as Player)
return ret
func get_active_players() -> Array[Player]:
return get_players().filter(func(player: Player): return player.playing)
func on_child_added(node: Node) -> void:
if node is Player:
print("Player joined: %d" % node.session_id)
player_joined.emit(node as Player)
func call_on_active_players(method: StringName):
for player in get_active_players():
player.call(method)

1
scripts/players.gd.uid Normal file
View File

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

54
scripts/prompt.gd Normal file
View File

@ -0,0 +1,54 @@
extends Resource
class_name Prompt
enum PromptType {TEXT, LONG_TEXT, NUMBER, MULTIPLE_CHOICE, COLOUR, PLAYER, CONFIRMATION, SHOW_TEAM, SHOW_ROLE}
@export_multiline var text: String:
set(val):
text = val
id = text.hash()
@export_multiline var explaination: String
@export var type: PromptType
@export var options: Array[String]
@export var interruptable: bool = false
var id: int = randi()
var context: Dictionary #[String, String]
static func create(text: String, type: PromptType) -> Prompt:
var prompt: Prompt = Prompt.new()
prompt.text = text
prompt.type = type
return prompt
func format_text() -> String:
return text.format(context)
func with_context(additional_context: Dictionary) -> Prompt:
var ret: Prompt = duplicate(true)
ret.context = context.merged(additional_context)
return ret
func save() -> Dictionary:
return {
"text" = text,
"explaination" = explaination,
"type" = type as int,
"options" = options,
"context" = context,
"interruptable" = interruptable,
"id" = id,
}
static func load(dict: Dictionary) -> Prompt:
if dict.is_empty():
return null
var ret: Prompt = Prompt.new()
ret.text = dict.get("text", "")
ret.explaination = dict.get("explaination", "")
ret.type = dict.get("type", 0) as PromptType
ret.options.assign(dict.get("options", []))
ret.context = dict.get("context", {})
#ret.context.assign(dict.get("context", {}))
ret.id = dict.get("id", 0)
ret.interruptable = dict.get("interruptable", false)
return ret

1
scripts/prompt.gd.uid Normal file
View File

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

View File

@ -4,33 +4,33 @@ class_name PromptManager
@export var multi_choice_button_scene: PackedScene = preload("res://objects/multi_choice_button.tscn")
@export var prompt_text_format: String = "[center]%s[/center]"
enum PromptType {TEXT, LONG_TEXT, NUMBER, MULTIPLE_CHOICE, COLOUR, PLAYER}
var current_prompt: Dictionary
var current_prompt: Prompt
var player: Player:
set(value):
if player != null:
player.disconnect("prompt_changed", display_prompt)
player = value
player.connect("prompt_changed", display_prompt)
display_prompt(player.current_prompt)
set = set_player
func _ready() -> void:
hide_everything()
func hide_everything():
# hide all children
for child in get_children():
child.visible = false
func display_prompt(prompt: Dictionary):
if prompt.is_empty():
func display_prompt(prompt: Prompt):
if prompt == null:
print("Displaying no prompt")
visible = false
return
print("Displaying prompt \"%s\"" % prompt["text"])
print("Displaying prompt \"%s\"" % prompt.text)
current_prompt = prompt
visible = true
# hide all children
for child in get_children():
child.visible = false
hide_everything()
$PromptLabel.text = prompt_text_format % prompt["text"]
$PromptLabel.text = prompt_text_format % prompt.format_text()
$PromptLabel.visible = true
# clear all prompt buttons
@ -38,33 +38,64 @@ func display_prompt(prompt: Dictionary):
$MultichoiceButtons.remove_child(button)
button.queue_free()
if prompt["type"] == PromptType.LONG_TEXT:
if prompt.type == Prompt.PromptType.LONG_TEXT:
$LongTextEdit.visible = true
$LongTextEdit.text = ""
$SubmitButton.visible = true
elif prompt["type"] == PromptType.TEXT:
elif prompt.type == Prompt.PromptType.TEXT:
$LineEdit.visible = true
$LineEdit.text = ""
$SubmitButton.visible = true
#$LongTextEdit.placeholder_text = prompt.options
elif prompt["type"] == PromptType.MULTIPLE_CHOICE:
elif prompt.type == Prompt.PromptType.MULTIPLE_CHOICE or prompt.type == Prompt.PromptType.PLAYER:
$MultichoiceButtons.visible = true
for option in prompt.options:
var button: Button = multi_choice_button_scene.instantiate() as Button
button.text = option
$MultichoiceButtons.add_child(button)
button.connect("pressed", submit_result.bind(option))
elif prompt.type == Prompt.PromptType.CONFIRMATION:
$SubmitButton.visible = true
elif prompt.type == Prompt.PromptType.SHOW_TEAM:
$SubmitButton.visible = true
var team: Team = Team.teams.get(prompt.context.get("team"))
if team != null:
$SubmitButton.disabled = true
$TeamCard.connect("revealed", func(): $SubmitButton.disabled = false)
$TeamCard.team = team
$TeamCard.visible = true
$TeamCard.appear()
elif prompt.type == Prompt.PromptType.SHOW_ROLE:
$SubmitButton.visible = true
var role: Role = Role.roles.get(prompt.context.get("role"))
if role != null:
$SubmitButton.disabled = true
$RoleCard.connect("revealed", func(): $SubmitButton.disabled = false)
$RoleCard.role = role
$RoleCard.visible = true
$RoleCard.appear()
func submit_button_pressed() -> void:
if current_prompt.is_empty():
if current_prompt == null:
return
match current_prompt["type"] as PromptType:
PromptType.TEXT:
match current_prompt.type as Prompt.PromptType:
Prompt.PromptType.TEXT:
submit_result($LineEdit.text)
PromptType.LONG_TEXT:
Prompt.PromptType.LONG_TEXT:
submit_result($LongTextEdit.text)
Prompt.PromptType.CONFIRMATION,Prompt.PromptType.SHOW_ROLE,Prompt.PromptType.SHOW_TEAM:
submit_result("confirmed")
func submit_result(result: String):
player.submit_prompt_answer.rpc_id(1, result)
var prompt: Prompt = current_prompt
visible = false
current_prompt.clear()
current_prompt = null
player.submit_prompt_answer.rpc(prompt.id, result)
func set_player(value: Player) -> void:
if player != null:
player.disconnect("prompt_changed", display_prompt)
player = value
player.connect("prompt_changed", display_prompt)
display_prompt(player.current_prompt)

View File

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

View File

@ -1,4 +1,4 @@
extends Node
extends Container
class_name ProxyDelegate
@export var delegate_scene: PackedScene

View File

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

View File

@ -0,0 +1,9 @@
@tool
extends Control
func _ready() -> void:
connect("resized", _center_pivot)
_center_pivot()
func _center_pivot() -> void:
pivot_offset = size/2.0

View File

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

View File

@ -0,0 +1,33 @@
extends RefCounted
class_name MultiAwait
var waiting_count: int = 0
var finished: bool = false
signal done(timed_out: bool)
func _init(signals: Array[Signal], timeout: float) -> void:
for sig in signals:
var err: int = sig.connect(receive_signal, CONNECT_ONE_SHOT)
if err == Error.OK:
waiting_count += 1
check_done()
var scene_tree: SceneTree = Engine.get_main_loop() as SceneTree
if scene_tree != null:
scene_tree.create_timer(timeout).timeout.connect(finish.bind(true))
print("MultiAwait: waiting for %d signals or %f seconds" % [waiting_count,timeout])
func receive_signal() -> void:
waiting_count -= 1
print("MultiAwait: got signal, %d signals left" % [waiting_count])
check_done()
func check_done() -> void:
if waiting_count <= 0:
finish(false)
func finish(timed_out: bool) -> void:
if !finished:
print("MultiAwait: done, timed_out: %s" % [timed_out])
done.emit(timed_out)
finished = true

View File

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