└── addons └── turnity ├── plugin.cfg ├── plugin.gd ├── icons ├── turn_socket.svg └── turn_socket.svg.import ├── turn_socket.gd └── turn_manager.gd /addons/turnity/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Turnity" 4 | description="Your go-to plugin for streamlined turn management in Godot. Elevate your game's experience with efficient turn-based mechanics. Craft dynamic battles and engaging scenarios effortlessly" 5 | author="BananaHolograma" 6 | version="1.0.4" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/turnity/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree(): 6 | add_custom_type("TurnitySocket", "Node", preload("res://addons/turnity/turn_socket.gd"), preload("res://addons/turnity/icons/turn_socket.svg")) 7 | add_autoload_singleton("TurnityManager", "res://addons/turnity/turn_manager.gd") 8 | 9 | 10 | func _exit_tree(): 11 | remove_custom_type("TurnitySocket") 12 | remove_autoload_singleton("TurnityManager") 13 | -------------------------------------------------------------------------------- /addons/turnity/icons/turn_socket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addons/turnity/icons/turn_socket.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://ceas1ivjwelkt" 6 | path="res://.godot/imported/turn_socket.svg-3d22ba4f26d88f5995d786daa518e9d7.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/turnity/icons/turn_socket.svg" 14 | dest_files=["res://.godot/imported/turn_socket.svg-3d22ba4f26d88f5995d786daa518e9d7.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/turnity/turn_socket.gd: -------------------------------------------------------------------------------- 1 | class_name TurnitySocket extends Node 2 | 3 | signal active_turn 4 | signal ended_turn 5 | signal changed_turn_duration(old_duration: int, new_duration: int) 6 | signal reset_current_timer 7 | signal blocked_n_turns(turns: int, total_turns: int) 8 | signal blocked_turn_consumed(remaining_turns: int) 9 | signal blocked_turns_removed 10 | signal skipped 11 | signal enabled_socket 12 | signal disabled_socket 13 | 14 | ## The linked actor in the turn system 15 | @export var actor: Node 16 | ## The turn duration for this socket, leave it to zero to make it infinite 17 | @export var turn_duration := 0 18 | ## Automatically move on to next turn when this socket is skipped 19 | @export var next_turn_when_skipped := true 20 | ## Automatically move on to next turn when this socket is blocked 21 | @export var next_turn_when_blocked := true 22 | 23 | var id: String 24 | var timer: Timer 25 | var active := false 26 | var disabled := false: 27 | set(value): 28 | if value != disabled: 29 | if value: 30 | disabled_socket.emit() 31 | else: 32 | enabled_socket.emit() 33 | 34 | disabled = value 35 | var blocked_turns := 0 36 | 37 | 38 | func _enter_tree(): 39 | add_to_group("turnity-socket") 40 | 41 | if id == null or id.is_empty(): 42 | id = _generate_random_id() 43 | 44 | if not actor: 45 | actor = get_parent() 46 | if actor == null: 47 | push_error("Turnity: The TurnitySocket needs a valid actor linked, cannot stand alone") 48 | 49 | 50 | func _ready(): 51 | _create_timer() 52 | 53 | active_turn.connect(on_active_turn) 54 | ended_turn.connect(on_ended_turn) 55 | 56 | 57 | func _create_timer(): 58 | timer = Timer.new() 59 | timer.name = "TurnitySocketTimer" 60 | timer.process_callback = Timer.TIMER_PROCESS_PHYSICS 61 | timer.wait_time = max(0.05, turn_duration) 62 | timer.one_shot = true 63 | timer.autostart = false 64 | 65 | add_child(timer) 66 | timer.timeout.connect(on_timeout) 67 | 68 | 69 | func change_turn_duration(new_duration: int): 70 | if timer.is_inside_tree(): 71 | changed_turn_duration.emit(timer.wait_time, new_duration) 72 | timer.stop() 73 | timer.wait_time = new_duration 74 | turn_duration = new_duration 75 | 76 | 77 | func reset_active_timer(): 78 | if timer.is_inside_tree() and timer.time_left > 0: 79 | timer.start() 80 | reset_current_timer.emit() 81 | 82 | 83 | func reset_blocked_turns(): 84 | if blocked_turns > 0: 85 | blocked_turns = 0 86 | blocked_turns_removed.emit() 87 | 88 | 89 | func block_a_number_of_turns(turns: int): 90 | blocked_turns += turns 91 | blocked_n_turns.emit(turns, blocked_turns) 92 | 93 | 94 | func is_blocked() -> bool: 95 | return blocked_turns > 0 96 | 97 | 98 | func skip(): 99 | if active: 100 | skipped.emit() 101 | 102 | 103 | func enable() -> void: 104 | disabled = false 105 | 106 | 107 | func disable() -> void: 108 | disabled = true 109 | 110 | 111 | func is_disabled() -> bool: 112 | return disabled 113 | 114 | 115 | func _generate_random_id(length: int = 20, characters: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"): 116 | var random_number_generator = RandomNumberGenerator.new() 117 | var result = "" 118 | 119 | if not characters.is_empty() and length > 0: 120 | for i in range(length): 121 | result += characters[random_number_generator.randi() % characters.length()] 122 | 123 | return result 124 | 125 | ### SIGNAL CALLBACKS ### 126 | func on_active_turn(): 127 | if is_disabled(): 128 | skip() 129 | else: 130 | active = true 131 | 132 | if blocked_turns > 0: 133 | blocked_turns -= 1 134 | blocked_turn_consumed.emit(blocked_turns) 135 | return 136 | 137 | if timer and turn_duration > 0 and active: 138 | timer.start() 139 | 140 | 141 | func on_ended_turn(): 142 | active = false 143 | 144 | 145 | func on_timeout(): 146 | ended_turn.emit() 147 | 148 | -------------------------------------------------------------------------------- /addons/turnity/turn_manager.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | signal turnity_socket_connected(socket: TurnitySocket) 4 | signal turnity_socket_disconnected(socket: TurnitySocket) 5 | signal connected_turnity_sockets(sockets: Array[TurnitySocket]) 6 | signal disconnected_turnity_sockets(sockets: Array[TurnitySocket]) 7 | signal turn_changed(previous_socket: TurnitySocket, next_socket: TurnitySocket) 8 | signal activated_turn(current_socket: TurnitySocket) 9 | signal ended_turn(last_socket: TurnitySocket) 10 | signal last_turn_reached 11 | signal finished 12 | 13 | enum MODE { 14 | SERIAL, ## The turns comes one after another 15 | DYNAMIC_QUEUE, ## The queue changes every turn based on the custom sort rule applied 16 | } 17 | 18 | var current_turnity_sockets: Array[TurnitySocket] = [] 19 | var current_turn_socket: TurnitySocket 20 | var current_mode: MODE = MODE.SERIAL 21 | 22 | var sort_rule: Callable = func(a: TurnitySocket, b: TurnitySocket): return a.id > b.id 23 | var turn_duration := 0 24 | var turns_passed := 0 25 | var max_turns := 0 26 | var automatic_move_on_to_the_next_turn := false 27 | 28 | func _enter_tree(): 29 | add_to_group("turnity-manager") 30 | 31 | 32 | func _ready(): 33 | finished.connect(on_finished) 34 | 35 | 36 | func start(root_node = null): 37 | if is_node_ready(): 38 | reset_active_sockets() 39 | current_turnity_sockets = get_active_sockets(root_node) 40 | apply_sort_rule(current_turnity_sockets) 41 | connect_turnity_sockets() 42 | deactivate_sockets(current_turnity_sockets) ## We deactivate all the sockets to only make active the first one on the initialization 43 | set_turn_duration_on_sockets() 44 | 45 | current_turn_socket = current_turnity_sockets.front() 46 | 47 | current_turn_socket.active_turn.emit() 48 | activated_turn.emit(current_turn_socket) 49 | else: 50 | push_error("Turnity: The TurnityManager is not ready or appended into the scene tree, the turn system cannot be initialized") 51 | 52 | 53 | ### CUSTOM BEHAVIOUR FUNCTIONS ### 54 | func set_mode(mode: MODE) -> TurnityManager: 55 | current_mode = mode 56 | 57 | return self 58 | 59 | 60 | func set_serial_mode() -> TurnityManager: 61 | current_mode = MODE.SERIAL 62 | 63 | return self 64 | 65 | 66 | func set_dynamic_queue_mode() -> TurnityManager: 67 | current_mode = MODE.DYNAMIC_QUEUE 68 | 69 | return self 70 | 71 | 72 | func automatically_move_on_to_the_next_turn(enabled: bool = false): 73 | automatic_move_on_to_the_next_turn = enabled 74 | 75 | 76 | func set_limited_turns(turns: int) -> TurnityManager: 77 | max_turns = turns 78 | 79 | return self 80 | 81 | 82 | func set_turn_duration(time: int = 0) -> TurnityManager: 83 | turn_duration = abs(time) 84 | 85 | return self 86 | 87 | 88 | func set_turn_duration_on_sockets() -> void: 89 | if turn_duration > 0: 90 | for socket in current_turnity_sockets: 91 | if socket.turn_duration == 0: ## The local turn duration value of each socket is prioritized 92 | socket.change_turn_duration(turn_duration) 93 | 94 | 95 | func set_sort_rule(callable: Callable) -> TurnityManager: 96 | sort_rule = callable 97 | 98 | return self 99 | 100 | 101 | func apply_sort_rule(sockets: Array[TurnitySocket] = current_turnity_sockets): 102 | sockets.sort_custom(sort_rule) 103 | 104 | 105 | ### SOCKET CONNECTION & DISCONNECTION FUNCTIONS 106 | func deactivate_sockets(sockets: Array[TurnitySocket]) -> void: 107 | for socket in current_turnity_sockets: 108 | socket.active = false 109 | 110 | 111 | func all_sockets_are_disabled(sockets: Array[TurnitySocket]) -> bool: 112 | return sockets.filter(func(socket: TurnitySocket): return socket.is_disabled()).size() == sockets.size() 113 | 114 | 115 | func get_active_sockets(root_node = null) -> Array[TurnitySocket]: 116 | var sockets: Array[TurnitySocket] = [] 117 | var nodes = [] 118 | 119 | if root_node == null: 120 | nodes = get_tree().get_nodes_in_group("turnity-socket").filter(func(node): return node is TurnitySocket) 121 | ## We need to manual append the nodes to a new array to make the static typing works on arrays for the compiler 122 | for socket in nodes: 123 | sockets.append(socket) 124 | else: 125 | read_sockets_from_node(root_node, sockets) 126 | 127 | return sockets 128 | 129 | 130 | func read_sockets_from_node(node: Node, sockets: Array): 131 | var childrens = node.get_children(true) 132 | 133 | for child in childrens: 134 | if child is TurnitySocket: 135 | sockets.append(child) 136 | else: 137 | read_sockets_from_node(child, sockets) 138 | 139 | 140 | func next_turn() -> void: 141 | var next_socket: TurnitySocket 142 | 143 | if all_sockets_are_disabled(current_turnity_sockets) or (max_turns > 0 and turns_passed >= max_turns): 144 | finished.emit() 145 | return 146 | 147 | if not current_turnity_sockets.is_empty(): 148 | turns_passed += 1 149 | 150 | match(current_mode): 151 | MODE.SERIAL: 152 | var index = current_turnity_sockets.find(current_turn_socket) 153 | next_socket = current_turnity_sockets.front() if index + 1 >= current_turnity_sockets.size() else current_turnity_sockets[index + 1] 154 | MODE.DYNAMIC_QUEUE: 155 | ## TODO 156 | pass 157 | 158 | if turns_passed + 1 == max_turns: 159 | last_turn_reached.emit() 160 | 161 | turn_changed.emit(current_turn_socket, next_socket) 162 | next_socket.active_turn.emit() 163 | 164 | 165 | func reset_active_sockets() -> void: 166 | turns_passed = 0 167 | 168 | disconnect_turnity_sockets() 169 | current_turnity_sockets.clear() 170 | current_turn_socket = null 171 | 172 | 173 | func connect_turnity_sockets() -> void: 174 | for socket: TurnitySocket in current_turnity_sockets: 175 | _connect_socket(socket) 176 | 177 | if not current_turnity_sockets.is_empty(): 178 | connected_turnity_sockets.emit(current_turnity_sockets) 179 | 180 | 181 | func disconnect_turnity_sockets() -> void: 182 | if not current_turnity_sockets.is_empty(): 183 | disconnected_turnity_sockets.emit(current_turnity_sockets) 184 | 185 | for socket: TurnitySocket in current_turnity_sockets: 186 | _disconnect_socket(socket) 187 | 188 | 189 | func _connect_socket(socket: TurnitySocket): 190 | var connected_signal_emitted = false 191 | 192 | if not socket.active_turn.is_connected(on_socket_active_turn): 193 | socket.active_turn.connect(on_socket_active_turn.bind(socket)) 194 | turnity_socket_connected.emit(socket) 195 | connected_signal_emitted = true 196 | 197 | if not socket.ended_turn.is_connected(on_socket_ended_turn): 198 | socket.ended_turn.connect(on_socket_ended_turn.bind(socket)) 199 | 200 | if not connected_signal_emitted: 201 | turnity_socket_connected.emit(socket) 202 | 203 | 204 | func _disconnect_socket(socket: TurnitySocket): 205 | var disconnected_signal_emitted = false 206 | 207 | if socket.active_turn.is_connected(on_socket_active_turn): 208 | socket.active_turn.disconnect(on_socket_active_turn.bind(socket)) 209 | turnity_socket_disconnected.emit(socket) 210 | disconnected_signal_emitted = true 211 | 212 | if socket.ended_turn.is_connected(on_socket_ended_turn): 213 | socket.ended_turn.disconnect(on_socket_ended_turn.bind(socket)) 214 | 215 | if not disconnected_signal_emitted: 216 | turnity_socket_disconnected.emit(socket) 217 | 218 | 219 | ### SIGNAL CALLBACKS ### 220 | func on_socket_active_turn(socket: TurnitySocket): 221 | activated_turn.emit(socket) 222 | current_turn_socket = socket 223 | 224 | 225 | func on_socket_ended_turn(socket: TurnitySocket): 226 | ended_turn.emit(socket) 227 | 228 | if automatic_move_on_to_the_next_turn: 229 | next_turn() 230 | 231 | 232 | func on_socket_skipped(socket: TurnitySocket): 233 | if socket.next_turn_when_skipped: 234 | next_turn() 235 | 236 | 237 | func on_socket_blocked_turn_consumed(socket: TurnitySocket): 238 | if socket.next_turn_when_blocked: 239 | next_turn() 240 | 241 | 242 | func on_finished(): 243 | reset_active_sockets() 244 | turn_duration = 0 245 | max_turns = 0 246 | sort_rule = func(a: TurnitySocket, b: TurnitySocket): return a.id > b.id 247 | --------------------------------------------------------------------------------