27 Commits

Author SHA1 Message Date
fcf8bfaa45 Ajout de l'intégration twitch 2026-06-23 23:38:20 +02:00
4f93f7acaf Ajout de l'addon twitch 2026-06-23 23:28:12 +02:00
5bdf8db609 Ajout d'un effet visuel sur les nouvelles graines, et dev en cours de l'imprimante 3D 2026-06-22 20:16:27 +02:00
af91337017 Dev démo 1.3
* Ajout d'un paramètre pour la taille de l'UI
* Changement de la traduction anglaise de la fourche en "pitchfork" et correction de
* Réparation de bug suite à la montée de version de Godot en 4.7
2026-06-19 10:42:34 +02:00
69dc4444e3 Correction des fichiers d'import des plantes (changements auto faits par Godot) 2026-06-19 10:10:27 +02:00
18ecd5b820 Réparation du bug de cellule hors région 2026-06-14 18:04:14 +02:00
84ea00aae3 Correction du son des cristaux, ajout d'un fade out à tous les dialogues et correction de traduction 2026-06-14 17:27:06 +02:00
f7f1d2be2c remix de subterra, refonte des sfx de fin de map 2026-06-14 16:55:02 +02:00
ee16f14176 suppression des sfx de déblocage et ajout du morceau de Subterra 2026-06-14 16:55:01 +02:00
17f63729e7 refonte des sfx de la cave (mutation, cristal) 2026-06-14 16:52:21 +02:00
c9f6bf0162 Suppression des animation bounce in dans les dialogues et ajout d'un rayon de visibilité pour la grille 2026-06-14 16:48:03 +02:00
33a8f022e5 Réparation de bug, paufinnage du rayon tracteur et insert de l'outil dans l'histoire 2026-06-14 16:19:36 +02:00
7c66d8b9de Les backrooms pour le fun =) 2026-06-14 16:18:24 +02:00
Altaezio
c0d15dc817 No more feuilles de base si mutation avec feuilles 2026-06-14 16:17:17 +02:00
Altaezio
91855b5b43 Tractor beam 2026-06-14 12:21:31 +02:00
940b3c1553 Dev Démo 1.2
* les plantes se placent désormais sur une grille
* ajouts de curseurs relatifs à l'item
* ajout de settings sur la sensibilité à la souris
* ajout d'un défi en fin de run
2026-06-12 16:42:00 +02:00
5aff9eadaa Dev demo 1.2
* Ajout d'un paramètre de FOV
* Ajout d'un paramètre d'auto pickup des graines
2026-06-08 18:37:55 +02:00
Altaezio
1e2563e328 drag & drop with inventory slots 2026-06-08 12:17:37 +02:00
Altaezio
52ebf0e7d5 hover fix 2026-06-05 14:47:37 +02:00
Altaezio
1b56a648c3 Amélioration du survol des objets 2026-06-04 12:52:19 +02:00
749238b85b Demo 1.1
- Changement du logo pour la démo
- Suppression du flou pour le menu pause (pour cause de performance)
2026-05-31 21:48:48 +02:00
79357982d0 Dev Demo
* pleins de choses et j'ai pas le temps
* mais en gros
* le détecteur détecte les cellules
* ajout du sprite de Demeter
* ajout d'une limite dans la map
* la recharge ne peut plus de nouveau être utilisée après la fin de la région
2026-05-29 16:35:42 +02:00
Altaezio
8b794ee967 vibrating lift bug fix 2026-05-28 17:45:35 +02:00
1d6ff78535 Dev Demo 2
* Ajout des achievement Steam
* Ajout d'une annonce à la récupération d'un artefact et ajout de textes axplicatifs sur les annonces d'artefacts et de mutation
* Fix du léger glitch des tooltips
* Ajout de clarté sur la machine de respawn dans le vaisseau
2026-05-28 15:40:09 +02:00
7b09f2ba7c Dev de la démo
* Modification de l'apparence de l'UI des dialogues
* Changement de l'ordre de déblocage des mutations
* Ajout d'une confirmation pour l'abandon
* Ajout de la scène de fin avec la base Boréa, en tant que fin de démo
* Modification des icône de durée de vie, temps de pousse, et de mort
* Ajout d'un icône au dessus du joueur quand il n'a plus d'énergie
* Amélioration des dialogues du jeu
* Changement du modèle du téléphone
* Ajout de cellule d'énergie et de cellule de talion trouvable sur la carte
* Il est à nouveau possible de se recharger après la fin d'une région
* Buff des mutations ancien sociale et solide
* Modification de la mutation fertile (ne donne de gain de graine qu'à la maturation)
* Ajout d'une récupération automatique des graines
* Ajout de deux cartons de tutoriel ainsi qu'une option pour les revoir dans l'aide de jeu
* Amélioration générale du tutoriel
* Ajout d'un écran titre digne de ce nom
* Lors de l'arrivée à destination, ne téléporte plus le joueur sur une map vide, mais directement dans les lieux de cinématique
* Ajout graphique de plus de pattern de mousse et de roche
* Le talion apparait maintenant sur toute la carte
* La roche peut désormais apparaitre sur la zone de départ
* Ajout dud modificateur de région Canyon
* Equilibrage général
* Fix de bugs en tout genre
2026-05-28 10:18:36 +02:00
7764943714 Fix divers beta 1.4
* Changement du son de récupération d'objet
* Ajout d'une couleur de rareté et suppression de la boucle sur la couleur de rareté
* Changement de la mutation Prolifique : n'ajoute des graines que si mature
* Changement de la mutation Rapide : réduction du debuff de temps de vie par 2
* Modification de la mutation Vivace : Augmentation des points ajoutés
* Les graines données sur des plantes non mature ne mutent plus
* Fix sur la plantation, on ne peut plus planter là où il y a de la roche
* Fix visuel : les particule de vent ne s'affichent plus lorsqu'il pleut
2026-05-28 10:18:34 +02:00
d7ddcf14d1 création et implémentation du sfx de fin de map 2026-05-27 18:51:28 +02:00
485 changed files with 14504 additions and 2362 deletions

View File

@@ -4,10 +4,6 @@
content_margin_top = 5.0
content_margin_bottom = 5.0
bg_color = Color(0, 0, 0, 0.956863)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5

View File

@@ -5,8 +5,13 @@ content_margin_left = 15.0
content_margin_top = 15.0
content_margin_right = 15.0
content_margin_bottom = 15.0
bg_color = Color(1, 1, 1, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
bg_color = Color(0.0627451, 0.05882353, 0.16862746, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.5882353, 0.7019608, 0.85882354, 1)
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10

View File

@@ -23,3 +23,4 @@ func _set(property, what):
else:
name_label_root.show()
return true
return false

View File

@@ -27,15 +27,15 @@ signal variable_was_set(info:Dictionary)
####################################################################################################
func clear_game_state(clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR):
# loading default variables
if ! clear_flag & DialogicGameHandler.ClearFlags.KEEP_VARIABLES:
reset()
# loading default variables
if ! clear_flag & DialogicGameHandler.ClearFlags.KEEP_VARIABLES:
reset()
func load_game_state(load_flag:=LoadFlags.FULL_LOAD):
if load_flag == LoadFlags.ONLY_DNODES:
return
dialogic.current_state_info['variables'] = merge_folder(dialogic.current_state_info['variables'], ProjectSettings.get_setting('dialogic/variables', {}).duplicate(true))
if load_flag == LoadFlags.ONLY_DNODES:
return
dialogic.current_state_info['variables'] = merge_folder(dialogic.current_state_info['variables'], ProjectSettings.get_setting('dialogic/variables', {}).duplicate(true))
#endregion
@@ -54,226 +54,228 @@ func load_game_state(load_flag:=LoadFlags.FULL_LOAD):
## it will try to search for an autoload with the name `Game` and get the value
## of `player_name` to replace it.
func parse_variables(text:String) -> String:
# First some dirty checks to avoid parsing
if not '{' in text:
return text
# First some dirty checks to avoid parsing
if not '{' in text:
return text
# Trying to extract the curly brackets from the text
var regex := RegEx.new()
regex.compile(r"(?<!\\)\{(?<variable>([^{}]|\{[^}]*\})*)\}")
# Trying to extract the curly brackets from the text
var regex := RegEx.new()
regex.compile(r"(?<!\\)\{(?<variable>([^{}]|\{[^}]*\})*)\}")
var parsed := text.replace('\\{', '{')
for result in regex.search_all(text):
var value: Variant = get_variable(result.get_string('variable'), "<NOT FOUND>")
parsed = parsed.replace("{"+result.get_string('variable')+"}", str(value))
var parsed := text.replace('\\{', '{')
for result in regex.search_all(text):
var value: Variant = get_variable(result.get_string('variable'), "<NOT FOUND>")
parsed = parsed.replace("{"+result.get_string('variable')+"}", str(value))
return parsed
return parsed
func set_variable(variable_name: String, value: Variant) -> bool:
variable_name = variable_name.trim_prefix('{').trim_suffix('}')
variable_name = variable_name.trim_prefix('{').trim_suffix('}')
# First assume this is a simple dialogic variable
if has(variable_name):
DialogicUtil._set_value_in_dictionary(variable_name, dialogic.current_state_info['variables'], value)
variable_changed.emit({'variable':variable_name, 'new_value':value})
return true
# First assume this is a simple dialogic variable
if has(variable_name):
DialogicUtil._set_value_in_dictionary(variable_name, dialogic.current_state_info['variables'], value)
variable_changed.emit({'variable':variable_name, 'new_value':value})
return true
# Second assume this is an autoload variable
elif '.' in variable_name:
var from := variable_name.get_slice('.', 0)
var variable := variable_name.trim_prefix(from+'.')
# Second assume this is an autoload variable
elif '.' in variable_name:
var from := variable_name.get_slice('.', 0)
var variable := variable_name.trim_prefix(from+'.')
var autoloads := get_autoloads()
var object: Object = null
if from in autoloads:
object = autoloads[from]
while variable.count("."):
from = variable.get_slice('.', 0)
if from in object and object.get(from) is Object:
object = object.get(from)
variable = variable.trim_prefix(from+'.')
var autoloads := get_autoloads()
var object: Object = null
if from in autoloads:
object = autoloads[from]
while variable.count("."):
from = variable.get_slice('.', 0)
if from in object and object.get(from) is Object:
object = object.get(from)
variable = variable.trim_prefix(from+'.')
if object:
var sub_idx := ""
if '[' in variable:
sub_idx = variable.substr(variable.find('['))
variable = variable.trim_suffix(sub_idx)
sub_idx = sub_idx.trim_prefix('[').trim_suffix(']')
if object:
var sub_idx := ""
if '[' in variable:
sub_idx = variable.substr(variable.find('['))
variable = variable.trim_suffix(sub_idx)
sub_idx = sub_idx.trim_prefix('[').trim_suffix(']')
if variable in object:
match typeof(object.get(variable)):
TYPE_ARRAY:
if not sub_idx:
if typeof(value) == TYPE_ARRAY:
object.set(variable, value)
return true
elif sub_idx.is_valid_float():
object.get(variable).remove_at(int(sub_idx))
object.get(variable).insert(int(sub_idx), value)
return true
TYPE_DICTIONARY:
if not sub_idx:
if typeof(value) == TYPE_DICTIONARY:
object.set(variable, value)
return true
else:
object.get(variable).merge({str_to_var(sub_idx):value}, true)
return true
_:
object.set(variable, value)
return true
if variable in object:
match typeof(object.get(variable)):
TYPE_ARRAY:
if not sub_idx:
if typeof(value) == TYPE_ARRAY:
object.set(variable, value)
return true
elif sub_idx.is_valid_float():
object.get(variable).remove_at(int(sub_idx))
object.get(variable).insert(int(sub_idx), value)
return true
TYPE_DICTIONARY:
if not sub_idx:
if typeof(value) == TYPE_DICTIONARY:
object.set(variable, value)
return true
else:
object.get(variable).merge({str_to_var(sub_idx):value}, true)
return true
_:
object.set(variable, value)
return true
printerr("[Dialogic] Tried setting non-existant variable '"+variable_name+"'.")
return false
printerr("[Dialogic] Tried setting non-existant variable '"+variable_name+"'.")
return false
func get_variable(variable_path:String, default: Variant = null, no_warning := false) -> Variant:
if variable_path.begins_with('{') and variable_path.ends_with('}') and variable_path.count('{') == 1:
variable_path = variable_path.trim_prefix('{').trim_suffix('}')
if variable_path.begins_with('{') and variable_path.ends_with('}') and variable_path.count('{') == 1:
variable_path = variable_path.trim_prefix('{').trim_suffix('}')
# First assume this is just a single variable
var value: Variant = DialogicUtil._get_value_in_dictionary(variable_path, dialogic.current_state_info['variables'])
if value != null:
return value
# First assume this is just a single variable
var value: Variant = DialogicUtil._get_value_in_dictionary(variable_path, dialogic.current_state_info['variables'])
if value != null:
return value
# Second assume this is an expression.
else:
value = dialogic.Expressions.execute_string(variable_path, null, no_warning)
if value != null:
return value
# Second assume this is an expression.
else:
value = dialogic.Expressions.execute_string(variable_path, null, no_warning)
if value != null:
return value
# If everything fails, tell the user and return the default
if not no_warning:
printerr("[Dialogic] Failed parsing variable/expression '"+variable_path+"'.")
return default
# If everything fails, tell the user and return the default
if not no_warning:
printerr("[Dialogic] Failed parsing variable/expression '"+variable_path+"'.")
return default
## Resets all variables or a specific variable to the value(s) defined in the variable editor
func reset(variable:="") -> void:
if variable.is_empty():
dialogic.current_state_info['variables'] = ProjectSettings.get_setting("dialogic/variables", {}).duplicate(true)
else:
DialogicUtil._set_value_in_dictionary(variable, dialogic.current_state_info['variables'], DialogicUtil._get_value_in_dictionary(variable, ProjectSettings.get_setting('dialogic/variables', {})))
if variable.is_empty():
dialogic.current_state_info['variables'] = ProjectSettings.get_setting("dialogic/variables", {}).duplicate(true)
else:
DialogicUtil._set_value_in_dictionary(variable, dialogic.current_state_info['variables'], DialogicUtil._get_value_in_dictionary(variable, ProjectSettings.get_setting('dialogic/variables', {})))
## Returns true if a variable with the given path exists
func has(variable:="") -> bool:
return DialogicUtil._get_value_in_dictionary(variable, dialogic.current_state_info['variables']) != null
return DialogicUtil._get_value_in_dictionary(variable, dialogic.current_state_info['variables']) != null
## Allows to set dialogic built-in variables
func _set(property, value) -> bool:
property = str(property)
var vars: Dictionary = dialogic.current_state_info['variables']
if property in vars.keys():
if typeof(vars[property]) != TYPE_DICTIONARY:
vars[property] = value
return true
if value is VariableFolder:
return true
return false
property = str(property)
var vars: Dictionary = dialogic.current_state_info['variables']
if property in vars.keys():
if typeof(vars[property]) != TYPE_DICTIONARY:
vars[property] = value
return true
if value is VariableFolder:
return true
return false
## Allows to get dialogic built-in variables
func _get(property):
property = str(property)
if property in dialogic.current_state_info['variables'].keys():
if typeof(dialogic.current_state_info['variables'][property]) == TYPE_DICTIONARY:
return VariableFolder.new(dialogic.current_state_info['variables'][property], property, self)
else:
return DialogicUtil.logical_convert(dialogic.current_state_info['variables'][property])
property = str(property)
if property in dialogic.current_state_info['variables'].keys():
if typeof(dialogic.current_state_info['variables'][property]) == TYPE_DICTIONARY:
return VariableFolder.new(dialogic.current_state_info['variables'][property], property, self)
else:
return DialogicUtil.logical_convert(dialogic.current_state_info['variables'][property])
return null
func folders() -> Array:
var result := []
for i in dialogic.current_state_info['variables'].keys():
if dialogic.current_state_info['variables'][i] is Dictionary:
result.append(VariableFolder.new(dialogic.current_state_info['variables'][i], i, self))
return result
var result := []
for i in dialogic.current_state_info['variables'].keys():
if dialogic.current_state_info['variables'][i] is Dictionary:
result.append(VariableFolder.new(dialogic.current_state_info['variables'][i], i, self))
return result
func variables(_absolute:=false) -> Array:
var result := []
for i in dialogic.current_state_info['variables'].keys():
if not dialogic.current_state_info['variables'][i] is Dictionary:
result.append(i)
return result
var result := []
for i in dialogic.current_state_info['variables'].keys():
if not dialogic.current_state_info['variables'][i] is Dictionary:
result.append(i)
return result
#endregion
#region HELPERS
################################################################################
func get_autoloads() -> Dictionary:
var autoloads := {}
for node: Node in get_tree().root.get_children():
autoloads[node.name] = node
return autoloads
var autoloads := {}
for node: Node in get_tree().root.get_children():
autoloads[node.name] = node
return autoloads
func merge_folder(new:Dictionary, defs:Dictionary) -> Dictionary:
# also go through all groups in this folder
for x in new.keys():
if x in defs and typeof(new[x]) == TYPE_DICTIONARY:
new[x] = merge_folder(new[x], defs[x])
# add all new variables
for x in defs.keys():
if not x in new:
new[x] = defs[x]
return new
# also go through all groups in this folder
for x in new.keys():
if x in defs and typeof(new[x]) == TYPE_DICTIONARY:
new[x] = merge_folder(new[x], defs[x])
# add all new variables
for x in defs.keys():
if not x in new:
new[x] = defs[x]
return new
#endregion
#region VARIABLE FOLDER
################################################################################
class VariableFolder:
var data := {}
var path := ""
var outside: DialogicSubsystem
var data := {}
var path := ""
var outside: DialogicSubsystem
func _init(_data:Dictionary, _path:String, _outside:DialogicSubsystem):
data = _data
path = _path
outside = _outside
func _init(_data:Dictionary, _path:String, _outside:DialogicSubsystem):
data = _data
path = _path
outside = _outside
func _get(property:StringName):
property = str(property)
if property in data:
if typeof(data[property]) == TYPE_DICTIONARY:
return VariableFolder.new(data[property], path+"."+property, outside)
else:
return DialogicUtil.logical_convert(data[property])
func _get(property:StringName):
property = str(property)
if property in data:
if typeof(data[property]) == TYPE_DICTIONARY:
return VariableFolder.new(data[property], path+"."+property, outside)
else:
return DialogicUtil.logical_convert(data[property])
return null
func _set(property:StringName, value:Variant) -> bool:
property = str(property)
if not value is VariableFolder:
DialogicUtil._set_value_in_dictionary(path+"."+property, outside.dialogic.current_state_info['variables'], value)
return true
func _set(property:StringName, value:Variant) -> bool:
property = str(property)
if not value is VariableFolder:
DialogicUtil._set_value_in_dictionary(path+"."+property, outside.dialogic.current_state_info['variables'], value)
return true
func has(key:String) -> bool:
return key in data
func has(key:String) -> bool:
return key in data
func folders() -> Array:
var result := []
for i in data.keys():
if data[i] is Dictionary:
result.append(VariableFolder.new(data[i], path+"."+i, outside))
return result
func folders() -> Array:
var result := []
for i in data.keys():
if data[i] is Dictionary:
result.append(VariableFolder.new(data[i], path+"."+i, outside))
return result
func variables(absolute:=false) -> Array:
var result := []
for i in data.keys():
if not data[i] is Dictionary:
if absolute:
result.append(path+'.'+i)
else:
result.append(i)
return result
func variables(absolute:=false) -> Array:
var result := []
for i in data.keys():
if not data[i] is Dictionary:
if absolute:
result.append(path+'.'+i)
else:
result.append(i)
return result
#endregion

View File

@@ -0,0 +1,101 @@
class_name VSTAuthServer
extends Node
signal OnTokenReceived(token: String)
var SERVER_IDENTITY = 'AUTH_SERVER'
var TOKEN_KEY = 'token'
var AUTHENTICATION_REDIRECT_FILE_PATH = 'res://addons/very-simple-twitch/public/index.html'
var _clients: Array[StreamPeerTCP] = []
var _server: TCPServer
func start_server(port: int):
_server = TCPServer.new()
_server.listen(port)
print("Server started")
func stop_server():
if _server:
_server.stop()
_server = null
func _process(_delta: float) -> void:
if !_server:
return
var newConnection = _server.take_connection()
if newConnection:
_clients.push_back(newConnection)
for client in _clients:
if client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
continue
var bytes = client.get_available_bytes()
# after finisher reading bytes clear connection
_clients = _clients.filter(func (item): return item != client)
var requestAsString = client.get_string(bytes)
if requestAsString.length() < 1:
continue
var requestInformation = requestAsString.split('\n')[0].split(' ')
var method = requestInformation[0]
var url = requestInformation[1]
match method:
'GET':
# send html login page
handleGet(client)
'POST':
# handle token extraction
handlePost(client, url)
func handlePost(client: StreamPeer, url: String):
var urlSplitted:PackedStringArray = url.split('?')
if (len(urlSplitted) == 1):
return
var query = urlSplitted[1]
var token = getTokenFromQuery(query)
if !token:
return
OnTokenReceived.emit(token)
send200(client)
stop_server()
queue_free()
func handleGet(client: StreamPeer):
var pageAsString = loadLoginPage()
send200(client, pageAsString)
func getTokenFromQuery(query: String):
var queryKeyValues = query.split('&')
for keyValue in queryKeyValues:
var splittedKeyValue = keyValue.split('=')
var key = splittedKeyValue[0]
if key == TOKEN_KEY:
return splittedKeyValue[1]
func send200(client: StreamPeer, data: String = "", content_type: String = "text/html"):
var dataAsBuffer = data.to_ascii_buffer()
client.put_data(("HTTP/1.1 %d %s\r\n" % [200, 'OK']).to_ascii_buffer())
client.put_data(("Server: %s\r\n" % SERVER_IDENTITY).to_ascii_buffer())
client.put_data(("Content-Length: %d\r\n" % dataAsBuffer.size()).to_ascii_buffer())
client.put_data("Connection: close\r\n".to_ascii_buffer())
client.put_data(("Content-Type: %s\r\n\r\n" % content_type).to_ascii_buffer())
client.put_data(dataAsBuffer)
func loadLoginPage() -> String:
var file = FileAccess.open(AUTHENTICATION_REDIRECT_FILE_PATH, FileAccess.READ)
var loadedFile: String = file.get_as_text()
file.close()
return loadedFile

View File

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

View File

@@ -0,0 +1,145 @@
@tool
extends Control
const MAX_MESSAGES:int = 50
var line: PackedScene = load("res://addons/very-simple-twitch/chat/vst_chat_dock_line.tscn")
var twitch_chat: VSTChat:
get:
if twitch_chat == null:
twitch_chat = VSTChat.new()
add_child(twitch_chat)
return twitch_chat
@onready var support_button: Button = %SupportButton
@onready var channel_line_edit: LineEdit = %ChannelLineEdit
@onready var connect_button: Button = %ConnectButton
@onready var chat_layout: Control = %ChatLayout
@onready var chat_scroll:ScrollContainer = %ChatScroll
@onready var clear_button:Button = %ClearButton
@onready var disconnect_button:Button = %DisconnectButton
func _ready():
support_button.icon = get_theme_icon("Heart", "EditorIcons")
support_button.tooltip_text = "Support me on Ko-fi"
func _on_button_pressed():
twitch_chat.Connected.connect(on_chat_connected)
twitch_chat.OnMessage.connect(create_chatter_msg)
twitch_chat.login_anon(channel_line_edit.text)
connect_button.disabled = true
channel_line_edit.editable = false
func _on_clear_button_pressed():
clear_all_messages()
func _on_line_edit_text_changed(new_text):
connect_button.disabled = len(new_text) == 0
func _on_disconnect_button_pressed():
twitch_chat.disconnect_api()
clear_all_messages()
show_connect_layout()
if twitch_chat.Connected.is_connected(on_chat_connected):
twitch_chat.Connected.disconnect(on_chat_connected)
if twitch_chat.OnMessage.is_connected(create_chatter_msg):
twitch_chat.OnMessage.disconnect(create_chatter_msg)
func _on_support_button_pressed() -> void:
OS.shell_open("https://ko-fi.com/rothiotome?ref=VST")
func on_chat_connected():
create_system_msg("Connected to chat")
show_chat_layout()
func create_system_msg(message: String):
var msg = line.instantiate()
msg.set_chatter_string("[i]"+message+"[/i]")
chat_layout.add_child(msg)
check_scroll()
func create_chatter_msg(chatter: VSTChatter):
var msg = line.instantiate()
var badges: String = await get_badges(chatter)
chatter.message = escape_bbcode(chatter.message)
await add_emotes(chatter)
msg.set_chatter_msg(badges, chatter)
chat_layout.add_child(msg)
check_scroll()
func check_scroll():
var bottom: bool = is_scroll_bottom()
check_number_messages()
await get_tree().process_frame
if bottom: chat_scroll.scroll_vertical = chat_scroll.get_v_scroll_bar().max_value
func check_number_messages():
if chat_layout.get_child_count() > MAX_MESSAGES:
chat_layout.remove_child(chat_layout.get_children()[0])
# TODO: Can't get badges when the connection is annonymous, we should clear this method
func get_badges(chatter: VSTChatter) -> String:
var badges:= ""
for badge in chatter.tags.badges:
var result = await twitch_chat.get_badge(badge, chatter.tags.badges[badge], chatter.tags.user_id)
if result:
badges += "[img=center]" + result.resource_path + "[/img] "
return badges
func add_emotes(chatter: VSTChatter):
if chatter.tags.emotes.is_empty(): return
var locations: Array = []
for emote in chatter.tags.emotes:
for data in chatter.tags.emotes[emote].split(","):
var start_end = data.split("-")
locations.append(VSTEmoteLocation.new(emote, int(start_end[0]), int(start_end[1])))
locations.sort_custom(Callable(VSTEmoteLocation, "smaller"))
var offset = 0
for loc in locations:
var result = await twitch_chat.get_emote(loc.id)
var emote_string = "[img=center]" + result.resource_path +"[/img]"
var pre: String = chatter.message.substr(0, loc.start + offset)
var post: String = chatter.message.substr(loc.end + offset + 1)
chatter.message = pre + emote_string + post
offset += emote_string.length() + loc.start - loc.end - 1
func is_scroll_bottom() -> bool:
var scroll_bar = chat_scroll.get_v_scroll_bar()
return chat_scroll.scroll_vertical >= scroll_bar.max_value - scroll_bar.get_rect().size.y
# Returns escaped BBCode that won't be parsed by RichTextLabel as tags.
func escape_bbcode(bbcode_text) -> String:
return bbcode_text.replace("[", "[lb]")
func clear_all_messages():
for childen in chat_layout.get_children():
chat_layout.remove_child(childen)
func show_chat_layout():
disconnect_button.visible = true
clear_button.visible = true
channel_line_edit.visible = false
connect_button.visible = false
func show_connect_layout():
disconnect_button.visible = false
clear_button.visible = false
channel_line_edit.editable = true
channel_line_edit.visible = true
connect_button.visible = true
connect_button.disabled = false

View File

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

View File

@@ -0,0 +1,111 @@
[gd_scene load_steps=4 format=3 uid="uid://c8jard0jvmfmt"]
[ext_resource type="Script" path="res://addons/very-simple-twitch/chat/vst_chat_dock.gd" id="1_fkddh"]
[sub_resource type="Image" id="Image_dfhsm"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_lqqxv"]
image = SubResource("Image_dfhsm")
[node name="VstChatDock" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_fkddh")
[node name="TabContainer" type="TabContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
current_tab = 0
[node name="Chat" type="VBoxContainer" parent="TabContainer"]
layout_mode = 2
metadata/_tab_index = 0
[node name="HBoxContainer" type="HBoxContainer" parent="TabContainer/Chat"]
layout_mode = 2
[node name="ChannelLineEdit" type="LineEdit" parent="TabContainer/Chat/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Channel name"
[node name="ConnectButton" type="Button" parent="TabContainer/Chat/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
disabled = true
text = "CONNECT"
[node name="ClearButton" type="Button" parent="TabContainer/Chat/HBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
text = "CLEAR CHAT"
[node name="DisconnectButton" type="Button" parent="TabContainer/Chat/HBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
text = "DISCONNECT"
[node name="VBoxContainer" type="VBoxContainer" parent="TabContainer/Chat"]
layout_mode = 2
size_flags_vertical = 3
[node name="Chat" type="Panel" parent="TabContainer/Chat/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="ChatScroll" type="ScrollContainer" parent="TabContainer/Chat/VBoxContainer/Chat"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="ChatLayout" type="VBoxContainer" parent="TabContainer/Chat/VBoxContainer/Chat/ChatScroll"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -8.0
offset_bottom = 33.0
grow_horizontal = 0
alignment = 2
[node name="SupportButton" type="Button" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 8
tooltip_text = "Support me on Ko-fi"
icon = SubResource("ImageTexture_lqqxv")
flat = true
[connection signal="text_changed" from="TabContainer/Chat/HBoxContainer/ChannelLineEdit" to="." method="_on_line_edit_text_changed"]
[connection signal="pressed" from="TabContainer/Chat/HBoxContainer/ConnectButton" to="." method="_on_button_pressed"]
[connection signal="pressed" from="TabContainer/Chat/HBoxContainer/ClearButton" to="." method="_on_clear_button_pressed"]
[connection signal="pressed" from="TabContainer/Chat/HBoxContainer/DisconnectButton" to="." method="_on_disconnect_button_pressed"]
[connection signal="pressed" from="HBoxContainer/SupportButton" to="." method="_on_support_button_pressed"]

View File

@@ -0,0 +1,9 @@
@tool
extends HBoxContainer
func set_chatter_msg(badges: String, chatter: VSTChatter):
set_chatter_string("%02d:%02d" %[chatter.date_time_dict["hour"], chatter.date_time_dict["minute"]] + " " + badges + " [b][color="+ chatter.tags.color_hex + "]" +chatter.tags.display_name +"[/color][/b]: " + chatter.message)
func set_chatter_string(message:String):
$RichTextLabel.text = message
queue_sort()

View File

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

View File

@@ -0,0 +1,16 @@
[gd_scene load_steps=2 format=3 uid="uid://bo1un8cwcrpqr"]
[ext_resource type="Script" path="res://addons/very-simple-twitch/chat/vst_chat_dock_line.gd" id="1_h0lnd"]
[node name="VstChatDockLine" type="HBoxContainer"]
offset_right = 1.0
offset_bottom = 115.0
size_flags_horizontal = 3
script = ExtResource("1_h0lnd")
[node name="RichTextLabel" type="RichTextLabel" parent="."]
layout_mode = 2
size_flags_horizontal = 3
bbcode_enabled = true
fit_content = true
scroll_active = false

View File

@@ -0,0 +1,32 @@
# Errors
## Information
This page is to explain the use and implementation of errors in VST. Errors have 3 parameters for code, description and extra information.
* error_code (enum @see VST_Error.VST_Code_Error) -> This parameter groups the types of errors in a general way.
* description (String) -> The description of the error is a general description of the general grouping in human language.
* info (String) -> The information is the particular description of the error. It should be used to add extra information about the particular error.
## New errors
To add new errors you must first ask yourself if it is necessary to add a new code or not. If necessary add the code in the VST_Error.VST_Code_Error enumeration.
To illustrate the error let's imagine that we have a function that updates the area of a triangle given a base and a height. This function updates an area parameter only if it is possible to calculate the area (the base or the height is greater than 0).
```GDScript
func calculateArea(base:int, heigth:int):
if base <= 0:
var error:VST_Error = VST_Error.new(VST_Error.VST_Code_Error.PARAM_ERROR, "base can't be less than or equals 0")
push_warning(str(error))
area = 0
elif height <= 0:
var error:VST_Error = VST_Error.new(VST_Error.VST_Code_Error.PARAM_ERROR, "height can't be less than or equals 0")
push_warning(str(error))
area = 0
else
area = (base*height)/2
```
## Error Codes
* PARAM_ERROR Use this code for errors that have to do with invalid parameters. An int that should be a String or a String that cannot be empty.
* TIMEOUT_ERROR Use this code when a timer runs out or there is no response from a system.
* NETWORK_ERROR Use this code when there is a network error ( > 400 and < 500)
* SERVER_ERROR Use this code when, in a network communication, the server is down or has a problem ( > 500 ).

View File

@@ -0,0 +1,46 @@
# Network
## Information
The motivation for using this wrapper is to make easier the call APIs and gather responses and errors from the server. The idea is to make it as verbose as possible using a builder pattern so the request can be read at first glance.
Before making the network call, the wrapper will check a number of conditions within the "check_request_data" method which will throw a number of errors or warnings if the request is not well built. The wrapper is permissive with the definition of REST APIs, so you can have a GET call with body or a POST with GET parameters in the url (TBD).
Note: To achieve this effect, it is necessary to call new() and pass a node to the "launch_request" method, which will self manage all that's necessary for it to work.
## Usage
To use the wrapper for network requests simply build a Network_Call object and start configuring it with the to, with, etc... methods.
* to (String) -> url destination
* with (String) -> request body
* add_get_param (String,String) y add_all_get_param(Dictionary) -> add get params to request
* add_header(String,String) y add_all_header(Dictionary) -> add headers to request
* verb(Http_Method) -> set the method for REST request
* in_time(int) -> set the timeout time to the request
* set_on_call_success (Callable) -> call this function if the request was ok (200)
* set_on_call_fail (Callable) -> call this function if the request was fail (>400)
* no_cache -> set no cache for request ( only used in GET requests )
## Example
```GDScript
func _onReady():
Network_Call.new().to("https://catfact.ninja/fact").set_on_call_success(on_cat_fact).set_on_call_fail(on_error).launch_request(self)
func on_cat_fact(result):
$Label.text = str(result)
func on_error(error):
$Label.text = "Error requesting fact about cats :("
```
## Cache
The network module has a cache for GET requests to save time and bandwidth for similar requests in 'short' time spans.
Since nodes are ephemeral (network requests are nodes that disappear) the cache is stored on disk and not in memory (this resposability was left for the upper layers).
The cache works as follows:
* 1. The request is hashed ( using the url )
* 2. A file with the specific name ( the hash ) is searched for on disk.
* 3.a If it exists and it has not passed 300 seconds ( arbitrary and improvable time using an etag? ) the request is resolved and returns the file content.
* 3.b If the file does not exist or it has been more than 300 seconds, the request is made and cached using the same hash.
The time validity of this cache is on CACHE_TIME_IN_SECONDS constant in network_call.gd
The cached content is an array of bytes due to the heterogeneous nature of the possible requests (text, images, sound...).

View File

@@ -0,0 +1,21 @@
# Testing
## Adding test
Use "assert_eq" for "primitive" values and "assert_eq_deep" for complex data
For add some test be sure:
1. Your test are inside /test folder
2. Your test file starts with test name
3. Your test script extends from "GutTest"
4. Your test methods starts also with test
You can use some useful methods for testing like:
* before_all -> Excecuted before all tests in the script
* before_each -> Excecuted before each test in the script
* after_each -> Excecuted after each test in the script
* after_all -> Excecuted after all tests in the script
## GUT Documentation
https://gut.readthedocs.io/en/latest/

View File

@@ -0,0 +1,32 @@
# Develop
## GDLint
The idea behind installing a linter in this plugin is mainly code readability. That can make collaboration easier for other developers. Using the same consistent coding style makes it easier to collaborate with others and easier to understand what the plugin is doing.
### Installation
There are a few steps before you can use GDLint. You can consult the official documentation, but here's a summary
1. GDLinter is installed on addons folders. You don't need do nothing with it.
2. Deactivate GDLint from pluggins ( project -> configuration -> plugins )
3. Check your python version using 'python --version' or 'py --version'
- no version installed?
- Check windows store for windows ( best option in my opinion )
- On Mac 'brew install python' using Homebrew
- On Linux, you can use APT 'sudo apt install python3'
4. Install godot toolkit using 'pip3 install "gdtoolkit==4.*"'
- No pip installed? Download the script, from https://bootstrap.pypa.io/get-pip.py and type 'python get-pip.py' to install it.
5. Check gdlint version with 'gdlint --version'
- Nothing or error showed? try repeating the steps from 2
6. Activate GdLint again.
- If GDLint menu at the bottom doesnt appear, relaunch godot.
GDScript Toolkit Documentation -> https://github.com/Scony/godot-gdscript-toolkit
GDLinter Addon Documentation -> https://github.com/el-falso/gdlinter/
### Usage
As soon as you install the plugin and the toolkit you will see a menu at the bottom called GDLint. There it will show the problems with the code :)
![](img/gdlint-usage-1.png?raw=true)

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://beqmxv0pbk5dp"
path="res://.godot/imported/gdlint-usage-1.png-06a3e1d7a5f85d1fd83e80b21ea14d73.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/very-simple-twitch/doc/img/gdlint-usage-1.png"
dest_files=["res://.godot/imported/gdlint-usage-1.png-06a3e1d7a5f85d1fd83e80b21ea14d73.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

View File

@@ -0,0 +1,62 @@
@tool
extends Control
@onready var copy_button: Button = %CopyButton
@onready var redirect_uri = %RedirectURI
@onready var client_id_line_edit = %ClientIDLineEdit
@onready var client_id_warning = %ClientIDWarning
@onready var help_icon = %HelpIcon
@onready var warning_icon = %WarningIcon
func _ready():
help_icon.texture = get_theme_icon("Help", "EditorIcons")
warning_icon.texture = get_theme_icon("StatusWarning", "EditorIcons")
copy_button.icon = get_theme_icon("ActionCopy", "EditorIcons")
copy_button.tooltip_text = "Copy Redirect URL to clipboard"
client_id_warning.add_theme_color_override(
"font_color",
EditorInterface.get_editor_settings()\
.get_setting("text_editor/theme/highlighting/comment_markers/warning_color")
)
visibility_changed.connect(on_visibility_changed)
func on_visibility_changed():
if visible:
update_visuals()
ProjectSettings.settings_changed.connect(update_visuals)
else:
if ProjectSettings.settings_changed.is_connected(update_visuals):
ProjectSettings.settings_changed.disconnect(update_visuals)
func update_visuals():
var client_id = VSTSettings.get_setting(VSTSettings.settings.client_id)
var redirect_host = VSTSettings.get_setting(VSTSettings.settings.redirect_host)
var redirect_port = VSTSettings.get_setting(VSTSettings.settings.redirect_port)
var uuid = VSTSettings.get_setting(VSTSettings.settings.uuid)
redirect_uri.text = redirect_host + str(redirect_port) + "/" + uuid
client_id_line_edit.text = client_id
if client_id == "":
client_id_warning.show()
warning_icon.show()
else:
client_id_warning.hide()
warning_icon.hide()
func copy_redirect_uri():
DisplayServer.clipboard_set(redirect_uri.text)
func client_id_submitted():
ProjectSettings.set_setting(
"very_simple_twitch/"+VSTSettings.settings.client_id.path,
client_id_line_edit.text
)
ProjectSettings.save()
func open_url(meta):
OS.shell_open(meta)

View File

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

View File

@@ -0,0 +1,111 @@
[gd_scene load_steps=4 format=3 uid="uid://m77ohpfa7qef"]
[ext_resource type="Script" path="res://addons/very-simple-twitch/dock/vst-dock.gd" id="1_kyfdh"]
[sub_resource type="Image" id="Image_cb0pc"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_oh773"]
image = SubResource("Image_cb0pc")
[node name="vst-dock" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_kyfdh")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="HelpIcon" type="TextureRect" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
texture = SubResource("ImageTexture_oh773")
stretch_mode = 5
[node name="RichTextLabel2" type="RichTextLabel" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 2
bbcode_enabled = true
text = " [url=https://github.com/rothiotome/godot-very-simple-twitch?tab=readme-ov-file#quick-setup]Quick Setup section in the readme[/url] and follow the steps in there.
"
fit_content = true
selection_enabled = true
[node name="Label" type="Label" parent="."]
layout_mode = 2
text = "Quick Start Guide"
[node name="RedirectURI" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Label" type="Label" parent="RedirectURI"]
layout_mode = 2
size_flags_horizontal = 3
text = "Redirect URI: "
[node name="RedirectURIContainer" type="HBoxContainer" parent="RedirectURI"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
[node name="RedirectURI" type="RichTextLabel" parent="RedirectURI/RedirectURIContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 2
text = "Value"
fit_content = true
selection_enabled = true
[node name="CopyButton" type="Button" parent="RedirectURI/RedirectURIContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Copy Redirect URL to clipboard"
icon = SubResource("ImageTexture_oh773")
[node name="ClientID" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Label" type="Label" parent="ClientID"]
layout_mode = 2
size_flags_horizontal = 3
text = "Client ID:"
[node name="ClientIDLineEdit" type="LineEdit" parent="ClientID"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
placeholder_text = "YOUR CLIENT ID HERE"
[node name="Warning" type="HBoxContainer" parent="."]
layout_mode = 2
alignment = 1
[node name="WarningIcon" type="TextureRect" parent="Warning"]
unique_name_in_owner = true
layout_mode = 2
texture = SubResource("ImageTexture_oh773")
stretch_mode = 5
[node name="ClientIDWarning" type="Label" parent="Warning"]
unique_name_in_owner = true
visible = false
layout_mode = 2
theme_override_colors/font_color = Color(0.72, 0.61, 0.48, 1)
text = "To use VST, you need a Client ID. Check the readme above for how to get one."
[connection signal="meta_clicked" from="HBoxContainer/RichTextLabel2" to="." method="open_url"]
[connection signal="pressed" from="RedirectURI/RedirectURIContainer/CopyButton" to="." method="copy_redirect_uri"]
[connection signal="focus_exited" from="ClientID/ClientIDLineEdit" to="." method="client_id_submitted"]
[connection signal="text_submitted" from="ClientID/ClientIDLineEdit" to="." method="client_id_submitted"]

View File

@@ -0,0 +1,15 @@
class_name VSTEmoteLocation
extends RefCounted
var id : String
var start : int
var end : int
func _init(emote_id, start_idx, end_idx):
self.id = emote_id
self.start = start_idx
self.end = end_idx
static func smaller(a: VSTEmoteLocation, b: VSTEmoteLocation):
return a.start < b.start

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://crhtjoakp6j05"
path="res://.godot/imported/icon.png-952ca81aead1a8efe9432e636ee0d5ac.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/very-simple-twitch/icon.png"
dest_files=["res://.godot/imported/icon.png-952ca81aead1a8efe9432e636ee0d5ac.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

View File

@@ -0,0 +1,19 @@
class_name VSTChatter
var date_time_dict: Dictionary
var login: String
var channel: String
var message: String
var tags: VSTIRCTags
func is_mod() -> bool:
return tags.badges.has("moderator")
func is_sub() -> bool:
return tags.badges.has("subscriber")
func is_broadcaster() -> bool:
return tags.badges.has("broadcaster")

View File

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

View File

@@ -0,0 +1,13 @@
class_name VSTIRCTags
# Model for a IRC twitch chat
var color_hex: String # color of user used in twtich chat
var display_name: String # name of a user
var channel_id: String # not used
var user_id: String # numeric id of the user used in twitch
var badges: Dictionary # badges of the user in message
var emotes: Dictionary # emotes writed by user in message
func _to_string():
return "color_hex: %s, display_name: %s, channel_id: %s, user_id: %s, badges: %s, emotes: %s" % [color_hex, display_name, str(channel_id), str(user_id), badges, emotes]

View File

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

View File

@@ -0,0 +1,5 @@
class_name VSTChannel
var login: String
var id: String
var token: String

View File

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

View File

@@ -0,0 +1,198 @@
@tool
class_name VSTNetwork_Call extends Node
const CACHE_TIME_IN_SECONDS = 300
# TODO: add dynamic version here
const USER_AGENT = {"User-Agent": "VSTC/0.1.0 (Godot Engine)"}
const CACHE_PATH = "user://very-simple-chat/cache"
var url: String
var timeout: float
var body: String
var headers: Dictionary
var get_params: Dictionary
var on_call_success: Callable
var on_call_fail: Callable
var method: HTTPClient.Method
var use_cache: bool
func _init():
headers = {}
get_params = {}
timeout = 0.0
use_cache = true
func to(url_request: String) -> VSTNetwork_Call:
url = url_request
return self
func with(body_object) -> VSTNetwork_Call:
body = JSON.stringify(body_object)
return self
func in_time(timeout_request: float) -> VSTNetwork_Call:
timeout = timeout_request
return self
func verb(method_request: HTTPClient.Method) -> VSTNetwork_Call:
method = method_request
return self
func no_cache() -> VSTNetwork_Call:
use_cache = false
return self
func add_header(key_header: String, value_header: String) -> VSTNetwork_Call:
headers[key_header] = value_header
return self
func add_all_headers(headers_dic: Dictionary) -> VSTNetwork_Call:
for key in headers_dic:
headers[key] = headers_dic[key]
return self
func add_get_param(key_get_param:String, value_get_param) -> VSTNetwork_Call:
get_params[key_get_param] = str(value_get_param)
return self
func add_all_get_params(get_params_dic: Dictionary) -> VSTNetwork_Call:
for key in get_params_dic:
get_params[key] = get_params_dic[key]
return self
func set_on_call_success(on_call_success_request: Callable) -> VSTNetwork_Call:
on_call_success = on_call_success_request
return self
func set_on_call_fail(on_call_fail_request: Callable) -> VSTNetwork_Call:
on_call_fail = on_call_fail_request
return self
func _pile_headers(headers_to_pile: Dictionary) -> PackedStringArray:
var array:PackedStringArray = []
for key in headers_to_pile:
array.append("%s: %s" % [key, headers_to_pile[key]])
for key in USER_AGENT:
array.append("%s: %s" % [key, USER_AGENT[key]])
return array
func _compile_url(host: String, params_to_pile: Dictionary) -> String:
var final_url:String = host.strip_edges()+"?"
for key in params_to_pile:
var value_safe = str(params_to_pile[key]).uri_encode()
var key_safe = str(key).uri_encode()
final_url += "&%s=%s" % [key_safe, value_safe]
return final_url
func launch_request(parent: Node):
if method == HTTPClient.Method.METHOD_GET:
var final_url = _compile_url(url, get_params)
if use_cache:
var cached_data = read_from_cache(get_key_from_url(final_url))
if !cached_data.is_empty() && on_call_success != null:
on_call_success.call(cached_data)
return
_launch_network_request(parent)
func _launch_network_request(parent: Node):
var data_error = check_request_data()
if data_error != "":
var error:VSTError = VSTError.new(VSTError.VSTCodeError.PARAM_ERROR, data_error)
on_call_fail.call(error)
return
parent.add_child(self)
await get_tree().process_frame
var final_url = _compile_url(url, get_params)
var client = HTTPRequest.new()
client.timeout = timeout
client.request_completed.connect(func():
on_request_completed.bind(final_url)
client.queue_free()
)
add_child(client)
await get_tree().process_frame
var request_error = client.request(final_url, _pile_headers(headers), method, body)
if request_error != OK:
var error:VSTError = VSTError.new(VSTError.VSTCodeError.PARAM_ERROR, "The request can't be archieved reason: "+str(request_error))
on_call_fail.call(error)
client.queue_free()
return
func check_request_data() -> String:
if timeout < 0.0:
push_warning("Timeout can't be less than 0. Setted to 0")
timeout = 0
if !method:
method = HTTPClient.Method.METHOD_GET
if url == null or url.strip_edges() == "":
return "Url can't be empty"
return ""
func get_key_from_url(url:String) -> String:
var last_part_url:String = url.substr(url.rfind("/"))
return last_part_url.sha256_text()
func on_request_completed(_result: int, status: int, _headers: PackedStringArray, body: PackedByteArray, url_request: String):
if (status >= 200 and status < 400):
if method == HTTPClient.Method.METHOD_GET: update_cache(body, get_key_from_url(url_request))
if on_call_success: on_call_success.call(body)
elif (status >= 400 and status < 500):
if on_call_fail:
var info = "%s -> %s" % [str(status), body.get_string_from_utf8()]
var error:VSTError = VSTError.new(VSTError.VSTCodeError.NETWORK_ERROR, info)
on_call_fail.call(error)
elif (status >= 500):
if on_call_fail:
var info = "%s -> %s" % [str(status), body.get_string_from_utf8()]
var error:VSTError = VSTError.new(VSTError.VSTCodeError.SERVER_ERROR, info)
on_call_fail.call(error)
call_deferred("queue_free")
func read_from_cache(key:String) -> PackedByteArray:
var filename: String = CACHE_PATH.path_join(key)
if FileAccess.file_exists(filename): # is a hit on cache?
if FileAccess.get_modified_time(filename)+CACHE_TIME_IN_SECONDS > Time.get_unix_time_from_system(): # is expired?
return FileAccess.get_file_as_bytes(filename)
return []
func update_cache(content:PackedByteArray, key:String):
var filename: String = CACHE_PATH.path_join(key)
DirAccess.make_dir_recursive_absolute(filename.get_base_dir())
var file = FileAccess.open(filename, FileAccess.WRITE)
file.store_buffer(content)
file.close()
func clear_cache():
DirAccess.make_dir_recursive_absolute(CACHE_PATH)
var files:PackedStringArray = DirAccess.get_files_at(CACHE_PATH)
for file in files:
DirAccess.remove_absolute(CACHE_PATH.path_join(file))

View File

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

View File

@@ -0,0 +1,74 @@
class_name VSTParseHelper
# Parse login name from payload substring of twitch irc chat
static func parse_login(input_string:String) -> String:
return get_substring(input_string, ":", "!")
# Parse channel name from payload substring of twitch irc chat
static func parse_channel(input_string:String) -> String:
return input_string.trim_prefix("#")
# Parse message from payload substring of twitch irc chat
static func parse_message(input_string:String) -> String:
return input_string.trim_prefix(":").strip_edges()
static func parse_tags(input_string:String) -> VSTIRCTags:
var irc_tags = VSTIRCTags.new()
var tags:PackedStringArray = input_string.split(";")
for i in len(tags):
var splitted_tag:PackedStringArray = tags[i].split("=")
if splitted_tag.size() <= 1: continue
match(splitted_tag[0].strip_edges()):
"badges":
irc_tags.badges = parse_badges(splitted_tag[1].split(","))
"color":
irc_tags.color_hex = splitted_tag[1]
"display-name":
irc_tags.display_name = splitted_tag[1]
"emotes":
irc_tags.emotes = parse_emotes(splitted_tag[1].split("/"))
"room-id":
irc_tags.user_id = splitted_tag[1]
return irc_tags
# Parse badges from payload substring of twitch irc chat. Returns a dictionary with the badge itself
# and the position of the badge
static func parse_badges(input:PackedStringArray) -> Dictionary:
var badges: Dictionary = {}
if input.is_empty() || input[0].is_empty(): return badges
for i in len(input):
var substrings = input[i].split("/")
if len(substrings) > 1:
badges[substrings[0]] = substrings[1]
return badges
# Parse emotes from payload substring of twitch irc chat. Returns a dictionary with the emote
# itself and the position in the user message in order to replace the text with the image emote
static func parse_emotes(input:PackedStringArray) -> Dictionary:
var emotes: Dictionary = {}
if input.is_empty() || input[0].is_empty(): return emotes
for emote in input:
var substring: PackedStringArray = emote.split(":")
if len(substring) > 1:
emotes[substring[0]] = substring[1]
return emotes
static func get_substring(input_string:String, starting_char:String, ending_char:String) -> String:
var first_index = input_string.find(starting_char)
var last_index = input_string.find(ending_char)
if first_index == -1 or last_index == -1 or last_index < first_index:
return input_string
return input_string.substr(first_index + 1, last_index - first_index - 1)

View File

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

View File

@@ -0,0 +1,7 @@
[plugin]
name="Very Simple Twitch"
description="Godot websocket implementation for Twitch Chat using OICD Authentication flow"
author="RothioTome & collaborators"
version="0.1.0"
script="vst.gd"

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='cache-control' content='no-cache'>
<meta http-equiv='expires' content='0'>
<meta http-equiv='pragma' content='no-cache'>
</head>
<body>
<h1 id="title">Login...</h1>
<script>
(async () => {
const AUTH_URL = 'http://localhost:8090';
const hashKeyValue = location.hash.replace('#', '&').split('&');
const tokenKeyValue = hashKeyValue.find((item) => item.split('=')[0] === 'access_token');
const token = tokenKeyValue.split('=')[1];
fetch(AUTH_URL + `?token=${token}`, { method: 'POST' }).then((e) => {document.getElementById("title").innerHTML = "Everything seems to be OK. You can close this window.";} ).catch((error) => {document.getElementById("title").innerHTML = "ERROR: "+JSON.stringify(error);});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,164 @@
class_name VSTAPI
extends Node
signal token_received(TwitchChannel)
const RESPONSE_TYPE = 'token'
const TWITCH_VALIDATE_URL = "https://id.twitch.tv/oauth2/validate"
const TWITCH_BAN_URL = "https://api.twitch.tv/helix/moderation/bans"
const TWITCH_OAUTH_URL = "https://id.twitch.tv/oauth2/authorize"
const TWITCH_VIP_URL = "https://api.twitch.tv/helix/channels/vips"
const TWITCH_SETTINGS_URL = "https://api.twitch.tv/helix/chat/settings"
const TWITCH_CHATTERS_URL = "https://api.twitch.tv/helix/chat/chatters"
var auth_server: VSTAuthServer
var _scopes: PackedStringArray
var _client_id: String
var _user: VSTChannel
func initiate_twitch_auth():
_scopes = VSTSettings.get_setting(VSTSettings.settings.scopes)
_client_id = VSTSettings.get_setting(VSTSettings.settings.client_id)
var redirect_host = VSTSettings.get_setting(VSTSettings.settings.redirect_host)
var redirect_port = VSTSettings.get_setting(VSTSettings.settings.redirect_port)
var uuid = VSTSettings.get_setting(VSTSettings.settings.uuid)
if auth_server:
disconnect_api()
auth_server = VSTAuthServer.new()
add_child(auth_server)
auth_server.OnTokenReceived.connect(_on_auth_server_on_token_received)
auth_server.start_server(redirect_port)
var redirect_uri = redirect_host + str(redirect_port) + "/" + uuid
var scopes_string = "+".join(_scopes)
var url = "client_id=" + _client_id
url += "&redirect_uri=" +redirect_uri
url += "&response_type=" + RESPONSE_TYPE
url += "&scope=" + scopes_string
OS.shell_open(TWITCH_OAUTH_URL + "?" + url)
func _on_auth_server_on_token_received(token) -> void:
var validated_user = await validate_token_and_get_user_id(token)
if !(validated_user):
print('Invalid token')
_user = null
return
_user = validated_user
token_received.emit(_user)
func request_fail(status:int, error: VSTError, on_fail: Callable):
if status == 401 or status == 403:
#Unauthorized? No mi ciela
initiate_twitch_auth()
# TODO: should chain the request?
else:
push_warning(error)
on_fail.call()
func validate_token_and_get_user_id(token: String):
var client = HTTPRequest.new()
add_child(client)
client.request(TWITCH_VALIDATE_URL, [
'Authorization: OAuth ' + token
])
var result = await client.request_completed
var status = result[1]
if status != 200:
return null
var data = (result[3] as PackedByteArray).get_string_from_utf8()
var data_parsed = JSON.parse_string(data)
var user = VSTChannel.new()
user.id = data_parsed['user_id']
user.login = data_parsed['login']
user.token = token
client.queue_free()
return user
func timeout_user(user_to_ban_id: String, duration: int = 1, reason: String = '',
on_success: Callable = Callable(), on_fail: Callable = Callable()) -> void:
if !_user:
return
var timeout_duration = max(duration, 1)
var body = {
data = {
user_id = user_to_ban_id,
duration = timeout_duration,
reason = reason
},
}
var vst = VSTNetwork_Call.new()
vst.to(TWITCH_BAN_URL)
vst.add_all_get_params({
'broadcaster_id': _user.id,
'moderator_id': _user.id
}).\
vst.with(body)
vst.verb(HTTPClient.METHOD_POST)
vst.add_all_headers({
'Client-Id: ' : _client_id,
'Authorization': 'Bearer ' + _user.token,
'Content-Type': 'application/json'
})
vst.set_on_call_success(on_success)
vst.set_on_call_fail(request_fail.bind(on_fail))
vst.launch_request(self)
func add_vip(user_to_vip_id: String, on_success: Callable = Callable(),
on_fail: Callable = Callable()):
if !_user:
return
var vst = VSTNetwork_Call.new()
vst.to(TWITCH_VIP_URL)
vst.add_all_get_params({
'broadcaster_id': _user.id,
'user_id': user_to_vip_id
})
vst.verb(HTTPClient.METHOD_POST)
vst.add_all_headers({
'Client-Id: ' : _client_id,
'Authorization': 'Bearer ' + _user.token,
'Content-Type': 'application/json'
})
vst.set_on_call_success(on_success)
vst.set_on_call_fail(request_fail.bind(on_fail))
vst.launch_request(self)
func remove_vip(user_to_remove_vip_id: String, on_success: Callable = Callable(),
on_fail: Callable = Callable()) -> void:
if !_user:
return
var vst = VSTNetwork_Call.new()
vst.to(TWITCH_VIP_URL)
vst.add_all_get_params({
'broadcaster_id': _user.id,
'user_id': user_to_remove_vip_id
})
vst.verb(HTTPClient.METHOD_DELETE)
vst.add_all_headers({
'Client-Id: ' : _client_id,
'Authorization': 'Bearer ' + _user.token,
'Content-Type': 'application/json'
})
vst.set_on_call_success(on_success)
vst.set_on_call_fail(request_fail.bind(on_fail))
vst.launch_request(self)
func disconnect_api():
if auth_server:
auth_server.stop_server()
remove_child(auth_server)

View File

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

View File

@@ -0,0 +1,285 @@
@tool
class_name VSTChat extends Node
# TODO: rename to past simple?
signal Connected(_channel)
signal OnMessage(chatter: VSTChatter)
var _channel: VSTChannel
var _chatClient: WebSocketPeer
var _hasConnected:= false
enum RequestType {
EMOTE,
BADGE,
BADGE_MAPPING
}
var caches := {
RequestType.EMOTE: {},
RequestType.BADGE: {},
RequestType.BADGE_MAPPING: {}
}
var _client_id: String
var _twitch_chat_url: String
var _twitch_chat_port: int
var _use_cache: bool
var _cache_path: String
var _use_anon_connection:= false
var _chat_queue : Array[String] = []
var _last_msg : int = Time.get_ticks_msec()
var _chat_timeout_ms: int
const USER_AGENT : String = "User-Agent: VSTC/0.1.0 (Godot Engine)"
func _process(_delta: float):
if !_chatClient:
return
_chatClient.poll()
var state = _chatClient.get_ready_state()
match state:
WebSocketPeer.STATE_OPEN:
if (!_hasConnected):
onChatConnected()
while _chatClient.get_available_packet_count():
onReceivedData(_chatClient.get_packet())
if !_chat_queue.is_empty() and _last_msg + (_last_msg + _chat_timeout_ms) <= Time.get_ticks_msec():
_chatClient.send_text(_chat_queue.pop_front())
_last_msg = Time.get_ticks_msec()
WebSocketPeer.STATE_CLOSED:
if _hasConnected:
_hasConnected = false
var code = _chatClient.get_close_code()
var reason = _chatClient.get_close_reason()
print('Disconnected from twitch chat')
print("WebSocket closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1])
print("Reconnecting")
start_chat_client()
func start_chat_client():
get_settings()
if _chatClient:
_chatClient.close()
_chatClient = WebSocketPeer.new()
_chatClient.connect_to_url("%s:%d" % [_twitch_chat_url, _twitch_chat_port])
func login_anon(channel_name: String):
_channel = VSTChannel.new()
_channel.login = channel_name.to_lower()
_use_anon_connection = true
start_chat_client()
func login(twitch_channel: VSTChannel):
_channel = twitch_channel
start_chat_client()
func onChatConnected():
if !_channel:
return
_hasConnected = true
_chatClient.send_text("CAP REQ :twitch.tv/tags twitch.tv/commands")
if _use_anon_connection:
_chatClient.send_text('PASS ' + VSTSettings.get_setting(VSTSettings.settings.twitch_anon_pass))
_chatClient.send_text('NICK ' + VSTSettings.get_setting(VSTSettings.settings.twitch_anon_user))
else:
_chatClient.send_text('PASS oauth:' + _channel.token)
_chatClient.send_text('NICK ' + _channel.login.to_lower())
pass
_chatClient.send_text('JOIN ' + '#' + _channel.login.to_lower())
Connected.emit()
func send_message(message: String):
_chat_queue.append("PRIVMSG #" + _channel.login.to_lower() + " :" + message + "\r\n")
func onReceivedData(payload: PackedByteArray):
var message = payload.get_string_from_utf8()
var splittled_messages = message.split("\n")
for n in splittled_messages:
handle_message(n)
#TODO: move this to parse helper?
func parse_message_from_twtich_IRC(message: String) -> PackedStringArray:
return message.split(" ", true, 4) # We might need more than 3
func handle_message(message: String):
if message.begins_with("PING"):
_chatClient.send_text(message.replace("PING", "PONG"))
return
var parsed_message: PackedStringArray = parse_message_from_twtich_IRC(message)
if parsed_message.size() < 2: return
match parsed_message[2]:
"NOTICE":
var info : String = parsed_message[3].right(-1)
if (info == "Login authentication failed" || info == "Login unsuccessful"):
print_debug("Authentication failed.")
#login_attempt.emit(false)
elif (info == "You don't have permission to perform that action"):
print_debug("No permission. Check if access token is still valid. Aborting.")
#user_token_invalid.emit()
set_process(false)
else:
pass
#unhandled_message.emit(message, tags)
"001":
print_debug("Authentication successful.")
_chatClient.send_text('ROOMSTATE '+ '#' + _channel.login.to_lower())
#login_attempt.emit(true)
"PRIVMSG":
handle_privmsg(parsed_message)
#handle_command(sender_data, msg[3].split(" ", true, 1))
#chat_message.emit(sender_data, msg[3].right(-1))
"ROOMSTATE":
if _use_anon_connection:
var parsed_tags:VSTIRCTags = VSTParseHelper.parse_tags(parsed_message[0])
_channel.id = parsed_tags.user_id
func parse_message_to_chatter(message: PackedStringArray) -> VSTChatter:
var chatter = VSTChatter.new()
chatter.login = VSTParseHelper.parse_login(message[1])
chatter.channel = VSTParseHelper.parse_channel(message[3])
chatter.message = VSTParseHelper.parse_message(message[4])
chatter.tags = VSTParseHelper.parse_tags(message[0])
chatter.date_time_dict = Time.get_datetime_dict_from_system()
if chatter.tags.color_hex.is_empty():
chatter.tags.color_hex = VSTUtils.get_random_name_color(chatter.login)
return chatter
func handle_privmsg(msg: PackedStringArray):
var chatter = parse_message_to_chatter(msg)
OnMessage.emit(chatter)
func get_emote(emote_id: String, scale: String = "1.0") -> Texture2D:
var texture: Texture2D
var cachename: String = emote_id + "_" + scale
var filename: String = _cache_path + "/" + RequestType.keys()[RequestType.EMOTE] + "/" + cachename + ".png"
if !caches[RequestType.EMOTE].has(cachename):
if _use_cache && FileAccess.file_exists(filename):
var img: Image = Image.new()
img.load_png_from_buffer(FileAccess.get_file_as_bytes(filename))
texture = ImageTexture.create_from_image(img)
texture.take_over_path(filename)
else:
var request: HTTPRequest = HTTPRequest.new()
add_child(request)
request.request("https://static-cdn.jtvnw.net/emoticons/v1/" + emote_id + "/" + scale, [USER_AGENT,"Accept: */*"])
var data = await(request.request_completed)
var img: Image = Image.new()
img.load_png_from_buffer(data[3])
texture = ImageTexture.create_from_image(img)
if _use_cache:
DirAccess.make_dir_recursive_absolute(filename.get_base_dir())
texture.get_image().save_png(filename)
request.queue_free()
texture.take_over_path(filename)
caches[RequestType.EMOTE][cachename] = texture
return caches[RequestType.EMOTE][cachename]
func get_badge(badge_name: String, badge_level: String, channel_id: String = "_global", scale: String = "1") -> Texture2D:
if _use_anon_connection: return
var texture: Texture2D
var cachename = badge_name + "_" + badge_level + "_" + scale
var filename: String = _cache_path + "/" + RequestType.keys()[RequestType.BADGE] + "/" + channel_id + "/" + cachename + ".png"
if !caches[RequestType.BADGE].has(channel_id):
caches[RequestType.BADGE][channel_id] = {}
if !caches[RequestType.BADGE][channel_id].has(cachename):
if _use_cache && FileAccess.file_exists(filename):
var img : Image = Image.new()
img.load_png_from_buffer(FileAccess.get_file_as_bytes(filename))
texture = ImageTexture.create_from_image(img)
texture.take_over_path(filename)
else:
var map: Dictionary = caches[RequestType.BADGE_MAPPING].get(channel_id, await(get_badge_mapping(channel_id)))
if !map.is_empty():
if map.has(badge_name):
var request: HTTPRequest = HTTPRequest.new()
add_child(request)
request.request(map[badge_name]["versions"][badge_level]["image_url_" + scale + "x"], [USER_AGENT,"Accept: */*"])
var data = await(request.request_completed)
var img: Image = Image.new()
img.load_png_from_buffer(data[3])
texture = ImageTexture.create_from_image(img)
texture.take_over_path(filename)
request.queue_free()
elif channel_id != "_global":
return await(get_badge(badge_name, badge_level, "_global", scale))
elif channel_id != "_global":
return await(get_badge(badge_name, badge_level, "_global", scale))
if _use_cache:
DirAccess.make_dir_recursive_absolute(filename.get_base_dir())
texture.get_image().save_png(filename)
texture.take_over_path(filename)
caches[RequestType.BADGE][channel_id][cachename] = texture
return caches[RequestType.BADGE][channel_id][cachename]
func get_badge_mapping(channel_id: String = "_global") -> Dictionary:
if _use_anon_connection: return {}
if caches[RequestType.BADGE_MAPPING].has(channel_id):
return caches[RequestType.BADGE_MAPPING][channel_id]
var filename: String = _cache_path + "/" + RequestType.keys()[RequestType.BADGE_MAPPING] + "/" + channel_id + ".json"
if _use_cache && FileAccess.file_exists(filename):
var cache = JSON.parse_string(FileAccess.get_file_as_string(filename))
if "badge_sets" in cache:
return cache["badge_sets"]
var request : HTTPRequest = HTTPRequest.new()
add_child(request)
request.request("https://api.twitch.tv/helix/chat/badges" + ("/global" if channel_id == "_global" else "?broadcaster_id=" + _channel.id), [USER_AGENT, "Authorization: Bearer " + _channel.token, "Client-Id:" + _client_id, "Content-Type: application/json"], HTTPClient.METHOD_GET)
var reply : Array = await(request.request_completed)
var response : Dictionary = JSON.parse_string(reply[3].get_string_from_utf8())
var mappings : Dictionary = {}
for entry in response["data"]:
if (!mappings.has(entry["set_id"])):
mappings[entry["set_id"]] = {"versions": {}}
for version in entry["versions"]:
mappings[entry["set_id"]]["versions"][version["id"]] = version
request.queue_free()
if (reply[1] == HTTPClient.RESPONSE_OK):
caches[RequestType.BADGE_MAPPING][channel_id] = mappings
if _use_cache:
DirAccess.make_dir_recursive_absolute(filename.get_base_dir())
var file : FileAccess = FileAccess.open(filename, FileAccess.WRITE)
file.store_string(JSON.stringify(mappings))
else:
print("Could not retrieve badge mapping for channel_id " + channel_id + ".")
return {}
return caches[RequestType.BADGE_MAPPING][channel_id]
func get_settings():
_client_id = VSTSettings.get_setting(VSTSettings.settings.client_id)
_twitch_chat_url = VSTSettings.get_setting(VSTSettings.settings.twitch_chat_url)
_twitch_chat_port = VSTSettings.get_setting(VSTSettings.settings.twitch_port)
_use_cache = VSTSettings.get_setting(VSTSettings.settings.disk_cache)
_cache_path = VSTSettings.get_setting(VSTSettings.settings.disk_cache_path)
_chat_timeout_ms = VSTSettings.get_setting(VSTSettings.settings.twitch_timeout_ms)
# stops chat socket from tts server
func disconnect_api():
if _chatClient:
_chatClient.close()
_hasConnected = false

View File

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

View File

@@ -0,0 +1,124 @@
class_name VSTSettings
const settings: Dictionary = {
"client_id": {
"path": "config/client_id",
"type": TYPE_STRING,
"hint_string": "The client id from the twitch developer dashboard",
"is_basic": true,
"initial_value": "",
},
"redirect_host": {
"path": "advanced_config/redirect_host",
"type": TYPE_STRING,
"hint_string": "The host where the OAuth Callback will be received",
"is_basic": false,
"initial_value": "http://localhost:",
},
"uuid": {
"path": "advanced_config/uuid",
"type": TYPE_STRING,
"hint_string": "The useless UID to hide the token from the web browser",
"is_basic": false,
"initial_value": "53125396-3e32-4fad-8f7e-36475724168b-a8fe83ab-3373-4a6a-8967-2532eafe407f-41483db3-f011-4a23-80da-9a340672692a-e755c6d4-c546-43ce-b722-b5a799561b4e-5ba1697d-79b2-4d5d-96c3-f0d91f13f583-f08f18f9-bd56-4a0f-a597-96f90108cd85-14449d50-6cc9-450f-8119-ff4c525e31db-e41a6912-92a0-48b6-b6d3-845c21bea7eb-7dfd7948-2976-42cf-9cca-b23ae5854813-107224eb-81ea-46dd-9bf5-9ebbfcfc45dc/",
},
"redirect_port": {
"path": "config/redirect_port",
"type": TYPE_INT,
"hint_string": "The port where the oauth callback will be redirect",
"is_basic": true,
"initial_value": 8090,
},
"disk_cache_path": {
"path": "advanced_config/disk_cache_path",
"type": TYPE_STRING,
"hint": PROPERTY_HINT_GLOBAL_DIR,
"hint_string": "The absolute filepath where the images cache will be stored",
"is_basic": false,
"initial_value": "user://very-simple-chat/cache",
},
"disk_cache": {
"path": "config/disk_cache",
"type": TYPE_BOOL,
"hint_string": "Use cache to store the images from badges and emotes",
"is_basic": true,
"initial_value": true,
},
"scopes": {
"path": "config/scopes",
"type": TYPE_PACKED_STRING_ARRAY,
"hint_string": "Scopes that will be asked when the token is retrieved",
"is_basic": true,
"initial_value": ["moderator:manage:banned_users","chat:read", "channel:manage:vips"],
},
"twitch_chat_url": {
"path": "advanced_config/twitch_chat_url",
"type": TYPE_STRING,
"hint_string": "Twitch chat url",
"is_basic": false,
"initial_value": "wss://irc-ws.chat.twitch.tv",
},
"twitch_port": {
"path": "advanced_config/twitch_port",
"type": TYPE_INT,
"hint_string": "The port the websocket will connect to",
"is_basic": false,
"initial_value": 443,
},
"twitch_anon_user": {
"path": "advanced_config/twitch_anon_user",
"type": TYPE_STRING,
"hint_string": "Anon connection username",
"is_basic": false,
"initial_value": "justinfan5555",
},
"twitch_anon_pass": {
"path": "advanced_config/twitch_anon_pass",
"type": TYPE_STRING,
"hint_string": "Anon connection password",
"is_basic": false,
"initial_value": "kappa",
},
"twitch_timeout_ms":{
"path": "advanced_config/twitch_timeout_ms",
"type:": TYPE_INT,
"hint_string": "Time between messages sent by the client",
"is_basic": false,
"initial_value": 320,
}
}
static func add_settings():
for setting in settings:
var setting_value = settings[setting]
var path = "very_simple_twitch/"+ setting_value.get("path", "config" +setting)
var saved_value = ProjectSettings.get_setting(path)
if !saved_value:
ProjectSettings.set_setting(path, setting_value.get("initial_value"))
var property_info = {
"name": path,
"type": setting_value.get("type", typeof(setting_value.get("initial_value"))),
"hint": setting_value.get("hint"),
"hint_string": setting_value.get("hint_string", "")
}
ProjectSettings.set_as_basic(path, setting_value.get("is_basic", true))
ProjectSettings.set_initial_value(path, setting_value.get("initial_value"))
ProjectSettings.add_property_info(property_info)
ProjectSettings.save()
static func remove_settings():
for setting in settings:
var setting_value = settings[setting]
var path = "very_simple_twitch/"+ setting_value.get("path", "config") + "/" + setting
ProjectSettings.set_setting(path, null)
static func get_setting(setting: Dictionary):
var path = "very_simple_twitch/"+ setting.get("path")
var response = ProjectSettings.get_setting(path)
if !response:
return setting.get("initial_value")
else:
return response

View File

@@ -0,0 +1 @@
uid://7erhidjoher1

View File

@@ -0,0 +1,71 @@
extends Node
signal token_received(twitch_channel: VSTChannel)
signal chat_message_received(channel: VSTChatter)
signal chat_connected(channel_name: String)
var _twitch_api: VSTAPI
var _twitch_chat: VSTChat
func login_chat_anon(channel_name: String):
_start_chat_client()
_twitch_chat.login_anon(channel_name)
chat_connected.emit(await _twitch_chat.Connected)
func login_chat(channel_info: VSTChannel):
_start_chat_client()
_twitch_chat.login(channel_info)
chat_connected.emit(await _twitch_chat.Connected)
func get_token_and_login_chat():
var channel_info = await get_token()
await login_chat(channel_info)
func _start_chat_client():
if !_twitch_chat:
_twitch_chat = VSTChat.new()
add_child(_twitch_chat)
_twitch_chat.OnMessage.connect(on_chat_message_received)
func get_token() -> VSTChannel:
if !_twitch_api:
_twitch_api = VSTAPI.new()
add_child(_twitch_api)
_twitch_api.initiate_twitch_auth()
var channel_info = await _twitch_api.token_received
token_received.emit(channel_info)
return channel_info
func get_badge(badge_name: String, badge_level: String,
channel_id: String = "_global", scale: String = "1"):
return await _twitch_chat.get_badge(badge_name, badge_level, channel_id, scale)
func get_emote(loc_id: String):
return await _twitch_chat.get_emote(loc_id)
# clear all support nodes, disconects from chat/auth server
func end_chat_client():
if _twitch_chat:
_twitch_chat.disconnect_api()
_twitch_chat.OnMessage.disconnect(on_chat_message_received)
remove_child(_twitch_chat)
_twitch_chat.queue_free()
_twitch_chat = null
if _twitch_api:
_twitch_api.disconnect_api()
remove_child(_twitch_api)
_twitch_api.queue_free()
_twitch_api = null
func send_chat_message(message: String):
_twitch_chat.send_message(message)
func on_chat_message_received(chatter: VSTChatter):
chat_message_received.emit(chatter)

View File

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

View File

@@ -0,0 +1,40 @@
class_name VSTUtils
const LUMINANCE_LOW := 0.2
const LUMINANCE_HIGH := 0.8
const DEFAULT_NAME_COLORS:Array[String] = [
"#FF0000",
"#00FF00",
"#0000FF",
"#B22222",
"#FF7F50",
"#9ACD32",
"#FF4500",
"#2E8B57",
"#DAA520",
"#D2691E",
"#5F9EA0",
"#1E90FF",
"#FF69B4",
"#8A2BE2",
"#00FF7F",
]
# Returns a random non-unique color from a name and a seed
static func get_random_name_color(login: String, session_seed:int = 0):
var position: int = session_seed + hash(login)
return DEFAULT_NAME_COLORS[position % DEFAULT_NAME_COLORS.size()]
# Normalize color in order to be not bright or darker
static func normalize_color(color: Color) -> Color:
var luminance = color.get_luminance()
if luminance > LUMINANCE_HIGH:
return color.darkened(0.2)
if luminance < LUMINANCE_LOW:
return color.lightened(0.2)
return color
# Normalize color from string representation
static func normalize_hex_color(color: String) -> Color:
return normalize_color(Color(color))

View File

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

View File

@@ -0,0 +1,36 @@
@tool
extends EditorPlugin
var dock
var chat_dock
func _enter_tree() -> void:
add_custom_type("VerySimpleTwitchChat", "Node", preload("twitch_chat.gd"), preload("icon.png"))
add_custom_type("VerySimpleTwitchAPI", "Node", preload("twitch_api.gd"), preload("icon.png"))
add_autoload_singleton("VerySimpleTwitch", "twitch_node.gd")
VSTSettings.add_settings()
#Bottom setup dock
dock = preload("res://addons/very-simple-twitch/dock/vst-dock.tscn").instantiate()
add_control_to_bottom_panel(dock, "Very Simple Twitch")
#Chat dock
chat_dock = preload("res://addons/very-simple-twitch/chat/vst_chat_dock.tscn").instantiate()
add_control_to_dock(EditorPlugin.DOCK_SLOT_RIGHT_UL, chat_dock)
func _exit_tree() -> void:
remove_custom_type("VerySimpleTwitchChat")
remove_custom_type("VerySimpleTwitchAPI")
VSTSettings.remove_settings()
remove_autoload_singleton("VerySimpleTwitch")
remove_control_from_bottom_panel(dock)
dock.free()
remove_control_from_docks(chat_dock)
chat_dock.free()

View File

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

View File

@@ -0,0 +1,32 @@
class_name VSTError
enum VSTCodeError {PARAM_ERROR, TIMEOUT_ERROR, NETWORK_ERROR, SERVER_ERROR}
var code: VSTCodeError
var description: String
var info: String
func _init(error_code: VSTCodeError, error_info: String = ""):
code = error_code
info = error_info
description = _get_description_from_code(error_code)
func _get_description_from_code(error_code: VSTCodeError) -> String:
var result = ""
match (error_code):
VSTCodeError.PARAM_ERROR:
result = "The request aren't fullfilled properly. Check the data"
VSTCodeError.NETWORK_ERROR:
result = "The request result in an error"
VSTCodeError.SERVER_ERROR:
result = "There is an error in server"
VSTCodeError.TIMEOUT_ERROR:
result = "The server doesn't response"
_:
result = "Unknown error"
return result
func _to_string():
return "%s %s %s" % [str(code), description, info]

View File

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

View File

@@ -4,8 +4,9 @@
[resource]
diffuse_mode = 3
specular_mode = 1
specular_mode = 2
albedo_texture = ExtResource("1_cc1ni")
metallic_specular = 0.0
roughness = 0.0
rim_tint = 0.48
stencil_flags = 2

View File

@@ -13,7 +13,7 @@ dest_files=["res://.godot/imported/meeting_demeter.ogg-7dd58073d2ef5705a374bd8bb
[params]
loop=false
loop_offset=0
bpm=0
loop_offset=0.0
bpm=0.0
beat_count=0
bar_beats=4

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://bdsghxlbtduuy"
path="res://.godot/imported/subterra.ogg-458c2f15c134af211d5a71b5819758c5.oggvorbisstr"
[deps]
source_file="res://common/audio_manager/assets/morceaux/histoire/subterra.ogg"
dest_files=["res://.godot/imported/subterra.ogg-458c2f15c134af211d5a71b5819758c5.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://du3hfjbaoyc8"
path="res://.godot/imported/fin_de_map.ogg-62fbcdd24bc4df66b50e9a969579e8d9.oggvorbisstr"
[deps]
source_file="res://common/audio_manager/assets/sfx/fin_de_map/fin_de_map.ogg"
dest_files=["res://.godot/imported/fin_de_map.ogg-62fbcdd24bc4df66b50e9a969579e8d9.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bfhab51qe80j5"
path="res://.godot/imported/fin_de_map_cristal_1.wav-fbc2aabbedcf9b9ea4de792270616879.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/fin_de_map/fin_de_map_cristal_1.wav"
dest_files=["res://.godot/imported/fin_de_map_cristal_1.wav-fbc2aabbedcf9b9ea4de792270616879.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

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cr6y4e0p3xrqv"
path="res://.godot/imported/fin_de_map_cristal_2.wav-22f78a6101de5044c9eb27302000d841.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/fin_de_map/fin_de_map_cristal_2.wav"
dest_files=["res://.godot/imported/fin_de_map_cristal_2.wav-22f78a6101de5044c9eb27302000d841.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

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://ccppo3l1vyd08"
path="res://.godot/imported/fin_de_map_cristal_3.wav-08ca119096dca7f1ce13ef8a0fc9b262.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/fin_de_map/fin_de_map_cristal_3.wav"
dest_files=["res://.godot/imported/fin_de_map_cristal_3.wav-08ca119096dca7f1ce13ef8a0fc9b262.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

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://wphcqemoy810"
path="res://.godot/imported/fin_de_map_cristal_4.wav-b746d9213d33d907760d7149452e1cfd.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/fin_de_map/fin_de_map_cristal_4.wav"
dest_files=["res://.godot/imported/fin_de_map_cristal_4.wav-b746d9213d33d907760d7149452e1cfd.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

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://v2snsj54xlkw"
path="res://.godot/imported/fin_de_map_cristal_5.wav-03f398571aece8e59a223be04924d6a1.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/fin_de_map/fin_de_map_cristal_5.wav"
dest_files=["res://.godot/imported/fin_de_map_cristal_5.wav-03f398571aece8e59a223be04924d6a1.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

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bvhnpk7pbh75t"
path="res://.godot/imported/fin_de_map_cristal_6.wav-55680ff3c48778e28de53ad22c902751.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/fin_de_map/fin_de_map_cristal_6.wav"
dest_files=["res://.godot/imported/fin_de_map_cristal_6.wav-55680ff3c48778e28de53ad22c902751.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

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://c1jg6vbsd1y00"
path="res://.godot/imported/tremblement.mp3-2f1079501efc61a933c525d8b73ff10b.mp3str"
[deps]
source_file="res://common/audio_manager/assets/sfx/fin_de_map/tremblement.mp3"
dest_files=["res://.godot/imported/tremblement.mp3-2f1079501efc61a933c525d8b73ff10b.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://budu0cym6ximv"
path="res://.godot/imported/phone_call.wav-5f6468b6c2e194f4077a966b2b8ed027.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/phone/phone_call.wav"
dest_files=["res://.godot/imported/phone_call.wav-5f6468b6c2e194f4077a966b2b8ed027.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

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://c18orgaa5yect"
path="res://.godot/imported/pickaxe_cave_1.wav-c2db6fad48f4457a7e47ef7fabdce2ca.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/phone/pickaxe_cave_1.wav"
dest_files=["res://.godot/imported/pickaxe_cave_1.wav-c2db6fad48f4457a7e47ef7fabdce2ca.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

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cbkg6v76a1d1q"
path="res://.godot/imported/pickaxe_cave_2.wav-72432422e9b9eac863b31c5dda4262c9.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/phone/pickaxe_cave_2.wav"
dest_files=["res://.godot/imported/pickaxe_cave_2.wav-72432422e9b9eac863b31c5dda4262c9.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

View File

@@ -2,13 +2,13 @@
importer="wav"
type="AudioStreamWAV"
uid="uid://3c4nxjasebyk"
path="res://.godot/imported/pickaxe_1_reverb.wav-8d6172bc1e5c2f43ec5b5cf869d5b6e3.sample"
uid="uid://x36rvb4eso8q"
path="res://.godot/imported/pickaxe_cave_1.wav-5365b9250c03fe13766ad32a821704fa.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/pickaxe/pickaxe_1_reverb.wav"
dest_files=["res://.godot/imported/pickaxe_1_reverb.wav-8d6172bc1e5c2f43ec5b5cf869d5b6e3.sample"]
source_file="res://common/audio_manager/assets/sfx/pickaxe/pickaxe_cave_1.wav"
dest_files=["res://.godot/imported/pickaxe_cave_1.wav-5365b9250c03fe13766ad32a821704fa.sample"]
[params]

View File

@@ -2,13 +2,13 @@
importer="wav"
type="AudioStreamWAV"
uid="uid://bu278eqn8krnb"
path="res://.godot/imported/pickaxe_3_reverb.wav-d3e3d8b6b50a16c6757536f43c8c6ccd.sample"
uid="uid://dh3oho0pis6jv"
path="res://.godot/imported/pickaxe_cave_2.wav-bb5e1c82ba8ffd44758505ce286a04a9.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/pickaxe/pickaxe_3_reverb.wav"
dest_files=["res://.godot/imported/pickaxe_3_reverb.wav-d3e3d8b6b50a16c6757536f43c8c6ccd.sample"]
source_file="res://common/audio_manager/assets/sfx/pickaxe/pickaxe_cave_2.wav"
dest_files=["res://.godot/imported/pickaxe_cave_2.wav-bb5e1c82ba8ffd44758505ce286a04a9.sample"]
[params]

View File

@@ -2,13 +2,13 @@
importer="wav"
type="AudioStreamWAV"
uid="uid://bs5ldhabymm5p"
path="res://.godot/imported/pickaxe_2_reverb.wav-c953afb7e49205a0f4377738e1135a5b.sample"
uid="uid://c6jdmdjncamcu"
path="res://.godot/imported/pickaxe_cave_finalblow.wav-ce8ca7f063b31d6564d593b5a84e7560.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/pickaxe/pickaxe_2_reverb.wav"
dest_files=["res://.godot/imported/pickaxe_2_reverb.wav-c953afb7e49205a0f4377738e1135a5b.sample"]
source_file="res://common/audio_manager/assets/sfx/pickaxe/pickaxe_cave_finalblow.wav"
dest_files=["res://.godot/imported/pickaxe_cave_finalblow.wav-ce8ca7f063b31d6564d593b5a84e7560.sample"]
[params]

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cam4vv1am40dy"
path="res://.godot/imported/drop.wav-b76e8a2f6f5f0c41737e74496f01ceaa.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/tractor_beam/drop.wav"
dest_files=["res://.godot/imported/drop.wav-b76e8a2f6f5f0c41737e74496f01ceaa.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

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bdrdnli5k27a2"
path="res://.godot/imported/take.wav-d597c4daf1adbf3b061372ab36ef9940.sample"
[deps]
source_file="res://common/audio_manager/assets/sfx/tractor_beam/take.wav"
dest_files=["res://.godot/imported/take.wav-d597c4daf1adbf3b061372ab36ef9940.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

View File

@@ -2,7 +2,6 @@
[ext_resource type="Script" uid="uid://2p5d6vogtn82" path="res://common/audio_manager/scripts/audio_manager.gd" id="1_0tvca"]
[ext_resource type="AudioStream" uid="uid://dq2nodhwnp73f" path="res://common/audio_manager/assets/ambiance/cave/solarmusic-dripping-water-in-cave-114694.ogg" id="2_ge2sc"]
[ext_resource type="AudioStream" uid="uid://dipnmlprwfo12" path="res://common/audio_manager/assets/ambiance/niveau/ambiance_phase_1.ogg" id="2_tuvql"]
[ext_resource type="AudioStream" uid="uid://dipnmlprwfo12" path="res://common/audio_manager/assets/ambiance/niveau/ambiance.ogg" id="3_qvjf5"]
[ext_resource type="AudioStream" uid="uid://b1hut6lc1jevh" path="res://common/audio_manager/assets/morceaux/niveau/mines_phase_2.ogg" id="4_2fduo"]
[ext_resource type="AudioStream" uid="uid://cdohaice7nc8d" path="res://common/audio_manager/assets/ambiance/niveau/ambiance_foggy.ogg" id="4_ipd1r"]
@@ -20,10 +19,10 @@
[ext_resource type="AudioStream" uid="uid://kqbqhwhkv7o3" path="res://common/audio_manager/assets/morceaux/niveau/mines_waiting.ogg" id="11_ngi21"]
[ext_resource type="AudioStream" uid="uid://of68i2k1g6y2" path="res://common/audio_manager/assets/morceaux/niveau/desert_phase_1.ogg" id="11_yjs51"]
[ext_resource type="AudioStream" uid="uid://b8inedx4yjslw" path="res://common/audio_manager/assets/sfx/drop/drop_1.wav" id="12_4hp8f"]
[ext_resource type="AudioStream" uid="uid://bdsghxlbtduuy" path="res://common/audio_manager/assets/morceaux/histoire/subterra.ogg" id="12_mrdk3"]
[ext_resource type="AudioStream" uid="uid://cjbpfnlwcpjh0" path="res://common/audio_manager/assets/morceaux/niveau/forest_waiting.ogg" id="12_xmumj"]
[ext_resource type="AudioStream" uid="uid://8nmr5vifkt1f" path="res://common/audio_manager/assets/sfx/harvest/harvest_1.wav" id="13_xoaox"]
[ext_resource type="AudioStream" uid="uid://dgkdcq4j6fe3o" path="res://common/audio_manager/assets/sfx/harvest/harvest_2.wav" id="14_b5bgj"]
[ext_resource type="AudioStream" uid="uid://crncg0mdx1fdw" path="res://common/audio_manager/assets/morceaux/demo/ending.ogg" id="14_h3tkm"]
[ext_resource type="AudioStream" uid="uid://dsfqhcrard8o4" path="res://common/audio_manager/assets/morceaux/niveau/desert_phase_2.ogg" id="14_lwdce"]
[ext_resource type="AudioStream" uid="uid://eh3dbuxu5qtw" path="res://common/audio_manager/assets/sfx/harvest/harvest_3.wav" id="15_ynvb4"]
[ext_resource type="AudioStream" uid="uid://bown4yipeef8l" path="res://common/audio_manager/assets/sfx/harvest/harvest_4.wav" id="16_obeji"]
@@ -45,8 +44,6 @@
[ext_resource type="AudioStream" uid="uid://ch8wnrckanydg" path="res://common/audio_manager/assets/morceaux/histoire/meeting_demeter.ogg" id="22_mrdk3"]
[ext_resource type="AudioStream" uid="uid://bnwtgp8t46xwc" path="res://common/audio_manager/assets/sfx/recharge/recharge_capsule_6.wav" id="23_ge2sc"]
[ext_resource type="AudioStream" uid="uid://bp3wsncvda5gl" path="res://common/audio_manager/assets/sfx/recharge/recharge_capsule_7.wav" id="24_yr73o"]
[ext_resource type="AudioStream" uid="uid://b4jx8rflw7dss" path="res://common/audio_manager/assets/sfx/phone/phone_ringing_Astra.wav" id="27_0rjel"]
[ext_resource type="AudioStream" uid="uid://ocm1dkkhv7ls" path="res://common/audio_manager/assets/sfx/phone/phone_ringing.wav" id="28_3dfjn"]
[ext_resource type="AudioStream" uid="uid://8juy5ev3rdfh" path="res://common/audio_manager/assets/sfx/plant_points/plant_point_1.wav" id="29_ngi21"]
[ext_resource type="AudioStream" uid="uid://su387eovtrsg" path="res://common/audio_manager/assets/sfx/plant_points/plant_point_2.wav" id="30_xmumj"]
[ext_resource type="AudioStream" uid="uid://bp6mtpqjf4txo" path="res://common/audio_manager/assets/sfx/plant_points/plant_point_3.wav" id="31_spekb"]
@@ -65,12 +62,15 @@
[ext_resource type="AudioStream" uid="uid://dd1uu6dd6sloe" path="res://common/audio_manager/assets/sfx/pickaxe/pickaxe_3.wav" id="45_mur2l"]
[ext_resource type="AudioStream" uid="uid://eq7wufwnolto" path="res://common/audio_manager/assets/sfx/pickaxe/pickaxe_4.wav" id="46_t0v4u"]
[ext_resource type="AudioStream" uid="uid://cv7sj8n5oo1i8" path="res://common/audio_manager/assets/sfx/screen_bip/screen_bip.wav" id="47_svctq"]
[ext_resource type="AudioStream" uid="uid://budu0cym6ximv" path="res://common/audio_manager/assets/sfx/phone/phone_call.wav" id="48_ipd1r"]
[ext_resource type="AudioStream" uid="uid://sgwvpxiul5x5" path="res://common/audio_manager/assets/sfx/ship_exit/ship_exit.wav" id="48_j8acj"]
[ext_resource type="AudioStream" uid="uid://ca0wonha334cl" path="res://common/audio_manager/assets/sfx/teleportation/teleport.wav" id="50_rlnfe"]
[ext_resource type="AudioStream" uid="uid://cv5avkd3qekt7" path="res://common/audio_manager/assets/sfx/movement/movement.wav" id="51_iyxkn"]
[ext_resource type="AudioStream" uid="uid://53ixfbcd5qwu" path="res://common/audio_manager/assets/sfx/holo/holo_appear.wav" id="63_aedoe"]
[ext_resource type="AudioStream" uid="uid://dsijqgnnadgem" path="res://common/audio_manager/assets/sfx/holo/holo_disappear.wav" id="64_ge2sc"]
[ext_resource type="AudioStream" uid="uid://dscyqjujj1com" path="res://common/audio_manager/assets/sfx/unlock_tool/unlock_tool.wav" id="64_yr73o"]
[ext_resource type="AudioStream" uid="uid://bdrdnli5k27a2" path="res://common/audio_manager/assets/sfx/tractor_beam/take.wav" id="69_5rlid"]
[ext_resource type="AudioStream" uid="uid://cam4vv1am40dy" path="res://common/audio_manager/assets/sfx/tractor_beam/drop.wav" id="70_gfbcu"]
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_6o1yh"]
streams_count = 3
@@ -132,6 +132,7 @@ script = ExtResource("1_0tvca")
unique_name_in_owner = true
[node name="Cave" type="AudioStreamPlayer" parent="Ambiances" unique_id=71769481]
unique_name_in_owner = true
stream = ExtResource("2_ge2sc")
volume_db = -7.195
pitch_scale = 0.5
@@ -186,6 +187,10 @@ unique_name_in_owner = true
stream = ExtResource("8_tuvql")
volume_db = -5.0
[node name="Subterra" type="AudioStreamPlayer" parent="Ambiances" unique_id=2019011683]
unique_name_in_owner = true
stream = ExtResource("12_mrdk3")
[node name="Musics" type="Node" parent="." unique_id=1450527710]
unique_name_in_owner = true
@@ -242,11 +247,6 @@ stream = ExtResource("7_tuvql")
volume_db = -5.0
bus = &"Music"
[node name="Demo_end" type="AudioStreamPlayer" parent="Musics" unique_id=261817716]
unique_name_in_owner = true
stream = ExtResource("14_h3tkm")
volume_db = -5.0
[node name="Meeting_demeter" type="AudioStreamPlayer" parent="Musics" unique_id=1066359159]
unique_name_in_owner = true
stream = ExtResource("22_mrdk3")
@@ -314,7 +314,7 @@ volume_db = -7.0
[node name="Harvest" type="AudioStreamPlayer" parent="Sfx" unique_id=345539331]
stream = SubResource("AudioStreamRandomizer_i4m0x")
volume_db = -5.0
volume_db = -7.0
[node name="PickUp" type="AudioStreamPlayer" parent="Sfx" unique_id=176915166]
stream = SubResource("AudioStreamRandomizer_jjdv2")
@@ -325,11 +325,11 @@ stream = ExtResource("22_btfwx")
volume_db = 3.627
[node name="Astra_phone_call" type="AudioStreamPlayer" parent="Sfx" unique_id=721344636]
stream = ExtResource("27_0rjel")
stream = ExtResource("48_ipd1r")
volume_db = -5.0
[node name="Phone_call" type="AudioStreamPlayer" parent="Sfx" unique_id=1668278453]
stream = ExtResource("28_3dfjn")
stream = ExtResource("48_ipd1r")
volume_db = -5.0
[node name="PlantPoint" type="AudioStreamPlayer" parent="Sfx" unique_id=2044025024]
@@ -358,7 +358,7 @@ volume_db = -5.0
[node name="Elevator" type="AudioStreamPlayer" parent="Sfx" unique_id=1345852969]
stream = ExtResource("42_obkny")
volume_db = -5.0
pitch_scale = 1.1
[node name="Mining" type="AudioStreamPlayer" parent="Sfx" unique_id=1122216774]
stream = SubResource("AudioStreamRandomizer_yjs51")
@@ -391,3 +391,11 @@ volume_db = -11.0
[node name="Unlock_tool" type="AudioStreamPlayer" parent="Sfx" unique_id=667077616]
stream = ExtResource("64_yr73o")
volume_db = -5.0
[node name="TractorBeamTake" type="AudioStreamPlayer" parent="Sfx" unique_id=1321980229]
stream = ExtResource("69_5rlid")
volume_db = -5.0
[node name="TractorBeamDrop" type="AudioStreamPlayer" parent="Sfx" unique_id=2023233451]
stream = ExtResource("70_gfbcu")
volume_db = -5.0

View File

@@ -40,7 +40,7 @@ func _on_change_scene(scene : Scene):
stop_all_ambiances()
if (scene is TitleScene):
play_music_alone("Title", false, 5.0)
play_music_alone("Title", false, 0.0)
elif scene is IntroScene:
stop_all_musics()
elif scene is RegionScene:
@@ -59,8 +59,8 @@ func _on_change_scene(scene : Scene):
stop_all_musics()
play_ambiance_alone("Astra", false)
elif scene is BoreaScene:
stop_all_musics()
play_ambiance_alone("Borea", false)
play_music_alone("Meeting_demeter")
stop_all_ambiances()
elif scene is VendingMachineScene:
stop_all_musics()
play_ambiance_alone("VendingRoom", false)
@@ -207,12 +207,13 @@ func change_ambiances_volume(db_change := 0., fade := DEFAULT_FADE_TIME):
# Joue un
# - player_name : Nom de la Node dans la scène Godot à jouer
func play_sfx(sfx_name : String):
func play_sfx(sfx_name : String, pitch = 1.):
var player := %Sfx.find_child(sfx_name) as AudioStreamPlayer
if player:
player.play()
else:
printerr("Sfx %s not found" % sfx_name)
# ----------------- Partie Technique (pas touche Niels ;D) ----------------

View File

@@ -12,4 +12,4 @@ func get_3d_scene() -> PackedScene:
return preload("res://common/game_data/scripts/artefacts/talion_soil/talion_soil.blend")
func modify_plant_influence_radius(plant_influence_radius : float) -> float:
return plant_influence_radius * 1.4
return plant_influence_radius * 1.5

View File

@@ -28,16 +28,16 @@ func get_all_mutations() -> Array[PlantMutation]:
ProlificMutation.new(),
PrecociousMutation.new(),
PurificationMutation.new(),
VivaciousMutation.new(),
ToughMutation.new(),
QuickMutation.new(),
RobustMutation.new(),
SocialMutation.new(),
VivaciousMutation.new(),
FertileMutation.new(),
HurriedMutation.new(),
GenerousMutation.new(),
ProtectiveMutation.new(),
PureMutation.new(),
ToughMutation.new(),
]
func get_all_artifacts() -> Array[Artefact]:
@@ -53,7 +53,7 @@ func get_all_artifacts() -> Array[Artefact]:
func get_all_story_steps() -> Array[StoryStep]:
return [
TutorialStoryStep.new(),
AstraStoryStep.new(),
StartStoryStep.new(),
MercuryStoryStep.new(),
BetaStoryStep.new()
BoreaStoryStep.new()
]

View File

@@ -64,6 +64,10 @@ func generate_next_run_point(last_modifiers : Array[String] = []) -> RunPoint:
challenge_modifiers.pick_random(),
benefic_modifiers.pick_random()
] as Array[RegionModifier]
elif story_step.is_run_point_dangerous(next_level):
region_parameter.modifiers = [
challenge_modifiers.pick_random()
] as Array[RegionModifier]
else:
region_parameter.modifiers = [
normal_modifiers.pick_random()
@@ -96,6 +100,12 @@ func choose_next_run_point(run_point : RunPoint = null) -> RunPoint:
next_run_points = generate_next_run_points()
return current_run_point
func get_cockpit_exit_scene() -> Scene:
if story_step.is_run_finished(level):
return story_step.get_destination_scene()
else :
return RegionScene.new(GameInfo.game_data.current_region_data)
#endregion
#region ------------------ Modifiers ------------------
@@ -109,6 +119,7 @@ func generate_normal_modifiers() -> Array[RegionModifier]:
ToxicModifier.new(),
SandyModifier.new(),
MagneticModifier.new(),
CanyonModifier.new()
]
func generate_benefic_modifiers() -> Array[RegionModifier]:

View File

@@ -5,7 +5,7 @@ class_name RunPoint
const DANGER_ICON = preload("res://common/icons/skull.svg")
const TYPE_ICON = preload("res://common/icons/map-pin.svg")
const OBJECTIVE_ICON = preload("res://common/icons/growth.svg")
const CHARGE_ICON = preload("res://common/icons/bolt.svg")
const CHARGE_ICON = preload("res://common/icons/recharge.svg")
@export var region_parameter : RegionParameter = RegionParameter.new() :
set(v):
@@ -27,10 +27,13 @@ func card_info() -> CardInfo:
info.type_icon = TYPE_ICON
info.stats.append_array([
CardStatInfo.new(str(region_parameter.get_objective()), OBJECTIVE_ICON),
CardStatInfo.new(str(region_parameter.get_charge()), CHARGE_ICON),
])
if region_parameter.modifiers.find_custom(
func (m : RegionModifier) : return m is DestinationModifier
) == -1:
info.stats.append_array([
CardStatInfo.new(str(region_parameter.get_objective()), OBJECTIVE_ICON),
CardStatInfo.new(str(region_parameter.get_charge()), CHARGE_ICON),
])
for m in region_parameter.modifiers:

View File

@@ -9,6 +9,8 @@ signal language_changed(settings : SettingsData)
signal sound_changed(settings : SettingsData)
signal video_changed(settings : SettingsData)
signal game_changed(settings : SettingsData)
signal fov_changed(value : float)
signal twitch_changed(settings : SettingsData)
#region ------------------ Language ------------------
@@ -50,13 +52,30 @@ const AVAILABLE_LANGUAGES_LABEL = [
full_screen = v
video_changed.emit(self)
@export var ui_size : float = 1. :
set(v):
ui_size = v
video_changed.emit(self)
#region ------------------ Controls ------------------
@export var action_remapped : Array[String] = []
@export var input_remapped : Array[InputEvent] = []
@export var fov := 75. :
set(v):
fov = v
fov_changed.emit(fov)
#region ------------------ Game ------------------
@export var auto_pickup := true
@export var mouse_sensivity := 0.2
const MAX_ZOOM = 2.
const MIN_ZOOM = 0.5
@@ -79,4 +98,16 @@ func close_help_container(help_container_name : String):
func open_help_container(help_container_name : String):
if help_container_name in closed_help_containers:
closed_help_containers.erase(help_container_name)
game_changed.emit(self)
game_changed.emit(self)
#region ------------------ Twitch ------------------
@export var activate_twitch_integration := false :
set(v):
activate_twitch_integration = v
twitch_changed.emit(self)
@export var twitch_channel := "" :
set(v):
twitch_channel = v
twitch_changed.emit(self)

View File

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

View File

@@ -22,7 +22,7 @@ func is_run_finished(level : int) -> bool:
return level == get_region_sequence_length() - 1
func get_region_sequence_length() -> int:
return 6
return 7
func get_first_vending_machine_occurence(_level : int) -> int:
return 2
@@ -37,7 +37,7 @@ func get_cave_occurence(_level : int) -> int:
return 3
func get_challenge_chance(_level : int) -> float:
return 0.3
return 0.15
func get_run_point_number(level : int) -> int:
if is_run_finished(level):
@@ -47,6 +47,9 @@ func get_run_point_number(level : int) -> int:
func get_charge_number(_level : int) -> int:
return 10
func is_run_point_dangerous(level : int) -> bool:
return level == get_region_sequence_length() - 2
func get_objective_for_region(level : int) -> int:
match level:
1: return 10
@@ -62,12 +65,6 @@ func get_story_modifiers_for_region(level : int) -> Array[RegionModifier]:
var dest_mod = DestinationModifier.new()
dest_mod.destination_scene = get_destination_scene()
modifiers.append(dest_mod)
var first_cave = get_first_cave_occurence(level)
var cave_occurence = get_cave_occurence(level)
if cave_occurence > 0 and level >= first_cave:
if (level - first_cave)%cave_occurence == 0:
modifiers.append(CaveModifier.new())
return modifiers
@@ -80,6 +77,12 @@ func get_gameplay_modifiers_for_region(level : int) -> Array[RegionModifier]:
if vending_occurence > 0 and level >= first_vending:
if (level - first_vending)%vending_occurence == 0:
modifiers.append(VendingMachineModifier.new())
var first_cave = get_first_cave_occurence(level)
var cave_occurence = get_cave_occurence(level)
if cave_occurence > 0 and level >= first_cave:
if (level - first_cave)%cave_occurence == 0:
modifiers.append(CaveModifier.new())
return modifiers

View File

@@ -1,7 +1,7 @@
extends StoryStep
class_name TutorialStoryStep
const INTRO_DIALOG = "res://dialogs/timelines/tutorial/demeter_intro.dtl"
const INTRO_DIALOG = "res://dialogs/timelines/1_waking_up/2_demeter_intro.dtl"
func get_respawn_scene() -> Scene:
return AstraScene.new()
@@ -14,6 +14,10 @@ func get_region_sequence_length() -> int:
func get_destination_scene() -> Scene: return null
func need_gameplay_modifier(_level : int): return false
func get_objective_for_region(_level : int) -> int: return 3
func get_story_modifiers_for_region(_n : int) -> Array[RegionModifier]:
return [
TutorialModifier.new()

Some files were not shown because too many files have changed in this diff Show More