class_name Popochiu extends Node ## This is Popochiu's main hub, and is in charge of making the game to work. ## ## Is the shortcut for [b]Popochiu.gd[/b], and can be used (from any script) with [b]E[/b] (E.g. ## [code]E.camera.shake()[/code]). ## ## Some things you can do with it: ## - Change to another room. ## - Access the main camera and some game settings. ## - Run commands sequentially (even in a form that makes the skippable). ## - Use some utility methods (such as making a function of yours able to be in a run queue). ## ## Examples ## [codeblock] ## # Makes the player-controlled character say "Hi", wait a second, and then say another thing ## E.queue([ ## "Player: Hi", ## "...", ## "Player: I'm the character you can control!!!", ## ]) ## # Make the camera shake with a strength of 2.0 during 3.0 seconds ## E.camera.shake(2.0, 3.0) ## [/codeblock] ## Emitted when the text speed changes in [PopochiuSettings]. signal text_speed_changed ## Emitted when the language changes in [PopochiuSettings]. signal language_changed ## Emitted after [method save_game] saves a file with the current game data. signal game_saved ## Emitted before a loaded game starts the transition to show the loaded data. signal game_load_started ## Emitted by [method room_readied] when stored game [param data] is loaded for the current room. signal game_loaded(data: Dictionary) ## Emitted when [member current_command] changes. Can be used to know the active command for the ## current GUI template. signal command_selected ## Emitted when the dialog style changes in [PopochiuSettings]. signal dialog_style_changed ## A signal that is never emitted and serves to stop the execution of instructions by clicking ## anywhere in a [PopochiuRoom] when a [PopochiuClickable] has already been clicked. signal await_stopped ## Path to the script with the class used to save and load game data. const SAVELOAD_PATH := "res://addons/popochiu/engine/others/popochiu_save_load.gd" ## Used to prevent going to another room when there is one being loaded. var in_room := false : set = _set_in_room ## @deprecated ## [b]Deprecated[/b]. Now this is [member PopochiuIRoom.current]. var current_room: PopochiuRoom ## Stores the last clicked [PopochiuClickable] node to ease access to it from any other class. var clicked: PopochiuClickable = null ## Stores the last hovered [PopochiuClickable] node to ease access to it from any other class. var hovered: PopochiuClickable = null : get = get_hovered, set = set_hovered ## Used to know if a cutscene was skipped. ## A reference to [PopochiuSettings]. Can be used to quickly access its members. var settings := PopochiuSettings.new() ## Reference to the [PopochiuAudioManager]. var am: PopochiuAudioManager = null # NOTE: This might not just be a boolean, but there could be an array that puts the calls to queue # in an Array and executes them in order. Or perhaps it could be something that allows for more # dynamism, such as putting one queue to execute during the execution of another one. ## Indicates if the game is playing a queue of instructions. var playing_queue := false ## Reference to the [PopochiuGraphicInterface]. var gui: PopochiuGraphicInterface = null ## Reference to the [PopochiuTransitionLayer]. var tl: Control = null ## The current class used as the game commands var cutscene_skipped := false ## @deprecated ## [b]Deprecated[/b]. Now this is [member PopochiuIRoom.rooms_states]. var rooms_states := {} ## Stores a list of game events (triggered actions and dialog lines). Each event is defined by a ## [Dictionary]. var history := [] ## The width, in pixels, of the game native resolution ## (that is [code]get_viewport().get_visible_rect().end.x[/code]). var width := 0.0 : get = get_width ## The height, in pixels, of the game native resolution ## (that is [code]get_viewport().get_visible_rect().end.y[/code]). var height := 0.0 : get = get_height ## [member width] divided by 2. var half_width := 0.0 : get = get_half_width ## [member height] divided by 2. var half_height := 0.0 : get = get_half_height ## The text speed being used by the game. When this property changes, the ## [signal text_speed_changed] signal is emitted. var text_speed: float = settings.text_speed : set = set_text_speed ## The number of seconds to wait before moving to the next dialog line (when playing dialog lines ## triggered inside a [method queue]). var auto_continue_after := -1.0 ## The current dialog style used by the game. When this property changes, the ## [signal dialog_style_changed] signal is emitted. var current_dialog_style := settings.dialog_style : set = set_dialog_style ## The scale value of the game. Defined by the native game resolution compared with (356, 200), ## which is the default game resolution defined by Popochiu. var scale := Vector2.ONE ## A reference to the current commands script. ## (i.e. [NineVerbCommands], [SierraCommands] or [SimpleClickCommands]) var commands: PopochiuCommands = null ## Serves as a map to access the fallback methods of the current GUI. var commands_map := { -1: { "name" = "fallback", fallback = _command_fallback } } ## The ID of the current active command in the GUI. When this property changes, the ## [signal command_selected] signal is emitted. var current_command := -1 : set = set_current_command var loaded_game := {} var _hovered_queue := [] # Will have the instance of the PopochiuSaveLoad class in order to call the methods that save and # load the game. var _saveload: Resource = null ## A reference to the [PopochiuMainCamera]. @onready var camera: PopochiuMainCamera = %PopochiuMainCamera #region Godot ###################################################################################### func _init() -> void: Engine.register_singleton(&"E", self) func _ready() -> void: set_process_input(false) _saveload = load(SAVELOAD_PATH).new() # Create the AudioManager am = load(PopochiuResources.AUDIO_MANAGER).instantiate() # Instantiate the Graphic Interface node if settings.dev_use_addon_template: var template: String = PopochiuResources.get_data_value("ui", "template", "") var path := PopochiuResources.GUI_CUSTOM_SCENE if template != "custom": template = template.to_snake_case() path = PopochiuResources.GUI_TEMPLATES_FOLDER + "%s/%s_gui.tscn" % [template, template] gui = load(path).instantiate() else: gui = load(PopochiuResources.GUI_GAME_SCENE).instantiate() gui.name = "GUI" # Load the commands for the game commands = load(PopochiuResources.GUI_COMMANDS).new() # Instantiate the Transitions Layer node tl = load(PopochiuResources.TRANSITION_LAYER_ADDON).instantiate() # Calculate the scale that could be applied scale = Vector2(width, height) / PopochiuResources.RETRO_RESOLUTION # Add the AudioManager, the Graphic Interface, and the Transitions Layer to the tree $GraphicInterfaceLayer.add_child(gui) $TransitionsLayer.add_child(tl) add_child(am) # Load the Player-controlled Character (PC) PopochiuCharactersHelper.define_player() # Add inventory items checked to start with await get_tree().process_frame for key in settings.items_on_start: var ii: PopochiuInventoryItem = PopochiuUtils.i.get_item_instance(key) if is_instance_valid(ii): ii.add(false) if settings.scale_gui: PopochiuUtils.cursor.scale_cursor(scale) PopochiuUtils.r.store_states() # Connect to autoloads' signals PopochiuUtils.c.character_spoke.connect(_on_character_spoke) # Assign property values to singletons and other global classes PopochiuUtils.g.gui = gui func _input(event: InputEvent) -> void: if event.is_action_released("popochiu-skip"): cutscene_skipped = true tl.play_transition(PopochiuTransitionLayer.PASS_DOWN_IN, settings.skip_cutscene_time) await tl.transition_finished func _unhandled_key_input(event: InputEvent) -> void: # TODO: Capture keys for debugging or for triggering game signals that can ease tests pass #endregion #region Public ##################################################################################### ## Creates a delay timer that will last [param time] seconds. This method is intended to be used ## inside a [method queue] of instructions. func queue_wait(time := 1.0) -> Callable: return func (): await wait(time) ## Creates a delay timer that will last [param time] seconds. func wait(time := 1.0) -> void: if cutscene_skipped: await get_tree().process_frame return await get_tree().create_timer(time).timeout # TODO: Stop or break a queue in execution #func break_queue() -> void: # pass ## Executes an array of [param instructions] one by one. [param show_gui] determines if the ## Graphic Interface will appear once all instructions have ran. func queue(instructions: Array, show_gui := true) -> void: if instructions.is_empty(): await get_tree().process_frame return if playing_queue: await get_tree().process_frame await queue(instructions, show_gui) return playing_queue = true PopochiuUtils.g.block() for idx in instructions.size(): var instruction = instructions[idx] if instruction is Callable: await instruction.call() elif instruction is String: await PopochiuCharactersHelper.execute_string(instruction as String) if show_gui: PopochiuUtils.g.unblock() if camera.is_shaking: camera.stop_shake() if instructions.is_empty(): await get_tree().process_frame playing_queue = false ## Like [method queue], but [param instructions] can be skipped with the input action: ## [code]popochiu-skip[/code] (see [b]Project Settings... > Input Map[/b]). By default you can skip ## a cutscene with the [kbd]ESC[/kbd] key. func cutscene(instructions: Array) -> void: set_process_input(true) await queue(instructions) set_process_input(false) if cutscene_skipped: tl.play_transition(tl.PASS_DOWN_OUT, settings.skip_cutscene_time) await tl.transition_finished cutscene_skipped = false ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuIRoom.goto_room]. func goto_room( script_name := "", use_transition := true, store_state := true, ignore_change := false ) -> void: PopochiuUtils.r.goto_room(script_name, use_transition, store_state, ignore_change) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuIRoom.room_readied]. func room_readied(room: PopochiuRoom) -> void: PopochiuUtils.r.room_readied(room) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuMainCamera.queue_change_offset]. func queue_camera_offset(offset := Vector2.ZERO) -> Callable: return camera.queue_change_offset(offset) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuMainCamera.change_offset]. func camera_offset(offset := Vector2.ZERO) -> void: camera.change_offset(offset) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuMainCamera.queue_shake]. func queue_camera_shake(strength := 1.0, duration := 1.0) -> Callable: return camera.queue_shake(strength, duration) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuMainCamera.shake]. func camera_shake(strength := 1.0, duration := 1.0) -> void: camera.shake(strength, duration) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuMainCamera.queue_shake_bg]. func queue_camera_shake_bg(strength := 1.0, duration := 1.0) -> Callable: return camera.queue_shake_bg(strength, duration) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuMainCamera.shake_bg]. func camera_shake_bg(strength := 1.0, duration := 1.0) -> void: camera.shake_bg(strength, duration) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuMainCamera.queue_change_zoom]. func queue_camera_zoom(target := Vector2.ONE, duration := 1.0) -> Callable: return camera.queue_change_zoom(target, duration) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuMainCamera.change_zoom]. func camera_zoom(target := Vector2.ONE, duration := 1.0) -> void: camera.change_zoom(target, duration) ## Returns [param msg] translated to the current language if the game is using translations ## [member PopochiuSettings.use_translations]. Otherwise, the returned [String] will be the same ## as the one received as a parameter. func get_text(msg: String) -> String: return tr(msg) if settings.use_translations else msg ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuICharacter.get_instance]. func get_character_instance(script_name: String) -> PopochiuCharacter: return PopochiuUtils.c.get_instance(script_name) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuIInventory.get_instance]. func get_inventory_item_instance(script_name: String) -> PopochiuInventoryItem: return PopochiuUtils.i.get_instance(script_name) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuIDialog.get_instance]. func get_dialog(script_name: String) -> PopochiuDialog: return PopochiuUtils.d.get_instance(script_name) ## Adds an action, represented by [param data], to the [member history] of actions. ## The structure that [param data] can have may be in the form: ## [codeblock]# To store the Look At interaction with the prop ToyCar: ## { ## action = "look_at", ## target = "ToyCar" ## }[/codeblock] ## or ## [codeblock]# To store a dialog line said by the Popsy character ## { ## character = "Popsy", ## text = "Hi. I said this and now it is recorded in the game's log!" ## }[/codeblock] ## [method PopochiuClickable.handle_command] and [method PopochiuInventoryItem.handle_command] store ## interactions with clickables and inventory items. ## [method PopochiuGraphicInterface.on_dialog_line_started] stores dialog lines said by characters. func add_history(data: Dictionary) -> void: history.push_front(data) ## Makes a [param method] in [param node] to be able to be used inside an array of instructions for ## [method queue]. Parameters for [param method] can be passed as an array in [param params]. ## By default the queued method will wait for [code]"completed"[/code], but in can wait for a ## specific signal given the [param signal_name]. ## Examples: ## [codeblock] ## # queue() will wait until $AnimationPlayer.animation_finished signal is emitted ## E.queue([ ## "Player: Ok. This is a queueable example", ## E.queueable($AnimationPlayer, "play", ["glottis_appears"], "animation_finished"), ## "Popsy: Hi Goddiu!", ## "Player: You're finally here!!!" ## ]) ## [/codeblock] ## An example with a custom method: ## [codeblock] ## # queue pauses until _make_glottis_appear.completed signal is emitted ## func _ready() -> void: ## E.queue([ ## "Player: Ok. This is another queueable example", ## E.queueable(self, "_make_glottis_appear", [], "completed"), ## "Popsy: Hi Goddiu!", ## "Player: So... you're finally here!!!", ## ]) ## ## func _make_glottis_appear() -> void: ## $AnimationPlayer.play("make_glottis_appear") ## await $AnimationPlayer.animation_finished ## Globals.glottis_appeared = true ## await E.wait(1.0) ## [/codeblock] ## An example with a custom signal ## [codeblock] ## # queue pauses until the "clicked" signal is emitted in the %PopupButton ## # ---- In some prop ---- ## func on_click() -> void: ## E.run([ ## "Player: Ok. This is the last queueable example.", ## "Player: Promise!", ## E.queueable(%PopupButton, "_show_button", [], "clicked"), ## "Popsy: Are we done!?", ## "Player: Yup", ## ]) ## ## # ---- In the PopupButton node ---- ## signal clicked ## ## func _show_button() -> void: ## $BtnPlay.show() ## ## func _on_BtnPlay_pressed() -> void: ## await A.mx_mysterious_place.play() ## clicked.emit() ## [/codeblock] func queueable(node: Object, method: String, params := [], signal_name := "") -> Callable: return func (): await _queueable(node, method, params, signal_name) ## Plays the transition [param type] animation in the [TransitionLayer] with a [param duration] in ## seconds. Available type values can be found in [member TransitionLayer.Types]. This method is ## intended to be used inside a [method queue] of instructions. func queue_play_transition(type: int, duration: float) -> Callable: return func (): await play_transition(type, duration) ## Plays the transition [param type] animation in the [TransitionLayer] with a [param duration] in ## seconds. Available type values can be found in [member TransitionLayer.Types]. func play_transition(type: int, duration: float) -> void: tl.play_transition(type, duration) await tl.transition_finished ## Checks if there are any saved game sessions in the game's folder. By default Godot's ## [code]user://[/code] (you can open this folder with [b]Project > Open User Data Folder[/b]). func has_save() -> bool: return !_saveload.get_saves_descriptions().is_empty() ## Counts the number of saved game files in the game's folder. By default Godot's ## [code]user://[/code] (you can open this folder with [b]Project > Open User Data Folder[/b]). func saves_count() -> int: return _saveload.count_saves() ## Gets the names of the saved games (the name given to the slot when the game is saved). func get_saves_descriptions() -> Dictionary: return _saveload.get_saves_descriptions() ## Saves the current game state in a given [param slot] with the name in [param description]. func save_game(slot := 1, description := "") -> void: if _saveload.save_game(slot, description): game_saved.emit() ## Loads the game in the given [param slot]. func load_game(slot := 1) -> void: PopochiuUtils.i.clean_inventory(true) if PopochiuUtils.d.current_dialog: PopochiuUtils.d.current_dialog.stop() loaded_game = _saveload.load_game(slot) if loaded_game.is_empty(): return game_load_started.emit() PopochiuUtils.r.goto_room( loaded_game.player.room, true, false # Do not store the state of the current room ) ## @deprecated ## [b]Deprecated[/b]. Now this is done by [method PopochiuMainCamera.stop_shake]. func stop_camera_shake() -> void: camera.stop_shake() ## Adds the [param node] to the array of hovered PopochiuClickable. If [param prepend] is ## [code]true[/code], then the [param node] will be added at the beginning of the array. func add_hovered(node: PopochiuClickable, prepend := false) -> void: if prepend: _hovered_queue.push_front(node) else: _hovered_queue.append(node) ## Removes a [param node] from the array of hovered PopochiuClickable. Returns [code]true[/code] ## if, after deletion, the array becomes empty. func remove_hovered(node: PopochiuClickable) -> bool: _hovered_queue.erase(node) if not _hovered_queue.is_empty() and is_instance_valid(_hovered_queue[-1]): var clickable: PopochiuClickable = _hovered_queue[-1] PopochiuUtils.g.mouse_entered_clickable.emit(clickable) return false return true ## Clears the array of hovered PopochiuClickable. func clear_hovered() -> void: _hovered_queue.clear() self.hovered = null ## Registers a GUI command identified by [param id], with name [param command_name] and a ## [param fallback] method to be called when the object receiving the interaction doesn't has an ## implementation for the registered command. func register_command(id: int, command_name: String, fallback: Callable) -> void: commands_map[id] = { "name" = command_name, "fallback" = fallback } ## Registers a GUI command with just its name in [param command_name] and a [param fallback] method ## to be called when the object receiving the interaction doesn't has an implementation for the ## registered command. Returns the [code]id[/code] assigned to the registered command. func register_command_without_id(command_name: String, fallback: Callable) -> int: var id := commands_map.size() register_command(id, command_name, fallback) return id ## Calls the fallback method registered for the current active GUI command. If no fallback method is ## registered, [method _command_fallback] is called. func command_fallback() -> void: var fallback: Callable = commands_map[-1].fallback if commands_map.has(current_command): fallback = commands_map[current_command].fallback await fallback.call() ## Returns the name of the GUI command registered with [param command_id]. func get_command_name(command_id: int) -> String: var command_name := "" if commands_map.has(command_id): command_name = commands_map[command_id].name return command_name ## Returns the name of the current active GUI command. func get_current_command_name() -> String: return get_command_name(current_command) #endregion #region SetGet ##################################################################################### func get_width() -> float: return get_viewport().get_visible_rect().end.x func get_height() -> float: return get_viewport().get_visible_rect().end.y func get_half_width() -> float: return get_viewport().get_visible_rect().end.x / 2.0 func get_half_height() -> float: return get_viewport().get_visible_rect().end.y / 2.0 func set_hovered(value: PopochiuClickable) -> void: hovered = value if not hovered: PopochiuUtils.g.show_hover_text() func get_hovered() -> PopochiuClickable: if not _hovered_queue.is_empty() and is_instance_valid(_hovered_queue[-1]): return _hovered_queue[-1] return null func set_text_speed(value: float) -> void: text_speed = value text_speed_changed.emit() func set_current_command(value: int) -> void: current_command = value command_selected.emit() func set_dialog_style(value: int) -> void: current_dialog_style = value dialog_style_changed.emit() #endregion #region Private #################################################################################### func _set_in_room(value: bool) -> void: in_room = value PopochiuUtils.cursor.toggle_visibility(in_room) func _queueable(node: Object, method: String, params := [], signal_name := "") -> void: if cutscene_skipped: # TODO: What should happen if the skipped function was an animation that triggers calls # during execution? What should happen if the skipped function has to change the state of # the game? await get_tree().process_frame return var f := Callable(node, method) var c = f.callv(params) if not signal_name.is_empty(): if signal_name == "completed": await c else: # TODO: Is there a better way to do this in GDScript 2? await node.get(signal_name) else: await get_tree().process_frame func _on_character_spoke(chr: PopochiuCharacter, msg := "") -> void: add_history({ character = chr, text = msg }) func _command_fallback() -> void: PopochiuUtils.print_warning("[color=red]No fallback for that command![/color]") #endregion