├── HBEngine ├── Config │ ├── Variables.yaml │ ├── EngineAssetRegistry.yaml │ └── Game.yaml ├── Content │ ├── Music │ │ └── Jazz │ │ │ ├── Accreditation.txt │ │ │ └── bensound-romantic.mp3 │ ├── SFX │ │ └── whistle.wav │ ├── Sprites │ │ ├── Placeholder.png │ │ ├── Backgrounds │ │ │ ├── Basic_Dark_1280x720.png │ │ │ └── Orientation_Lines_1280x720.jpg │ │ ├── Interface │ │ │ ├── Buttons │ │ │ │ ├── Checkbox_Icon.png │ │ │ │ ├── Checkbox_Hover.png │ │ │ │ ├── Checkbox_Normal.png │ │ │ │ ├── Checkbox_Clicked.png │ │ │ │ ├── Menu_Button_Hover.png │ │ │ │ ├── Menu_Button_Normal.png │ │ │ │ ├── Choice_Button_Hover.png │ │ │ │ ├── Choice_Button_Normal.png │ │ │ │ ├── Menu_Button_Clicked.png │ │ │ │ └── Choice_Button_Clicked.png │ │ │ ├── Dialogue │ │ │ │ └── Dialogue_Frame.png │ │ │ └── Backgrounds │ │ │ │ └── Dark_Transparent_1280x720.png │ │ └── TransitionEffects │ │ │ ├── transition_fade_black.png │ │ │ └── transition_fade_white.png │ ├── Fonts │ │ └── Comfortaa │ │ │ ├── Comfortaa-Bold.ttf │ │ │ ├── Comfortaa-Light.ttf │ │ │ ├── Comfortaa-Regular.ttf │ │ │ ├── FONTLOG.txt │ │ │ └── OFL.txt │ └── Interfaces │ │ ├── dialogue_01.interface │ │ ├── pause_menu_01.interface │ │ └── main_menu_01.interface └── Core │ ├── DataTypes │ ├── input_states.py │ └── file_types.py │ ├── Objects │ ├── choice.py │ ├── interface_pause.py │ ├── checkbox.py │ ├── renderable_sprite.py │ ├── interface.py │ ├── renderable_group.py │ ├── renderable_container.py │ ├── button.py │ └── audio.py │ ├── Actions │ └── transitions.py │ └── Modules │ └── dialogue.py ├── launch_editor.bat ├── requirements.txt ├── HBEditor ├── Content │ ├── Icons │ │ ├── Up.png │ │ ├── Close.png │ │ ├── Copy.png │ │ ├── Down.png │ │ ├── Drag.png │ │ ├── File.png │ │ ├── Lock.png │ │ ├── Minus.png │ │ ├── Plus.png │ │ ├── Scene.png │ │ ├── Trash.png │ │ ├── Dialogue.png │ │ ├── Folder.png │ │ ├── Import.png │ │ ├── Rename.png │ │ ├── Arrow_Down.png │ │ ├── Checkmark.png │ │ ├── Condition.png │ │ ├── File_Image.png │ │ ├── Interface.png │ │ ├── Renderable.png │ │ ├── Small_Plus.png │ │ ├── Arrow_Right.png │ │ ├── Close_Hover.png │ │ ├── Color_Wheel.png │ │ ├── Engine_Logo.png │ │ ├── Information.png │ │ ├── Small_Minus.png │ │ ├── ChangesPending.png │ │ ├── Folder_Disabled.png │ │ ├── Arrow_Down_Hover.png │ │ ├── Arrow_Right_Hover.png │ │ ├── Checkmark_Disabled.png │ │ ├── SceneItem_Sprite.png │ │ ├── Small_Minus_Hover.png │ │ ├── Small_Plus_Hover.png │ │ ├── Color_Wheel_Disabled.png │ │ └── Connection_Inactive.png │ ├── Sprites │ │ └── Placeholder.png │ ├── Themes │ │ └── DarkGray │ │ │ └── ACCREDITATION.txt │ ├── Fonts │ │ └── Cabin │ │ │ ├── Cabin-Bold.ttf │ │ │ ├── Cabin-Italic.ttf │ │ │ ├── Cabin-Medium.ttf │ │ │ ├── Cabin-Regular.ttf │ │ │ ├── Cabin-SemiBold.ttf │ │ │ ├── Cabin-BoldItalic.ttf │ │ │ ├── Cabin-MediumItalic.ttf │ │ │ ├── Cabin-SemiBoldItalic.ttf │ │ │ ├── Readme.txt │ │ │ ├── FONTLOG.txt │ │ │ └── OFL.txt │ ├── Images │ │ └── Thumbnails │ │ │ ├── Template_Blank.jpg │ │ │ ├── Template_Interface_Dialogue.jpg │ │ │ ├── Template_Interface_Main_Menu.jpg │ │ │ └── Template_Interface_Pause_Menu.jpg │ └── content.qrc ├── Config │ ├── Editor.yaml │ └── Actions.yaml └── Core │ ├── DataTypes │ ├── log_types.py │ ├── parameter_types.py │ └── file_types.py │ ├── base_editor_ui.py │ ├── EditorCommon │ ├── DetailsPanel │ │ └── base_source_entry.py │ ├── connection_button.py │ └── SceneViewer │ │ └── scene_view.py │ ├── EditorUtilities │ ├── utils.py │ ├── image.py │ ├── path.py │ └── font.py │ ├── Primitives │ ├── toggleable_menu_action.py │ └── simple_checkbox.py │ ├── Dialogs │ ├── dialog_list.py │ ├── dialog_new_file_from_template.py │ └── dialog_file_system.py │ ├── base_editor.py │ ├── engine_launcher.py │ ├── EditorProjectSettings │ ├── editor_project_settings.py │ └── editor_project_settings_ui.py │ ├── EditorInterface │ ├── dialog_new_interface.py │ └── editor_interface_ui.py │ ├── ActionMenu │ └── action_menu.py │ ├── Logger │ ├── logger_ui.py │ └── logger.py │ ├── EditorVariables │ └── editor_variables.py │ ├── EditorDialogue │ └── editor_dialogue_ui.py │ ├── EditorScene │ └── editor_scene_ui.py │ └── Outliner │ └── outliner.py ├── Images ├── v02_Dialogue_Scene.png ├── General_Editor_Example.png ├── HBEditor_Dialogue_Editor.png ├── HeartbeatEngine_Banner.jpg └── Interface_Editor_Template_Options.jpg ├── Tools ├── rebuild_editor_css.bat ├── HBYaml │ ├── CustomTags │ │ └── connection.py │ └── hb_yaml.py └── HBBuilder │ └── hb_builder.py ├── main.py ├── .gitignore └── README.md /HBEngine/Config/Variables.yaml: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /launch_editor.bat: -------------------------------------------------------------------------------- 1 | set PYTHONPATH=%cd% 2 | %~dp0/venv/Scripts/python.exe main.py 3 | exit -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/requirements.txt -------------------------------------------------------------------------------- /HBEngine/Content/Music/Jazz/Accreditation.txt: -------------------------------------------------------------------------------- 1 | https://www.bensound.com/royalty-free-music/track/romantic -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Up.png -------------------------------------------------------------------------------- /Images/v02_Dialogue_Scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/Images/v02_Dialogue_Scene.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Close.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Copy.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Down.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Drag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Drag.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/File.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/File.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Lock.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Minus.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Plus.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Scene.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Trash.png -------------------------------------------------------------------------------- /HBEngine/Content/SFX/whistle.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/SFX/whistle.wav -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Dialogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Dialogue.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Folder.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Import.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Rename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Rename.png -------------------------------------------------------------------------------- /Images/General_Editor_Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/Images/General_Editor_Example.png -------------------------------------------------------------------------------- /Images/HBEditor_Dialogue_Editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/Images/HBEditor_Dialogue_Editor.png -------------------------------------------------------------------------------- /Images/HeartbeatEngine_Banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/Images/HeartbeatEngine_Banner.jpg -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Arrow_Down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Arrow_Down.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Checkmark.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Condition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Condition.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/File_Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/File_Image.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Interface.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Renderable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Renderable.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Small_Plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Small_Plus.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Arrow_Right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Arrow_Right.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Close_Hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Close_Hover.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Color_Wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Color_Wheel.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Engine_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Engine_Logo.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Information.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Small_Minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Small_Minus.png -------------------------------------------------------------------------------- /HBEditor/Content/Sprites/Placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Sprites/Placeholder.png -------------------------------------------------------------------------------- /HBEditor/Content/Themes/DarkGray/ACCREDITATION.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) <2013-2014> 2 | Copyright (c) <2017> -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Placeholder.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/ChangesPending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/ChangesPending.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Folder_Disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Folder_Disabled.png -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/Cabin-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Fonts/Cabin/Cabin-Bold.ttf -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/Cabin-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Fonts/Cabin/Cabin-Italic.ttf -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/Cabin-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Fonts/Cabin/Cabin-Medium.ttf -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Arrow_Down_Hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Arrow_Down_Hover.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Arrow_Right_Hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Arrow_Right_Hover.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Checkmark_Disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Checkmark_Disabled.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/SceneItem_Sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/SceneItem_Sprite.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Small_Minus_Hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Small_Minus_Hover.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Small_Plus_Hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Small_Plus_Hover.png -------------------------------------------------------------------------------- /Images/Interface_Editor_Template_Options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/Images/Interface_Editor_Template_Options.jpg -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/Cabin-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Fonts/Cabin/Cabin-Regular.ttf -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/Cabin-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Fonts/Cabin/Cabin-SemiBold.ttf -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Color_Wheel_Disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Color_Wheel_Disabled.png -------------------------------------------------------------------------------- /HBEditor/Content/Icons/Connection_Inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Icons/Connection_Inactive.png -------------------------------------------------------------------------------- /Tools/rebuild_editor_css.bat: -------------------------------------------------------------------------------- 1 | ..\venv\Scripts\python.exe -m qtsass ..\HBEditor\Content\Themes\Dark\Dark.scss -o ..\HBEditor\Content\Themes\Dark\Dark.css -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/Cabin-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Fonts/Cabin/Cabin-BoldItalic.ttf -------------------------------------------------------------------------------- /HBEngine/Content/Music/Jazz/bensound-romantic.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Music/Jazz/bensound-romantic.mp3 -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/Cabin-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Fonts/Cabin/Cabin-MediumItalic.ttf -------------------------------------------------------------------------------- /HBEngine/Content/Fonts/Comfortaa/Comfortaa-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Fonts/Comfortaa/Comfortaa-Bold.ttf -------------------------------------------------------------------------------- /HBEngine/Content/Fonts/Comfortaa/Comfortaa-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Fonts/Comfortaa/Comfortaa-Light.ttf -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/Cabin-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Fonts/Cabin/Cabin-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /HBEditor/Content/Images/Thumbnails/Template_Blank.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Images/Thumbnails/Template_Blank.jpg -------------------------------------------------------------------------------- /HBEngine/Content/Fonts/Comfortaa/Comfortaa-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Fonts/Comfortaa/Comfortaa-Regular.ttf -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Backgrounds/Basic_Dark_1280x720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Backgrounds/Basic_Dark_1280x720.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Icon.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Hover.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Normal.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Dialogue/Dialogue_Frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Dialogue/Dialogue_Frame.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Clicked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Clicked.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Buttons/Menu_Button_Hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Buttons/Menu_Button_Hover.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Buttons/Menu_Button_Normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Buttons/Menu_Button_Normal.png -------------------------------------------------------------------------------- /HBEditor/Content/Images/Thumbnails/Template_Interface_Dialogue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Images/Thumbnails/Template_Interface_Dialogue.jpg -------------------------------------------------------------------------------- /HBEditor/Content/Images/Thumbnails/Template_Interface_Main_Menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Images/Thumbnails/Template_Interface_Main_Menu.jpg -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Backgrounds/Orientation_Lines_1280x720.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Backgrounds/Orientation_Lines_1280x720.jpg -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Buttons/Choice_Button_Hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Buttons/Choice_Button_Hover.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Buttons/Choice_Button_Normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Buttons/Choice_Button_Normal.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Buttons/Menu_Button_Clicked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Buttons/Menu_Button_Clicked.png -------------------------------------------------------------------------------- /HBEditor/Content/Images/Thumbnails/Template_Interface_Pause_Menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEditor/Content/Images/Thumbnails/Template_Interface_Pause_Menu.jpg -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Buttons/Choice_Button_Clicked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Buttons/Choice_Button_Clicked.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/TransitionEffects/transition_fade_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/TransitionEffects/transition_fade_black.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/TransitionEffects/transition_fade_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/TransitionEffects/transition_fade_white.png -------------------------------------------------------------------------------- /HBEngine/Content/Sprites/Interface/Backgrounds/Dark_Transparent_1280x720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cronza/HeartbeatEngine/HEAD/HBEngine/Content/Sprites/Interface/Backgrounds/Dark_Transparent_1280x720.png -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/Readme.txt: -------------------------------------------------------------------------------- 1 | Impallari Type 2 | http://www.impallari.com/ 3 | 4 | Our fonts are free for both personal and commercial use. 5 | 6 | Please donate if you like our fonts! 7 | Via paypal to impallari@gmail.com 8 | 9 | Thanks 10 | Pablo Impallari 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /HBEditor/Config/Editor.yaml: -------------------------------------------------------------------------------- 1 | EditorSettings: 2 | version: 0.1 3 | version_string: Created with Heartbeat Editor v0.1 4 | max_tabs: 3 5 | active_theme: "EditorContent:Themes/Dark/Dark.css" 6 | active_fonts: 7 | - "EditorContent:Fonts/Cabin/Cabin-Regular.ttf" 8 | - "EditorContent:Fonts/Cabin/Cabin-Medium.ttf" 9 | - "EditorContent:Fonts/Cabin/Cabin-MediumItalic.ttf" 10 | - "EditorContent:Fonts/Cabin/Cabin-Bold.ttf" 11 | - "EditorContent:Fonts/Cabin/Cabin-BoldItalic.ttf" 12 | - "EditorContent:Fonts/Cabin/Cabin-SemiBold.ttf" 13 | - "EditorContent:Fonts/Cabin/Cabin-SemiBoldItalic.ttf" 14 | Outliner: 15 | icon_size: 16 | - 40 17 | - 40 -------------------------------------------------------------------------------- /Tools/HBYaml/CustomTags/connection.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | """ 4 | A custom YAML tag designed to create a 'Connection' object that denotes when action data parameters are assigned to 5 | variables as opposed to having normal values. 6 | """ 7 | 8 | 9 | class Connection: 10 | def __init__(self, variable): 11 | self.variable = variable 12 | 13 | 14 | """ Define the YAML -> Python representation""" 15 | def connection_constructor(loader: yaml.FullLoader, node: yaml.nodes.ScalarNode): 16 | return Connection(loader.construct_scalar(node)) 17 | 18 | 19 | """ Define the Python -> YAML representation """ 20 | def connection_representer(dumper: yaml.SafeDumper, connection: Connection) -> yaml.nodes.ScalarNode: 21 | return dumper.represent_scalar('!Connection', connection.variable) 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import sys 16 | from HBEditor.hb_editor import HBEditor 17 | 18 | 19 | editor = HBEditor() 20 | -------------------------------------------------------------------------------- /HBEngine/Core/DataTypes/input_states.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import enum 16 | 17 | 18 | class State(enum.Enum): 19 | normal = 0 20 | hover = 1 21 | pressed = 2 -------------------------------------------------------------------------------- /HBEditor/Core/DataTypes/log_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from enum import Enum 16 | 17 | 18 | class LogType(Enum): 19 | Normal = 1 20 | Success = 2 21 | Warning = 3 22 | Error = 4 -------------------------------------------------------------------------------- /HBEngine/Core/DataTypes/file_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from enum import Enum 16 | 17 | 18 | class FileType(Enum): 19 | Scene = 1 20 | Dialogue = 2 21 | Project_Settings = 3 22 | -------------------------------------------------------------------------------- /HBEngine/Core/Objects/choice.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from HBEngine.Core.Objects.renderable_container import Container 16 | 17 | 18 | class Choice(Container): 19 | def __init__(self, renderable_data): 20 | super().__init__(renderable_data) 21 | self.visible = False 22 | 23 | # Pass in a button list, and generate buttons 24 | if 'choices' not in self.renderable_data: 25 | raise ValueError(f"No 'choices' block assigned to {self}. This makes for an impossible action!") 26 | -------------------------------------------------------------------------------- /HBEngine/Core/Objects/interface_pause.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import pygame 16 | from HBEngine.Core import settings 17 | from HBEngine.Core.Objects.renderable import Renderable 18 | from HBEngine.Core.Objects.interface import Interface 19 | 20 | 21 | class InterfacePause(Interface): 22 | def __init__(self, renderable_data: dict, parent: Renderable = None): 23 | renderable_data["key"] = "!&HBENGINE_INTERNAL_PAUSE_INTERFACE!&" 24 | renderable_data["z_order"] = 10000000000 25 | super().__init__(renderable_data, parent) 26 | 27 | -------------------------------------------------------------------------------- /HBEngine/Config/EngineAssetRegistry.yaml: -------------------------------------------------------------------------------- 1 | # This file contains a registry of engine content files that are accessible by users. This file is manually maintained, 2 | # and should not be touched 3 | Content: 4 | Interfaces: 5 | pause_menu_01.interface: Interface 6 | dialogue_01.interface: Interface 7 | main_menu_01.interface: Interface 8 | Fonts: 9 | Comfortaa: 10 | Comfortaa-Bold.ttf: Asset_Font 11 | Comfortaa-Light.ttf: Asset_Font 12 | Comfortaa-Regular.ttf: Asset_Font 13 | Music: 14 | Jazz: 15 | bensound-romantic.mp3: Asset_Sound 16 | SFX: 17 | whistle.wav: Asset_Sound 18 | Sprites: 19 | Interface: 20 | Buttons: 21 | Choice_Button_Normal.png: Asset_Image 22 | Choice_Button_Hover.png: Asset_Image 23 | Choice_Button_Clicked.png: Asset_Image 24 | Menu_Button_Normal.png: Asset_Image 25 | Menu_Button_Hover.png: Asset_Image 26 | Menu_Button_Clicked.png: Asset_Image 27 | Dialogue: 28 | Dialogue_Frame.png: Asset_Image 29 | Backgrounds: 30 | Dark_Transparent_1280x720.png: Asset_Image 31 | Placeholder.png: Asset_Image 32 | Backgrounds: 33 | Orientation_Lines_1280x720.jpg: Asset_Image 34 | Basic_Dark_1280x720.png: Asset_Image -------------------------------------------------------------------------------- /HBEditor/Core/DataTypes/parameter_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from enum import Enum 16 | 17 | 18 | class ParameterType(Enum): 19 | String = 1 20 | Bool = 2 21 | Int = 3 22 | Float = 4 23 | Vector2 = 5 24 | Paragraph = 6 25 | Color = 7 26 | Scene = 8 27 | Dialogue = 9 28 | Interface = 10 29 | Asset_Data = 11 30 | Asset_Font = 12 31 | Asset_Image = 13 32 | Asset_Sound = 14 33 | Dropdown = 15 34 | Container = 16 35 | Array_Element = 17 36 | Array = 18 37 | Event = 19 38 | CUST_Resolution = 20 39 | -------------------------------------------------------------------------------- /HBEditor/Core/base_editor_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from PyQt6 import QtWidgets, QtCore 16 | 17 | 18 | class EditorBaseUI(QtWidgets.QWidget): 19 | SIG_USER_UPDATE = QtCore.pyqtSignal() # Emitted whenever any editor child widget is modified by the user 20 | SIG_USER_SAVE = QtCore.pyqtSignal() # Emitted whenever the user saves this editor 21 | 22 | def __init__(self, core_ref): 23 | super().__init__() 24 | 25 | self.core = core_ref 26 | self.pending_changes = False 27 | 28 | def AdjustSize(self): 29 | """ 30 | Adjust the size of child widgets. This is typically only called when the window has changed in a meaningful way 31 | """ 32 | pass 33 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorCommon/DetailsPanel/base_source_entry.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | 16 | 17 | class SourceEntry: 18 | """ A base class for all widgets that may be considered "Source" or "Active" entries to the details panel """ 19 | def Refresh(self, change_tree: list = None): 20 | """ 21 | Called when a relevant change was made in the details panel, and the active source needs to be informed. If 22 | 'change_tree' was not provided, then consider this a "full" refresh 23 | 24 | Optional: change_tree - A descending list, starting with the top-most parent, of all action_data names 25 | leading to the specific entry that was edited (IE. ["parent", "parent", "center_align"]) 26 | """ 27 | raise NotImplementedError("'Refresh' not implemented") 28 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorUtilities/utils.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from PyQt6 import QtGui 3 | from HBEditor.Core.Logger.logger import Logger 4 | 5 | 6 | def ValidateClipboard(source_name: str, data_type: any) -> any: 7 | """ 8 | Validates the data currently held by the clipboard before returning it. 9 | 10 | The data held by the clipboard is required to be structured as: {'source': '', 'data': ''}. 11 | This format ensures the context is respected through checking the 'source' identifier, and ensuring the data is of 12 | the type 'data_type' 13 | """ 14 | data = QtGui.QGuiApplication.clipboard().text() 15 | 16 | if data: 17 | try: 18 | conv_data = yaml.load(data, Loader=yaml.FullLoader) 19 | except Exception as exc: 20 | Logger.getInstance.Log("Failed to paste from clipboard - Data invalid") 21 | return None 22 | 23 | if isinstance(conv_data, dict): 24 | if 'source' not in conv_data or 'data' not in conv_data: 25 | Logger.getInstance.Log("Failed to paste from clipboard - Data invalid") 26 | return None 27 | 28 | elif conv_data['source'] != source_name: 29 | Logger.getInstance.Log("Failed to paste from clipboard - Data invalid for current context") 30 | return None 31 | 32 | elif not isinstance(conv_data['data'], data_type): 33 | Logger.getInstance.Log("Failed to paste from clipboard - Data not of the expected data") 34 | return None 35 | 36 | # Validation complete. Return the data 37 | return conv_data['data'] 38 | -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/FONTLOG.txt: -------------------------------------------------------------------------------- 1 | FONTLOG for the Cabin fonts 2 | 3 | This file provides detailed information on the Cabin Font Software. 4 | This information should be distributed along with the Cabin fonts 5 | and any derivative works. 6 | 7 | Basic Font Information 8 | 9 | Cabin is a Unicode typeface family that supports languages that 10 | use the Latin script and its variants, and could be expanded to support other 11 | scripts. 12 | 13 | More specifically, this release supports the following Unicode ranges: Latin-1 14 | 15 | Documentation can be found at www.impallari.com/cabin 16 | To contribute to the project contact Pablo Impallari at impallari@gmail.com 17 | 18 | ChangeLog 19 | 20 | 28 Dec 2011 (Pablo Impallari & Igino Marini) Cabin v1.006 21 | - Handmade TTF Hinting by Irina Kheyso 22 | 23 | 21 Mar 2011 (Pablo Impallari & Igino Marini) Cabin v1.005 24 | - Added Italics for Regular, Medium, SemiBold & Bold weights. 25 | 26 | 31 Jan 2011 (Pablo Impallari & Igino Marini) Cabin v1.004 27 | - Added Regular, Medium and SemiBold weights. 28 | - Some glyphs refined. 29 | - iKerned by Igino Marini 30 | 31 | 14 Dec 2010 (Pablo Impallari) Cabin v1.003 32 | - Initial release 33 | 34 | Acknowledgements 35 | 36 | If you make modifications be sure to add your name (N), email (E), web-address 37 | (if you have one) (W) and description (D). This list is in alphabetical order. 38 | 39 | N: Pablo Impallari 40 | E: impallari@gmail.com 41 | W: http://www.impallari.com 42 | D: Designer 43 | 44 | N: Igino Marini 45 | E: mail@iginomarini.com 46 | W: http://www.ikern.com 47 | D: Spacing and Kerning 48 | 49 | N: Irina Kheyso 50 | E: irina.kheyso@gmail.com 51 | D: TTF Hinting 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /HBEditor/Core/DataTypes/file_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from enum import Enum 16 | 17 | 18 | class FileType(Enum): 19 | Folder = 1 20 | Project_Settings = 2 21 | Variables = 3 22 | Interface = 4 23 | Dialogue = 5 24 | Scene = 6 25 | Asset_Data = 7 26 | Asset_Image = 8 27 | Asset_Font = 9 28 | Asset_Sound = 10 29 | #Character = 3 30 | 31 | 32 | class FileTypeIcons: 33 | icons = { 34 | FileType.Folder: "EditorContent:Icons/Folder.png", 35 | FileType.Interface: "EditorContent:Icons/Interface.png", 36 | FileType.Project_Settings: "EditorContent:Icons/File.png", 37 | FileType.Dialogue: "EditorContent:Icons/File.png", 38 | FileType.Scene: "EditorContent:Icons/File.png", 39 | FileType.Asset_Data: "EditorContent:Icons/File.png", 40 | FileType.Asset_Image: "EditorContent:Icons/File_Image.png", 41 | FileType.Asset_Font: "EditorContent:Icons/File.png", 42 | FileType.Asset_Sound: "EditorContent:Icons/File.png" 43 | } 44 | -------------------------------------------------------------------------------- /HBEditor/Core/Primitives/toggleable_menu_action.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from PyQt6 import QtWidgets, QtGui 16 | 17 | 18 | class ToggleableAction(QtWidgets.QToolButton): 19 | def __init__(self, text: str, not_toggled_icon: QtGui.QIcon, toggled_icon: QtGui.QIcon = None): 20 | super().__init__() 21 | 22 | self.not_toggled_icon = not_toggled_icon 23 | self.toggled_icon = toggled_icon 24 | if not self.toggled_icon: 25 | self.toggled_icon = self.not_toggled_icon 26 | 27 | self.setText(text) 28 | self.setIcon(self.not_toggled_icon) 29 | 30 | self.setObjectName("toggleable-action") 31 | 32 | def Toggle(self, toggle: bool): 33 | if toggle: 34 | self.setIcon(self.toggled_icon) 35 | self.setProperty("toggleable-action", "toggled") # Apply the match css rule in the active .qss file 36 | self.setStyle(self.style()) 37 | 38 | else: 39 | self.setIcon(self.not_toggled_icon) 40 | self.setProperty("toggleable-action", "") # Clear the property, reverting the CSS back to normal 41 | self.setStyle(self.style()) 42 | -------------------------------------------------------------------------------- /HBEditor/Core/Dialogs/dialog_list.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from PyQt6 import QtWidgets 16 | 17 | 18 | class DialogList(QtWidgets.QDialog): 19 | def __init__(self, title: str, body: str, items: list): 20 | super().__init__() 21 | 22 | self.setWindowTitle(title) 23 | 24 | self.resize(400, 300) 25 | self.main_layout = QtWidgets.QVBoxLayout(self) 26 | 27 | self.description = QtWidgets.QLabel(body) 28 | self.description.setWordWrap(True) 29 | self.main_layout.addWidget(self.description) 30 | 31 | self.list = QtWidgets.QListWidget(self) 32 | self.main_layout.addWidget(self.list) 33 | 34 | self.buttons_layout = QtWidgets.QHBoxLayout(self) 35 | self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) 36 | self.buttons_layout.addWidget(self.button_box) 37 | self.main_layout.addLayout(self.buttons_layout) 38 | self.button_box.accepted.connect(self.accept) 39 | self.button_box.rejected.connect(self.reject) 40 | 41 | for item in items: 42 | self.list.addItem(QtWidgets.QListWidgetItem(item)) 43 | -------------------------------------------------------------------------------- /HBEditor/Core/base_editor.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import os 16 | from HBEditor.Core.Logger.logger import Logger 17 | 18 | 19 | class EditorBase: 20 | def __init__(self, file_path): 21 | self.file_path = file_path 22 | self.editor_ui = None 23 | 24 | Logger.getInstance().Log("Initializing Editor...") 25 | 26 | def GetFileName(self): 27 | """ Returns the name of the file that is being targeted by this editor """ 28 | return os.path.basename(self.file_path) 29 | 30 | def GetFilePath(self): 31 | """ Returns the path of the file that is being targeted by this editor """ 32 | return self.file_path 33 | 34 | def GetUI(self): 35 | """ Returns the UI object for this editor """ 36 | return self.editor_ui 37 | 38 | def Save(self): 39 | """ Write the data held in the editor to the file it points to """ 40 | pass 41 | 42 | def Export(self): 43 | """ 44 | Writes the data held in the editor to a variant of the file it points to in the structure usable by the 45 | engine 46 | """ 47 | pass 48 | 49 | def Import(self): 50 | """ Reads in and loads file this editor points to """ 51 | 52 | -------------------------------------------------------------------------------- /HBEngine/Core/Objects/checkbox.py: -------------------------------------------------------------------------------- 1 | from HBEngine.Core.Objects.renderable import Renderable 2 | from HBEngine.Core.Objects.renderable_sprite import SpriteRenderable 3 | from HBEngine.Core.Objects.interactable import Interactable 4 | from HBEngine.Core import settings 5 | 6 | 7 | class Checkbox(Interactable): 8 | def __init__(self, renderable_data: dict, parent: Renderable = None): 9 | super().__init__(renderable_data, parent) 10 | 11 | self.check_icon_renderable = SpriteRenderable( 12 | renderable_data={ 13 | "key": f"{self.renderable_data['key']}_Text", 14 | "position": [0.5, 0.5], 15 | "sprite": self.renderable_data["sprite_icon"], 16 | "z_order": self.renderable_data["z_order"] + 1 17 | }, 18 | parent=self 19 | ) 20 | self.children.append(self.check_icon_renderable) 21 | self.check_icon_renderable.visible = False 22 | 23 | # Setup Connections 24 | if "connect_project_setting" in self.renderable_data: 25 | if not self.ConnectProjectSetting(self.renderable_data["connect_project_setting"]): 26 | print(f"Unable to setup project setting connection for '{self.key}'. Please review the connection settings") 27 | 28 | def Interact(self): 29 | # If a connection has been established, then the check state is determined by the connected value. If no 30 | # connection is active, then the check state is set by the interaction 31 | if not self.connected: 32 | if self.check_icon_renderable.visible: 33 | self.check_icon_renderable.visible = False 34 | else: 35 | self.check_icon_renderable.visible = True 36 | 37 | super().Interact() 38 | settings.scene.Draw() # Draw to apply the icon changes 39 | 40 | def ConnectionUpdate(self, new_value): 41 | if isinstance(new_value, bool): 42 | self.check_icon_renderable.visible = new_value 43 | else: 44 | raise ValueError(f"Connection value is an invalid type. Received '{type(new_value)}' when expecting 'bool'") -------------------------------------------------------------------------------- /Tools/HBYaml/hb_yaml.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import yaml 16 | from Tools.HBYaml.CustomTags import connection 17 | 18 | 19 | class Reader: 20 | @staticmethod 21 | def ReadAll(file_path: str): 22 | """ 23 | Given a file path, read in the contents of the file and return them 24 | """ 25 | loader = yaml.FullLoader 26 | 27 | # Add custom constructors that may be needed 28 | loader.add_constructor("!Connection", connection.connection_constructor) 29 | 30 | with open(file_path) as f: 31 | return yaml.load(f, Loader=loader) 32 | 33 | 34 | class Writer: 35 | @staticmethod 36 | def WriteFile(data: dict, file_path: str, metadata: str = ""): 37 | """ 38 | Given a dict, write it to the provided file path. If 'metadata' is provided, prepend that to the beginning 39 | of the file (Useful for comments) 40 | """ 41 | with open(file_path, 'w') as file: 42 | if metadata: 43 | file.write(metadata + "\n\n") 44 | 45 | dumper = yaml.SafeDumper 46 | 47 | # Add custom representors that may be needed 48 | dumper.add_representer(connection.Connection, connection.connection_representer) 49 | 50 | # By default, yaml dumps data in a 'sorted order' instead of by 'insertion order'. As per this: 51 | # https://github.com/yaml/pyyaml/issues/110, you can specify 'sort_keys=False' to force the dump to 52 | # skip the sorting and use insertion order 53 | return yaml.dump(data, file, sort_keys=False, Dumper=dumper) 54 | 55 | -------------------------------------------------------------------------------- /HBEditor/Core/engine_launcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import subprocess 16 | import os 17 | from PyQt6.QtWidgets import QMessageBox 18 | from HBEditor.Core.Logger.logger import Logger 19 | 20 | 21 | class EngineLauncher: 22 | """ 23 | A manager for operations relating to launch the HBEngine. This class requires that the HBEngine be available 24 | alongside the HBEditor so the subprocess call works correctly 25 | """ 26 | 27 | @staticmethod 28 | def Play(parent, project_path, engine_parent_root): 29 | Logger.getInstance().Log("Launching engine...") 30 | try: 31 | # Launch the engine, and wait until it shuts down before continuing 32 | result = subprocess.Popen( 33 | [ 34 | f"venv/Scripts/python", 35 | "HBEngine/hb_engine.py", 36 | "-p", 37 | project_path 38 | ], 39 | stdout=True, 40 | stderr=True 41 | ) 42 | Logger.getInstance().Log("Engine Launched - Editor temporarily unavailable") 43 | result.wait() 44 | 45 | Logger.getInstance().Log("Engine closed - Editor functionality resumed") 46 | 47 | except Exception as exc: 48 | QMessageBox.about( 49 | parent, 50 | "Unable to Launch Engine", 51 | "The HBEngine could not be launched.\n\n" 52 | "Please make sure the engine is available alongside the editor, or that you're\n" 53 | "PYTHONPATH is configured correctly to include the engine." 54 | ) 55 | print(exc) 56 | 57 | -------------------------------------------------------------------------------- /HBEngine/Core/Objects/renderable_sprite.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import pygame 16 | from HBEngine.Core import settings 17 | from HBEngine.Core.Objects.renderable import Renderable 18 | 19 | 20 | class SpriteRenderable(Renderable): 21 | """ 22 | The Sprite Renderable class is the base class for renderable sprite elements in the HBEngine. This includes: 23 | - Interactables 24 | - Non-Interactables 25 | - Backgrounds 26 | - etc 27 | """ 28 | def __init__(self, renderable_data: dict, initial_rescale: bool = True, parent: Renderable = None, 29 | use_placeholder: bool = True): 30 | super().__init__(renderable_data, parent) 31 | 32 | if "sprite" in self.renderable_data: 33 | if self.renderable_data['sprite'] != "None" and self.renderable_data['sprite'] != "": 34 | sprite = settings.ConvertPartialToAbsolutePath(self.renderable_data['sprite']) 35 | 36 | try: 37 | self.surface = pygame.image.load(sprite).convert_alpha() 38 | self.rect = self.surface.get_rect() 39 | except Exception as exc: 40 | raise ValueError(f"Failed to load sprite: '{sprite}' - Either the file was not found, or it is not a " 41 | f"supported file type\n Exception: {exc}") from None # PEP 409: Suppressing exception context 42 | 43 | # For new objects, resize initially in case we're already using a scaled resolution. Allow descendents 44 | # to defer this though if they need to do any additional work beforehand 45 | if initial_rescale: 46 | self.RecalculateSize(settings.resolution_multiplier) 47 | 48 | 49 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorUtilities/image.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | from PIL import Image, UnidentifiedImageError 4 | from HBEditor.Core import settings 5 | from HBEditor.Core.Logger.logger import Logger 6 | 7 | 8 | def GenerateThumbnail(path: str) -> str: 9 | """ 10 | Given a relative file path to an image with the Content folder as the root, generate a thumbnail image for it 11 | and save it to /Thumbnails. Return the path for the thumbnail if successfully created. Otherwise, return an empty 12 | string 13 | """ 14 | # Full path to the source image 15 | full_image_path = f"{settings.user_project_dir}/{path}" 16 | 17 | # Full path to the thumbnail (Remove the root folder for 'path' as we need the thumbnail folder to be the root) 18 | thumbnail_image_path = f"{settings.user_project_dir}/Thumbnails/{'/'.join(path.split('/')[1:])}" 19 | pathlib.Path(os.path.dirname(thumbnail_image_path)).mkdir(parents=True, exist_ok=True) 20 | 21 | try: 22 | # Open the original file, and overwrite it with a generated, smaller copy. Use (64, 64) to match the size 23 | # of the editor icons 24 | img = Image.open(full_image_path) 25 | img.thumbnail((64, 64)) 26 | bg = Image.new('RGB', (64, 64), (0, 0, 0)) 27 | 28 | # Paste the image onto the center of the background 29 | bg.paste(img, (round(bg.width / 2 - img.width / 2), round(bg.height / 2 - img.height / 2))) 30 | 31 | # Write the changes back to the copy on disc 32 | img = bg 33 | img.save(thumbnail_image_path) 34 | 35 | return thumbnail_image_path 36 | except UnidentifiedImageError: 37 | Logger.getInstance().Log( 38 | f"Unable to generate thumbnail for '{full_image_path}' as it does not appear to be an image") 39 | 40 | return "" 41 | 42 | 43 | def GetThumbnail(path: str) -> str: 44 | """ 45 | Given a relative file path to an image with the Content folder as the root, check if there is a matching 46 | thumbnail in the thumbnails directory. Returns the thumbnail file path if found. Otherwise, returns an empty string 47 | """ 48 | # Full path to the thumbnail (Remove the root folder for 'path' as we need the thumbnail folder to be the root) 49 | thumbnail_image_path = f"{settings.user_project_dir}/Thumbnails/{'/'.join(path.split('/')[1:])}" 50 | if os.path.exists(thumbnail_image_path): 51 | return thumbnail_image_path 52 | else: 53 | return "" 54 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorProjectSettings/editor_project_settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from HBEditor.Core import settings 16 | from HBEditor.Core.Logger.logger import Logger 17 | from HBEditor.Core.base_editor import EditorBase 18 | from HBEditor.Core.EditorProjectSettings.editor_project_settings_ui import EditorProjectSettingsUI 19 | from HBEditor.Core.DataTypes.file_types import FileType 20 | from HBEditor.Core.EditorUtilities import path 21 | from Tools.HBYaml.hb_yaml import Reader, Writer 22 | 23 | 24 | class EditorProjectSettings(EditorBase): 25 | def __init__(self, file_path): 26 | super().__init__(file_path) 27 | 28 | # Read this data in first as the U.I will need it to initialize properly 29 | self.project_settings = Reader.ReadAll(self.file_path) 30 | 31 | self.editor_ui = EditorProjectSettingsUI(self) 32 | Logger.getInstance().Log("Editor initialized") 33 | 34 | def Export(self): 35 | super().Export() 36 | Logger.getInstance().Log(f"Exporting Project Settings") 37 | 38 | # Just in case the user has made any changes to the settings, save them 39 | self.editor_ui.UpdateProjectSettingsData() 40 | 41 | # Write the data out 42 | Logger.getInstance().Log("Writing data to file...") 43 | try: 44 | Writer.WriteFile( 45 | self.project_settings, 46 | self.file_path, 47 | f"# {settings.editor_data['EditorSettings']['version_string']}" 48 | ) 49 | self.editor_ui.SIG_USER_SAVE.emit() 50 | Logger.getInstance().Log("File Exported!", 2) 51 | except: 52 | Logger.getInstance().Log("Failed to Export!", 4) 53 | 54 | # Reload the project settings 55 | settings.LoadProjectSettings() 56 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorUtilities/path.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from pathlib import Path 16 | from HBEditor.Core.Logger.logger import Logger 17 | from HBEditor.Core import settings 18 | 19 | 20 | # Default files that live in the engine content directory and are available to all projects 21 | DEFAULT_FONT = "Content/Fonts/Comfortaa/Comfortaa-Regular.ttf" 22 | 23 | 24 | def ConvertPartialToAbsolutePath(partial_path): 25 | """ Given a partial path, return an absolute path using the editor's root """ 26 | return f"{settings.editor_root}/{partial_path}" 27 | 28 | 29 | def ResolveFilePath(path: str, fallback_path: str = ""): 30 | """ 31 | Given a relative file path, return an absolute variant. If 'fallback_path' is provided, load it instead if 32 | 'path' can not be found or is null 33 | """ 34 | # If the path (assumably) points to the engine, check if the path exists within the engine root 35 | if path.startswith("HBEngine"): 36 | full_eng_path = f"{settings.engine_root}{path[len('HBEngine'):]}" 37 | if Path(full_eng_path).exists(): 38 | return full_eng_path 39 | 40 | # The path is likely relative to the user's project directory 41 | else: 42 | if Path(f"{settings.user_project_dir}/{path}").exists(): 43 | return f"{settings.user_project_dir}/{path}" 44 | else: 45 | if fallback_path: 46 | Logger.getInstance().Log(f"File does not Exist: '{path}' - Loading default file: '{fallback_path}'", 3) 47 | return f"{settings.engine_root}/{fallback_path}" 48 | else: 49 | Logger.getInstance().Log(f"File does not Exist: '{path}'", 3) 50 | 51 | 52 | def ResolveFontFilePath(path: str): 53 | """ Given a relative font path, return an absolute variant """ 54 | return ResolveFilePath(path, DEFAULT_FONT) 55 | -------------------------------------------------------------------------------- /HBEditor/Core/Primitives/simple_checkbox.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from PyQt6 import QtWidgets, QtCore 16 | 17 | 18 | class SimpleCheckbox(QtWidgets.QWidget): 19 | """ 20 | A custom wrapper for the QCheckbox class when provides a textless, centered checkbox 21 | """ 22 | SIG_USER_UPDATE = QtCore.pyqtSignal(object, bool) 23 | 24 | def __init__(self): 25 | super().__init__() 26 | self.owner = None 27 | 28 | # For some unholy reason, the QCheckbox widget does not support center alignment natively. To make matters 29 | # worse, the text is considered in the size when used in layouts regardless if text is actually specified 30 | 31 | # This can be bypassed by surrounding the QCheckbox in spacers that force it to the center of the layout 32 | self.main_layout = QtWidgets.QHBoxLayout(self) 33 | self.main_layout.setSpacing(0) 34 | self.main_layout.setContentsMargins(0, 0, 0, 0) 35 | 36 | self.left_spacer = QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Policy.Expanding) 37 | self.right_spacer = QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Policy.Expanding) 38 | 39 | self.checkbox = QtWidgets.QCheckBox(self) 40 | self.checkbox.setText("") 41 | 42 | self.main_layout.addItem(self.left_spacer) 43 | self.main_layout.addWidget(self.checkbox) 44 | self.main_layout.addItem(self.right_spacer) 45 | 46 | def Get(self) -> bool: 47 | """ Returns whether the checkbox is checked """ 48 | return self.checkbox.isChecked() 49 | 50 | def Set(self, value: bool) -> None: 51 | self.checkbox.setChecked(value) 52 | 53 | def Connect(self): 54 | #@TODO: Investigate moving this to init 55 | self.checkbox.stateChanged.connect(lambda update: self.SIG_USER_UPDATE.emit(self.owner, self.Get())) 56 | 57 | def Disconnect(self): 58 | self.checkbox.disconnect() 59 | -------------------------------------------------------------------------------- /HBEngine/Content/Interfaces/dialogue_01.interface: -------------------------------------------------------------------------------- 1 | # Created by Cronza 2 | type: Interface 3 | settings: 4 | description: '' 5 | key: "!&INTERFACE_DIALOGUE&!" 6 | pages: 7 | Persistent: 8 | description: '' 9 | items: 10 | # Dialogue Frame 11 | - create_sprite: 12 | key: "!&INTERFACE_DIALOGUE_Frame&!" 13 | sprite: HBEngine/Content/Sprites/Interface/Dialogue/Dialogue_Frame.png 14 | position: 15 | - 0.5 16 | - 0.8 17 | center_align: true 18 | z_order: 10000 19 | conditions: { } 20 | 21 | # Main Menu Button 22 | - create_text_button: 23 | key: "!&INTERFACE_DIALOGUE_Main_Menu_Button&!" 24 | position: 25 | - 0.3 26 | - 0.96 27 | center_align: true 28 | z_order: 10001 29 | text: Menu 30 | text_size: 16 31 | text_color_hover: 32 | - 92 33 | - 176 34 | - 255 35 | text_color_clicked: 36 | - 255 37 | - 111 38 | - 111 39 | wrap_bounds: 40 | - 0.06 41 | - 0.05 42 | events: 43 | event_0: 44 | action: 45 | action: load_scene 46 | scene_file: '' 47 | conditions: { } 48 | conditions: { } 49 | 50 | # Quick Save Button 51 | - create_text_button: 52 | key: "!&INTERFACE_DIALOGUE_Quick_Save_Button&!" 53 | position: 54 | - 0.375 55 | - 0.96 56 | center_align: true 57 | z_order: 10001 58 | text: Q. Save 59 | text_size: 16 60 | text_color_hover: 61 | - 92 62 | - 176 63 | - 255 64 | text_color_clicked: 65 | - 255 66 | - 111 67 | - 111 68 | wrap_bounds: 69 | - 0.2 70 | - 0.2 71 | events: { } 72 | conditions: { } 73 | 74 | # Quick Load Button 75 | - create_text_button: 76 | key: "!&INTERFACE_DIALOGUE_Quick_Load_Button&!" 77 | position: 78 | - 0.45 79 | - 0.96 80 | center_align: true 81 | z_order: 10001 82 | text: Q. Load 83 | text_size: 16 84 | text_color_hover: 85 | - 92 86 | - 176 87 | - 255 88 | text_color_clicked: 89 | - 255 90 | - 111 91 | - 111 92 | wrap_bounds: 93 | - 0.2 94 | - 0.2 95 | events: { } 96 | conditions: { } 97 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorInterface/dialog_new_interface.py: -------------------------------------------------------------------------------- 1 | from HBEditor.Core.Dialogs.dialog_new_file_from_template import DialogNewFileFromTemplate, FileOption 2 | from HBEditor.Core.DataTypes.file_types import FileType 3 | 4 | """ Available templates for users to clone from when creating new interfaces """ 5 | REGISTERED_TEMPLATES = [ 6 | # Main Menu 7 | { 8 | "display_name": "Main Menu 01", 9 | "description": "A simple main menu U.I with centered icons, and a project title card which is connected to the 'title' project setting.", 10 | "template_path": "HBEngine/Content/Interfaces/main_menu_01.interface", 11 | "preview_image": "EditorContent:Images/Thumbnails/Template_Interface_Main_Menu.jpg" 12 | }, 13 | # Pause Menu 14 | { 15 | "display_name": "Pause Menu 01", 16 | "description": "A simple pause menu that splits the screen into two sections:\n\n- The left side is persistent and contains the main buttons\n- The right side is populated as you click certain buttons, such as 'Options' and 'Save'", 17 | "template_path": "HBEngine/Content/Interfaces/pause_menu_01.interface", 18 | "preview_image": "EditorContent:Images/Thumbnails/Template_Interface_Pause_Menu.jpg" 19 | }, 20 | # Dialogue 21 | { 22 | "display_name": "Dialogue 01", 23 | "description": "A simple dialogue interface resembling ones used often in Western Visual Novels. Contains a Save, Load and Menu button along with a Speaker and Dialogue text frame", 24 | "template_path": "HBEngine/Content/Interfaces/dialogue_01.interface", 25 | "preview_image": "EditorContent:Images/Thumbnails/Template_Interface_Dialogue.jpg" 26 | } 27 | ] 28 | 29 | 30 | class DialogNewInterface(DialogNewFileFromTemplate): 31 | def __init__(self): 32 | super().__init__("EditorContent:Icons/Interface.png") 33 | 34 | FileOption( 35 | "None", 36 | "Create a blank interface. Perfect if you want to start from scratch.", 37 | "", 38 | "EditorContent:Images/Thumbnails/Template_Blank.jpg", 39 | self.options_list 40 | ) 41 | 42 | for item in REGISTERED_TEMPLATES: 43 | FileOption( 44 | item["display_name"], 45 | item["description"], 46 | item["template_path"], 47 | item["preview_image"], 48 | self.options_list 49 | ) 50 | 51 | # Weird fix: For some reason, QListWidgets don't "select" the first entry by default despite it being 52 | # considered the "currentitem". This makes for a visual bug, so let's forcefully select it 53 | self.options_list.setCurrentRow(0) 54 | -------------------------------------------------------------------------------- /HBEditor/Core/ActionMenu/action_menu.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from PyQt6 import QtWidgets, QtGui, QtCore 16 | from HBEditor.Core import settings 17 | from HBEditor.Core.DataTypes.file_types import FileType 18 | 19 | 20 | class ActionMenu(QtWidgets.QMenu): 21 | """ 22 | A generic menu of categories and actions that the active editor can use. 'button' func is used when any menu option 23 | is clicked. 24 | """ 25 | def __init__(self, button_func, editor_type: FileType): 26 | super().__init__() 27 | 28 | self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowType.NoDropShadowWindowHint) 29 | 30 | for category, data in settings.available_actions.items(): 31 | # Create the category submenu 32 | cat_menu = QtWidgets.QMenu(self) 33 | cat_menu.setTitle(category) 34 | cat_menu.setIcon(QtGui.QIcon(data["icon"])) 35 | 36 | # Populate the category submenu 37 | for option_data in data['options']: 38 | for applicable_editor in option_data["applicable_editors"]: 39 | if FileType[applicable_editor] == editor_type: 40 | option = ActionMenuOption(self, option_data['name'], option_data['display_name']) 41 | option.SIG_USER_CLICKED.connect(button_func) 42 | cat_menu.addAction(option) 43 | break 44 | 45 | # Only use the menu if there were any actions that are allowed for this editor type 46 | if len(cat_menu.actions()) > 0: 47 | self.addMenu(cat_menu) 48 | 49 | 50 | class ActionMenuOption(QtWidgets.QWidgetAction): 51 | SIG_USER_CLICKED = QtCore.pyqtSignal(str) 52 | 53 | def __init__(self, parent, action_name: str, display_name: str): 54 | super().__init__(parent) 55 | 56 | self.action_name = action_name # The real action name, separate from the user-facing display name 57 | self.setText(display_name) 58 | self.triggered.connect(lambda: self.SIG_USER_CLICKED.emit(self.action_name)) 59 | -------------------------------------------------------------------------------- /HBEditor/Core/Logger/logger_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from PyQt6 import QtWidgets, QtGui, QtCore 16 | from HBEditor.Core import settings 17 | 18 | 19 | class LoggerUI(QtWidgets.QWidget): 20 | 21 | def __init__(self, l_core): 22 | super().__init__() 23 | 24 | self.l_core = l_core 25 | 26 | self.setObjectName("vertical") 27 | 28 | # Main Layout 29 | self.main_layout = QtWidgets.QVBoxLayout(self) 30 | self.main_layout.setContentsMargins(0, 0, 0, 0) 31 | self.main_layout.setSpacing(0) 32 | 33 | # Toolbar 34 | self.logger_toolbar = QtWidgets.QToolBar(self) 35 | self.logger_toolbar.setObjectName("vertical") 36 | 37 | # Clear Log Button 38 | self.logger_toolbar.addAction( 39 | QtGui.QIcon(QtGui.QPixmap("EditorContent:Icons/Trash.png")), 40 | "Clear Log", 41 | self.l_core.ClearLog 42 | ) 43 | 44 | # Logger data list 45 | self.log_list = QtWidgets.QListWidget(self) 46 | self.log_list.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) 47 | self.log_list.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.SelectedClicked) 48 | self.log_list.setAutoScroll(True) 49 | 50 | # Add everything to the main container 51 | self.main_layout.addWidget(self.logger_toolbar) 52 | self.main_layout.addWidget(self.log_list) 53 | 54 | def AddEntry(self, text, style): 55 | new_entry = QtWidgets.QListWidgetItem() 56 | entry_text = QtWidgets.QLabel(text) # Use a QLabel so we can apply css styling per-entry 57 | entry_text.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextSelectableByMouse) 58 | entry_text.setProperty("model-entry", style) # Apply the match css rule in the active .qss file 59 | 60 | self.log_list.addItem(new_entry) 61 | self.log_list.setItemWidget(new_entry, entry_text) 62 | 63 | # Since Qt only refreshes widgets when it regains control of the main thread, force the update here 64 | # as long updates are high priority in terms of visibility 65 | self.log_list.repaint() 66 | self.repaint() 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */__pycache__# Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # User-specific stuff 141 | .idea/ 142 | HBEngine/.idea/ 143 | HBEditor/.idea/ 144 | HBEngine/venv/ 145 | HBEditor/venv/ 146 | HBEditor/Temp/ -------------------------------------------------------------------------------- /Tools/HBBuilder/hb_builder.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import subprocess 16 | import os 17 | import shutil 18 | import time 19 | 20 | 21 | class HBBuilder: 22 | @staticmethod 23 | def Build(logger, engine_dir: str, project_dir: str, project_name: str): 24 | """ Generate an executable based on the provided information and project """ 25 | logger.Log(f"*** Starting build for: '{project_dir}'... ***") 26 | build_dir = f"{project_dir}/build" 27 | working_dir = f"{build_dir}/intermediate" 28 | output_dir = f"{build_dir}/output" 29 | 30 | # Remove the build folder if it exists, just in case the state changed significantly 31 | HBBuilder.Clean(logger, build_dir) 32 | 33 | # Use a subprocess call to invoke PyInstaller so it can fail independently 34 | args = f"venv/Scripts/pyinstaller.exe " \ 35 | "--noconsole " \ 36 | f"--workpath \"{working_dir}\" "\ 37 | f"--distpath \"{output_dir}\" "\ 38 | f"--specpath \"{working_dir}\" "\ 39 | f"--add-data \"{project_dir}/Content;Content\" "\ 40 | f"--add-data \"{project_dir}/Config;Config\" "\ 41 | f"--add-data \"{engine_dir}/Content;HBEngine/Content\" "\ 42 | f"--name \"{project_name}\" "\ 43 | f"HBEngine/hb_engine.py" 44 | 45 | logger.Log(f"Generating executable...") 46 | result = subprocess.Popen( 47 | args, 48 | stdout=True, 49 | stderr=True 50 | ) 51 | 52 | # Wait for the build to finish, then collect and report the result 53 | result_code = result.wait() 54 | if result_code == 0: 55 | logger.Log("*** BUILD SUCCESS ***", 2) 56 | else: 57 | logger.Log("*** BUILD FAILED ***", 4) 58 | 59 | @staticmethod 60 | def Clean(logger, project_dir: str): 61 | """ Deletes the active project's build directory """ 62 | build_dir = f"{project_dir}/build" 63 | if os.path.exists(build_dir): 64 | logger.Log(f"Build folder exists - Cleaning...", 3) 65 | try: 66 | shutil.rmtree(build_dir) 67 | logger.Log(f"Build folder deleted!", 2) 68 | except Exception as exc: 69 | logger.Log(f"Failed to delete the build folder: {exc}", 4) 70 | else: 71 | logger.Log(f"Build folder does not exist - Skipping the clean", 3) 72 | -------------------------------------------------------------------------------- /HBEngine/Core/Objects/interface.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import pygame 16 | from HBEngine.Core import settings, action_manager 17 | from HBEngine.Core.Objects.renderable import Renderable 18 | from Tools.HBYaml.hb_yaml import Reader 19 | 20 | 21 | class Interface(Renderable): 22 | def __init__(self, renderable_data: dict, parent: Renderable = None): 23 | # Page renderables are independent of the persistent renderables, and can be created and removed at runtime. In 24 | # order to faciliate quick removal, keep a list of page renderables as they're created 25 | self.page_renderables = [] 26 | 27 | self.visible = False 28 | if "key" not in renderable_data: renderable_data["key"] = id(self) #@TODO: Dialogue files don't have a key 29 | if "z_order" not in renderable_data: renderable_data["z_order"] = 10000 30 | super().__init__(renderable_data, parent) 31 | 32 | if "Persistent" in renderable_data["pages"]: 33 | for item in renderable_data["pages"]["Persistent"]["items"]: 34 | action_name, action_data = next(iter(item.items())) 35 | action_manager.PerformAction(action_data=action_data, action_name=action_name, parent=self, no_draw=True) 36 | else: 37 | raise ValueError("'Persistent' missing from the interface file - No items to display!") 38 | 39 | def LoadPage(self, page_name: str) -> bool: 40 | """ 41 | Unload the prior page if applicable, and load the provided page. Only one page is supported at a time. 42 | Returns whether the load succeeded 43 | """ 44 | if "pages" in self.renderable_data: 45 | if page_name in self.renderable_data["pages"]: 46 | if self.page_renderables: 47 | self.RemovePage() 48 | 49 | # Create and record new page renderables 50 | for page_action in self.renderable_data["pages"][page_name]["items"]: 51 | action_name, action_data = next(iter(page_action.items())) 52 | renderable = action_manager.PerformAction(action_data=action_data, action_name=action_name, parent=self, no_draw=True) 53 | self.page_renderables.append(renderable) 54 | return True 55 | 56 | return False 57 | 58 | def RemovePage(self): 59 | """ Removes all active page renderables """ 60 | # Wipe existing page renderables if there are any 61 | for renderable in self.page_renderables: 62 | self.children.remove(renderable) 63 | self.page_renderables.clear() 64 | -------------------------------------------------------------------------------- /HBEngine/Core/Objects/renderable_group.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | 16 | 17 | class RenderableGroup: 18 | def __init__(self): 19 | """ 20 | This class mimics the base pygame sprite group class, but uses a dictionary for the renderable list. 21 | The 'key' value in each renderable is used as the dictionary key 22 | """ 23 | self.renderables = {} 24 | 25 | super().__init__() 26 | 27 | def Add(self, *r_to_add): 28 | """ 29 | Add the given renderables to the rend dict using the renderable key as the key, and the actual 30 | renderable as the value 31 | """ 32 | for renderable in r_to_add: 33 | if renderable.key is None: 34 | print(f"Renderable has no key assigned - Removal will be impossible: {renderable}") 35 | self.renderables[renderable.key] = renderable 36 | 37 | def Remove(self, *key_to_remove) -> bool: 38 | """ 39 | Remove the given renderable key from the dict. Returns whether the key was successfully removed. Returns False 40 | if the key was not found 41 | """ 42 | for key in key_to_remove: 43 | try: 44 | del self.renderables[key] 45 | return True 46 | except KeyError as exc: 47 | print(f"Key not found: {exc}") 48 | except Exception as rexc: 49 | print(f"Unknown error while removing: {rexc}") 50 | 51 | return False 52 | 53 | def Clear(self): 54 | self.renderables.clear() 55 | 56 | def Exists(self, key) -> bool: 57 | """ Returns a boolean for whether the provided key exists in the renderables list """ 58 | return key in self.renderables 59 | 60 | def Get(self) -> list: 61 | """ Returns the list of renderables inside this group""" 62 | return list(self.renderables.values()) 63 | 64 | def GetFromKey(self, key: str): 65 | """ Returns the renderable that matches the given key. Returns 'None' if there is no matching renderable """ 66 | if key in self.renderables: 67 | return self.renderables[key] 68 | else: 69 | return None 70 | 71 | def Update(self, target: list = None): 72 | if not target: 73 | target = list(self.renderables.values()) 74 | #@TODO: Is there a safer way of parsing these lists that avoid issues with size changing? 75 | for renderable in target: 76 | renderable.update() 77 | if renderable.children: 78 | self.Update(renderable.children) 79 | -------------------------------------------------------------------------------- /HBEditor/Core/Logger/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from datetime import datetime 16 | from HBEditor.Core import settings 17 | from PyQt6.QtWidgets import QListWidgetItem 18 | from PyQt6.QtGui import QColor, QFont 19 | from HBEditor.Core.Logger.logger_ui import LoggerUI 20 | from HBEditor.Core.DataTypes.log_types import LogType 21 | 22 | 23 | class Logger: 24 | __instance = None 25 | 26 | @staticmethod 27 | def getInstance(): 28 | """ 29 | Static access method - Used to acquire the singleton instance, or instantiate it if it doesn't already exist 30 | """ 31 | if Logger.__instance is None: 32 | Logger() 33 | return Logger.__instance 34 | 35 | def __init__(self): 36 | # Enforce the use of the singleton instance 37 | if Logger.__instance is not None: 38 | raise Exception("This class is a singleton!") 39 | else: 40 | Logger.__instance = self 41 | 42 | # Build the Logger UI 43 | self.log_ui = LoggerUI(self) 44 | 45 | # Values refer to QProperties defined in the active .qss file 46 | self.log_styles = { 47 | LogType.Normal: "text-normal", 48 | LogType.Success: "text-success", 49 | LogType.Warning: "text-warning", 50 | LogType.Error: "text-error" 51 | } 52 | 53 | self.log_prefixes = { 54 | LogType.Normal: "", 55 | LogType.Success: "Success: ", 56 | LogType.Warning: "Warning: ", 57 | LogType.Error: "Error: " 58 | } 59 | 60 | self.Log("Logger Initialized!") 61 | 62 | def Log(self, log_text, log_type=LogType.Normal): 63 | prefix = "" 64 | 65 | # Since the expectation is that the user provides ints that are converted to the respective log type, this 66 | # can lead to an invalid log type. If this ever happens, just default to the normal style 67 | style = self.log_styles[LogType.Normal] 68 | prefix = self.log_styles[LogType.Normal] 69 | try: 70 | converted_type = LogType(log_type) 71 | style = self.log_styles[converted_type] 72 | prefix = self.log_prefixes[converted_type] 73 | except Exception as exc: 74 | print(f"Failed to determine log color type - Resorting to normal colors: {exc}") 75 | 76 | log_str = datetime.now().strftime("%H:%M:%S") + ": " + prefix + log_text 77 | self.log_ui.AddEntry(log_str, style) 78 | print(log_str) 79 | 80 | def ClearLog(self): 81 | """ Deletes all log entries """ 82 | self.log_ui.log_list.clear() 83 | 84 | def GetUI(self): 85 | """ Returns a reference to the logger U.I """ 86 | return self.log_ui 87 | -------------------------------------------------------------------------------- /HBEngine/Core/Objects/renderable_container.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from HBEngine.Core.Objects.renderable import Renderable 16 | 17 | 18 | class Container(Renderable): 19 | def __init__(self, scene, renderable_data): 20 | super().__init__(scene, renderable_data) 21 | self.visible = False 22 | 23 | #@TODO: Minimize the specific references to objects here so that containers can be more generalized 24 | # Load any specified objects (Non-interactables) 25 | if 'sprites' in renderable_data: 26 | f_objects = renderable_data['sprites'] 27 | 28 | for f_object in f_objects: 29 | self.children.append(self.scene.a_manager.PerformAction(f_object, 'create_sprite'), no_draw=True) 30 | else: 31 | print('Data file does not specify any sprites') 32 | 33 | # Load any specified interactables 34 | if 'interactables' in renderable_data: 35 | f_interactables = renderable_data['interactables'] 36 | 37 | for f_interactable in f_interactables: 38 | self.children.append(self.scene.a_manager.PerformAction(f_interactable, 'create_interactable'), no_draw=True) 39 | else: 40 | print('Data file does not specify any interactables') 41 | 42 | # Load any specified buttons 43 | if 'buttons' in renderable_data: 44 | f_buttons = renderable_data['buttons'] 45 | 46 | for f_button in f_buttons: 47 | self.children.append(self.scene.a_manager.PerformAction(f_button, 'create_button'), no_draw=True) 48 | else: 49 | print('Data file does not specify any buttons') 50 | 51 | # Load any specified text 52 | if 'text' in renderable_data: 53 | f_texts = renderable_data['text'] 54 | 55 | for f_text in f_texts: 56 | self.children.append(self.scene.a_manager.PerformAction(f_text, 'create_text'), no_draw=True) 57 | else: 58 | print('Data file does not specify any text') 59 | 60 | def GetAllChildren(self) -> list: 61 | """ Recursively collect all children to this container """ 62 | f_children = [] 63 | return self.GetChild(self, f_children) 64 | 65 | def GetChild(self, parent, f_children): 66 | """ Given a renderable, recursively collect itself, and any children it has, and return them as a list """ 67 | 68 | # If this object has children, loop through them. Collect a reference to the child, then recurse for it's 69 | # children 70 | if parent.children: 71 | for child in parent.children: 72 | f_children.append(child) # Add the child 73 | self.GetChild(child, f_children) # Recurse for this child's children 74 | 75 | # Return the list so the caller can continue 76 | return f_children 77 | else: 78 | return parent 79 | -------------------------------------------------------------------------------- /HBEditor/Config/Actions.yaml: -------------------------------------------------------------------------------- 1 | Renderables: 2 | icon: "EditorContent:Icons/Renderable.png" 3 | options: 4 | - name: "create_sprite" 5 | display_name: "Create Sprite" 6 | applicable_editors: 7 | - Dialogue 8 | - Scene 9 | - Interface 10 | - name: "create_background" 11 | display_name: "Create Background" 12 | applicable_editors: 13 | - Dialogue 14 | - Scene 15 | - Interface 16 | - name: "create_text" 17 | display_name: "Create Text" 18 | applicable_editors: 19 | - Dialogue 20 | - Scene 21 | - Interface 22 | - name: "remove_renderable" 23 | display_name: "Remove Renderable" 24 | applicable_editors: 25 | - Dialogue 26 | 27 | Interactables: 28 | icon: "EditorContent:Icons/Renderable.png" 29 | options: 30 | - name: "create_interactable" 31 | display_name: "Create Interactable" 32 | applicable_editors: 33 | - Scene 34 | - Interface 35 | - name: "create_button" 36 | display_name: "Create Button" 37 | applicable_editors: 38 | - Scene 39 | - Interface 40 | - name: "create_text_button" 41 | display_name: "Create Button (Text Only)" 42 | applicable_editors: 43 | - Scene 44 | - Interface 45 | 46 | Dialogue: 47 | icon: "EditorContent:Icons/Dialogue.png" 48 | options: 49 | - name: "dialogue" 50 | display_name: "Dialogue" 51 | applicable_editors: 52 | - Dialogue 53 | - name: "choice" 54 | display_name: "Choice" 55 | applicable_editors: 56 | - Dialogue 57 | 58 | Scene: 59 | icon: "EditorContent:Icons/Scene.png" 60 | options: 61 | - name: "load_scene" 62 | display_name: "Load Scene" 63 | applicable_editors: 64 | - Dialogue 65 | - name: "scene_fade_in" 66 | display_name: "Fade In" 67 | applicable_editors: 68 | - Dialogue 69 | - name: "scene_fade_out" 70 | display_name: "Fade Out" 71 | applicable_editors: 72 | - Dialogue 73 | 74 | Sound: 75 | icon: "EditorContent:Icons/Dialogue.png" 76 | options: 77 | - name: "play_sfx" 78 | display_name: "Play SFX" 79 | applicable_editors: 80 | - Dialogue 81 | - name: "play_music" 82 | display_name: "Play Music" 83 | applicable_editors: 84 | - Dialogue 85 | - name: "stop_sfx" 86 | display_name: "Stop SFX" 87 | applicable_editors: 88 | - Dialogue 89 | - name: "stop_music" 90 | display_name: "Stop Music" 91 | applicable_editors: 92 | - Dialogue 93 | - name: "set_mute" 94 | display_name: "Set Mute" 95 | applicable_editors: 96 | - Dialogue 97 | Interface: 98 | icon: "EditorContent:Icons/Interface.png" 99 | options: 100 | - name: "load_interface" 101 | display_name: "Load Interface" 102 | applicable_editors: 103 | - Dialogue 104 | - name: "unload_interface" 105 | display_name: "Unload Interface" 106 | applicable_editors: 107 | - Dialogue 108 | Utils: 109 | icon: "EditorContent:Icons/Dialogue.png" 110 | options: 111 | - name: "set_value" 112 | display_name: "Set Value" 113 | applicable_editors: 114 | - Dialogue 115 | - name: "conditional" 116 | display_name: "Conditional" 117 | applicable_editors: 118 | - Scene 119 | - Interface 120 | - name: "conditional_dialogue" 121 | display_name: "Conditional" 122 | applicable_editors: 123 | - Dialogue -------------------------------------------------------------------------------- /HBEngine/Core/Objects/button.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from HBEngine.Core.DataTypes.input_states import State 16 | from HBEngine.Core.Objects.renderable import Renderable 17 | from HBEngine.Core.Objects.interactable import Interactable 18 | from HBEngine.Core.Objects.renderable_text import TextRenderable 19 | 20 | 21 | class Button(Interactable): 22 | """ 23 | The Button class extends the 'Interactable' class, and adds an additional child 'TextRenderable' 24 | """ 25 | def __init__(self, renderable_data: dict, parent: Renderable = None): 26 | super().__init__(renderable_data, parent) 27 | # Buttons come in 2 variant forms: Regular and Text-Only: 28 | # Regular: Sprite Renderable + Text Renderable 29 | # Text-Only: Text Renderable 30 | # 31 | # While Text-Only buttons have parameters relating specifically to a Text Renderable, Regular buttons require 32 | # additional parameters for the Sprite Renderable. To differentiate them, Regular buttons store the Text 33 | # Renderable parameters inside a "button_text" parameter 34 | # 35 | # If "button_text" appears in the renderable_data, then we can assume this is a Regular button. Otherwise, we 36 | # can assume it is a Text-Only button 37 | if "button_text" in self.renderable_data: 38 | self.renderable_data["button_text"]["key"] = f"{self.renderable_data['key']}_Text" 39 | self.button_text_renderable = TextRenderable( 40 | self.renderable_data["button_text"], 41 | self 42 | ) 43 | else: 44 | self.button_text_renderable = TextRenderable( 45 | self.renderable_data, 46 | self 47 | ) 48 | 49 | self.children.append(self.button_text_renderable) 50 | 51 | # If the button doesn't make use of a sprite surface, then use the button text surface instead (This allows for 52 | # text-only buttons) 53 | if self.surface.get_width() == 0 and self.surface.get_height() == 0: 54 | self.rect = self.button_text_renderable.rect 55 | self.surface = self.button_text_renderable.surface 56 | 57 | def GetText(self): 58 | pass 59 | 60 | def SetText(self): 61 | pass 62 | 63 | def ChangeState(self, new_state: State): 64 | # We need to intercept the state change in order to update the button_text state as well 65 | if new_state != self.state: 66 | if new_state == State.normal: 67 | self.button_text_renderable.text_color = self.button_text_renderable.renderable_data["text_color"] 68 | elif new_state == State.hover: 69 | self.button_text_renderable.text_color = self.button_text_renderable.renderable_data["text_color_hover"] 70 | elif new_state == State.pressed: 71 | self.button_text_renderable.text_color = self.button_text_renderable.renderable_data["text_color_clicked"] 72 | 73 | self.button_text_renderable.WrapText() 74 | 75 | super().ChangeState(new_state) 76 | 77 | -------------------------------------------------------------------------------- /HBEngine/Core/Actions/transitions.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from HBEngine.Core import settings 16 | 17 | 18 | class Transition: 19 | def __init__(self, renderable, speed=5): 20 | self.renderable = renderable 21 | self.speed = speed 22 | 23 | self.complete = False 24 | 25 | def Start(self): 26 | pass 27 | 28 | def Update(self): 29 | pass 30 | 31 | def Skip(self): 32 | pass 33 | 34 | 35 | class fade_in(Transition): 36 | def __init__(self, renderable, speed=5): 37 | super().__init__(renderable, speed) 38 | 39 | self.progress = 0 40 | self.goal = 256 41 | 42 | def Start(self): 43 | # Start the fade in at 0 opacity 44 | self.renderable.GetSurface().set_alpha(0) 45 | settings.scene.Draw() 46 | 47 | def Update(self): 48 | self.progress += (self.speed * settings.scene.delta_time) 49 | self.renderable.GetSurface().set_alpha(self.progress) 50 | 51 | settings.scene.Draw() 52 | 53 | if self.progress >= self.goal: 54 | print("Transition Complete") 55 | self.complete = True 56 | # TODO: If you have an unload and load action next to eachother withou a pause, and those two actions refer to 57 | # TODO: the same key, they'll attempt to overrid eachother. We need a function for "wait" that works alongside 58 | # TODO: "wait_for_input" 59 | 60 | def Skip(self): 61 | self.renderable.GetSurface().set_alpha(self.goal) 62 | settings.scene.Draw() 63 | self.complete = True 64 | 65 | 66 | class fade_out(Transition): 67 | def __init__(self, renderable, speed=5): 68 | super().__init__(renderable, speed) 69 | 70 | self.progress = self.renderable.GetSurface().get_alpha() 71 | self.goal = 0 72 | 73 | def Update(self): 74 | self.progress -= (self.speed * settings.scene.delta_time) 75 | self.renderable.GetSurface().set_alpha(self.progress) 76 | 77 | settings.scene.Draw() 78 | 79 | if self.progress <= self.goal: 80 | print("Transition Complete") 81 | self.complete = True 82 | 83 | def Skip(self): 84 | self.renderable.GetSurface().set_alpha(self.goal) 85 | settings.scene.Draw() 86 | self.complete = True 87 | 88 | 89 | class text_loading(Transition): 90 | """ Reveals each letter of a text renderable's text sequentially based on the provided transition speed """ 91 | def __init__(self, renderable, speed=5): 92 | super().__init__(renderable, speed) 93 | 94 | self.progress = "" 95 | self.goal = 0 96 | 97 | def Update(self): 98 | self.progress -= (self.speed * settings.scene.delta_time) 99 | self.renderable.GetSurface().set_alpha(self.progress) 100 | 101 | settings.scene.Draw() 102 | 103 | if self.progress <= self.goal: 104 | print("Transition Complete") 105 | self.complete = True 106 | 107 | def Skip(self): 108 | self.renderable.GetSurface().set_alpha(self.goal) 109 | settings.scene.Draw() 110 | self.complete = True 111 | 112 | -------------------------------------------------------------------------------- /HBEngine/Content/Fonts/Comfortaa/FONTLOG.txt: -------------------------------------------------------------------------------- 1 | FONTLOG for Comfortaa: 2 | This file provides detailed information on the Comfortaa Font Software. 3 | This information should be distributed along with the Comfortaa fonts and any derivative works. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Font Information / Notes from the Author: 12 | 13 | Read me V1.6 - 26/12 - 2011 14 | 15 | Thank you for downloading Comfortaa! 16 | 17 | If you like Comfortaa and use it, you are very welcome to leave a donation! 18 | Making fonts is much work and even a very small amount of money will be highly appreciated, 19 | encouraging me to keep making fonts available free of charge. 20 | To make a donation, use the link "donate" included in the comfortaa folder. 21 | 22 | (Note that it is entirely up to you if you want to leave a donation, and not doing so should by no means ethically stop you from using Comfortaa) 23 | 24 | 25 | To install: 26 | 27 | Windows: 28 | Copy the font files to C:\windows\fonts 29 | 30 | Mac: 31 | Put the files into /Library/Fonts (for all users), 32 | or into /Users/Your_username/Library/Fonts (for you only). 33 | 34 | Linux: 35 | Copy the font files to fonts:// in the File manager 36 | 37 | 38 | I hope you will enjoy using it. 39 | 40 | 41 | Brief copyright information: 42 | 43 | Comfortaa is free for personal/noncommercial use, AND commercial use. 44 | 45 | Comfortaa is licensed under the SIL Open Font License, Version 1.1. 46 | http://scripts.sil.org/OFL 47 | This is the standard license of open source fonts and has very few restrictions. Read the "OFL" file included in the Comfortaa folder for detailed information on the license. 48 | 49 | If you still have questions or comments, you are more than welcome to write to my email: aajohan@gmail.com 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ChangeLog: 58 | Please list both major and minor changes made to Comfortaa here (in case of derivative works) - most recent first. 59 | 60 | January 2017 61 | - Version 3.001 62 | - Comfortaa has been improved as part of the Google Font Improvements Project 63 | - Smaller and major changes to all characters 64 | - Added support for Vietnamese 65 | 66 | 8. May 2013 67 | - Version 2.004 2013 68 | - Corrected problems with Cyrillic kerning 69 | - Small correction to uppercase "S" 70 | 71 | 8. August 2012 72 | - Version 2.003 2012 73 | - Corrected the "upside down questionmark" which was facing the wrong direction. 74 | 75 | 2. January 2012 76 | - Version 2.002 2012 77 | - Fixes an issue with the font naming that could result in diffuculty when installing or using. 78 | 79 | 26. December 2011 80 | - Version 2.001 2011 81 | - basic hinting added (not included in v2.000) 82 | - small increase of the space width. 83 | 84 | 29. July 2011 85 | - Version 2.000 2011 86 | - All characters redone at a higher quality. Some characters have changed more than others, e.g. s and A. 87 | - Spacing rethought, as well as kerning 88 | - This version is released to the google web fonts service only. 89 | 90 | 6. December 2010 91 | - Changed filenames to included dashes "-" instead of spaces. 92 | - Changed name of "Comfortaa thin" to "Comfortaa-light" 93 | - Changed the font weight info of Comfortaa-light from 250 to 300 94 | - Made changes to Fontlog.txt (Included the readme file in the fontlog file) 95 | 96 | 30. November 2010 97 | - Corrected "cyrillic small letter yi" and "dotless i" (version 1.004 2010) 98 | 99 | 20. July 2010 100 | - Comfortaa released under a new license: SIL Open Font License, Version 1.1. 101 | 102 | 22. June 2009 103 | - Cyrillic character set added 104 | - Minor changes made to kerning 105 | 106 | 4. Decemeber 2008 107 | - Initial release of font family "Comfortaa" 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | Acknowledgements 116 | If you make modifications be sure to add your name (N), email (E), web-address (W) and description (D). This list is sorted by last name in alphabetical order. 117 | 118 | N: Johan Aakerlund 119 | E: aajohan@gmail.com 120 | W: aajohan.deviantart.com/ 121 | D: Designer/creator of Comfortaa -------------------------------------------------------------------------------- /HBEditor/Core/EditorVariables/editor_variables.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from PyQt6 import QtWidgets 16 | from HBEditor.Core import settings 17 | from HBEditor.Core.Logger.logger import Logger 18 | from HBEditor.Core.base_editor import EditorBase 19 | from HBEditor.Core.EditorVariables.editor_variables_ui import EditorVariablesUI 20 | from HBEditor.Core.EditorVariables.editor_variables_ui import VariableNameUndefined, VariableAlreadyExists, VariableNameReserved 21 | 22 | from HBEditor.Core.DataTypes.file_types import FileType 23 | from HBEditor.Core.EditorUtilities import path 24 | from Tools.HBYaml.hb_yaml import Reader, Writer 25 | 26 | 27 | class EditorVariables(EditorBase): 28 | def __init__(self, file_path): 29 | super().__init__(file_path) 30 | 31 | # Read this data in first as the U.I will need it to initialize properly 32 | self.variables_data = Reader.ReadAll(self.file_path) 33 | 34 | self.editor_ui = EditorVariablesUI(self) 35 | Logger.getInstance().Log("Editor initialized") 36 | 37 | def Export(self): 38 | Logger.getInstance().Log(f"Exporting Variables") 39 | 40 | # Collect the table data 41 | data_to_export = {} 42 | try: 43 | data_to_export = self.editor_ui.GetData() 44 | except VariableNameUndefined: 45 | QtWidgets.QMessageBox.about( 46 | self.editor_ui, 47 | "Unable to Save", 48 | "Variables are required to have a name. Please specify a name for all variables and try again." 49 | ) 50 | return 51 | except VariableAlreadyExists: 52 | QtWidgets.QMessageBox.about( 53 | self.editor_ui, 54 | "Unable to Save", 55 | "Variable names are unique and can not be duplicated. Please ensure all variables have a unique name and try again." 56 | ) 57 | return 58 | except VariableNameReserved: 59 | QtWidgets.QMessageBox.about( 60 | self.editor_ui, 61 | "Unable to Save", 62 | "'' is a reserved word and can not be used for variable names. Please choose a different name and try again." 63 | ) 64 | return 65 | 66 | # Write the data out 67 | Logger.getInstance().Log("Writing data to file...") 68 | try: 69 | Writer.WriteFile( 70 | data_to_export, 71 | self.file_path, 72 | f"# {settings.editor_data['EditorSettings']['version_string']}" 73 | ) 74 | self.editor_ui.SIG_USER_SAVE.emit() 75 | Logger.getInstance().Log("File Exported!", 2) 76 | except Exception as exc: 77 | print(exc) 78 | Logger.getInstance().Log("Failed to Export!", 4) 79 | 80 | # Reload the project variables 81 | settings.LoadVariables() 82 | 83 | def Import(self): 84 | super().Import() 85 | Logger.getInstance().Log(f"Importing Variables data for: {self.file_path}") 86 | 87 | file_data = Reader.ReadAll(self.file_path) 88 | 89 | # Skip importing if the file has no data to load 90 | if file_data: 91 | # Disable signals to prevent marking the editor as dirty while we're populating it 92 | self.editor_ui.blockSignals(True) 93 | 94 | for val_name, val_data in file_data.items(): 95 | self.editor_ui.AddValue(val_name, val_data['type'], val_data['value']) 96 | 97 | self.editor_ui.blockSignals(False) 98 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorUtilities/font.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import os 16 | from io import BytesIO 17 | from PIL import ImageFont 18 | from PyQt6 import QtGui, QtCore 19 | from HBEditor.Core.Logger.logger import Logger 20 | 21 | 22 | def LoadCustomFont(file_path): 23 | """ 24 | Loads and creates a font using the provided file path (either absolute, or a Qt resource path), 25 | applying any style used by that font 26 | """ 27 | if os.path.exists(file_path) and not os.path.isdir(file_path): 28 | QtGui.QFontDatabase.addApplicationFont(file_path) 29 | 30 | font_file = QtCore.QFile(file_path) 31 | if font_file.open(QtCore.QIODevice.OpenModeFlag.ReadOnly): 32 | # 1: 33 | # 'QFontDatabase.addApplicationFont' doesn't return anything that would give us information on 34 | # what type of style the loaded font used (Bold, Italic, etc). All we get is an (int) ID 35 | # that lets us access the family name. This is only half of what we need. 36 | # 37 | # In order to get the style, we're using a font lib to get it directly from the file 38 | 39 | # 2: 40 | # Only Qt can understand data stored by the resource system. Attempting to load a font resource with PIL 41 | # leads to an exception. Instead of passing PIL the file path (which it can't read), pass it the file's 42 | # data in byte form instead 43 | byte_data = BytesIO(font_file.readAll()) 44 | name, style = ImageFont.truetype(font=byte_data, encoding="utf-8").getname() 45 | new_font = QtGui.QFont(name) 46 | 47 | # Apply any style used by the loaded font 48 | ApplyStyle(style, new_font) 49 | 50 | font_file.close() 51 | return new_font 52 | else: 53 | print(font_file.errorString()) 54 | return None 55 | 56 | 57 | else: 58 | Logger.getInstance().Log(f"File does not Exist: '{file_path}'", 3) 59 | return None 60 | 61 | def LoadFont(name, style=""): 62 | """ Creates a font using the name of an already loaded family, and a pre-existing style """ 63 | # Does this font exist? 64 | if QtGui.QFontDatabase().styles(name): 65 | # Does the provided style exist? 66 | available_styles = QtGui.QFontDatabase().styles(name) 67 | if style: 68 | if style in available_styles: 69 | new_font = QtGui.QFont(name) 70 | ApplyStyle(style, new_font) 71 | return new_font 72 | else: 73 | Logger.getInstance().Log(f"Invalid style provided for '{name}'", 3) 74 | style = available_styles[0] 75 | new_font = QtGui.QFont(name) 76 | ApplyStyle(style, new_font) 77 | return new_font 78 | else: 79 | style = available_styles[0] 80 | Logger.getInstance().Log(f"No style provided for '{name}' - Defaulting to '{style}'", 3) 81 | new_font = QtGui.QFont(name) 82 | ApplyStyle(style, new_font) 83 | return new_font 84 | else: 85 | return None 86 | 87 | 88 | def ApplyStyle(style_string: str, font: QtGui.QFont) -> None: 89 | """ Given a string with style text within it, apply the correlating style to the given font """ 90 | # Due to unique cases where certain styles state multiple style (IE. 'Bold Italic' for Arial), do an 91 | # 'in' check, instead of '==' 92 | font.setBold("Bold" in style_string) 93 | font.setItalic("Italic" in style_string) 94 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorDialogue/editor_dialogue_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import copy 16 | from PyQt6 import QtWidgets, QtCore 17 | from HBEditor.Core.base_editor_ui import EditorBaseUI 18 | from HBEditor.Core.EditorCommon.DetailsPanel.details_panel import DetailsPanel 19 | from HBEditor.Core.EditorDialogue.dialogue_sequence_panel import DialogueSequencePanel 20 | from HBEditor.Core.EditorCommon.GroupsPanel.groups_panel import GroupsPanel 21 | from HBEditor.Core.EditorCommon.DetailsPanel.base_source_entry import SourceEntry 22 | 23 | 24 | class EditorDialogueUI(EditorBaseUI): 25 | def __init__(self, core_ref): 26 | super().__init__(core_ref) 27 | 28 | # Build the core editor layout object 29 | self.central_grid_layout = QtWidgets.QGridLayout(self) 30 | self.central_grid_layout.setContentsMargins(0, 0, 0, 0) 31 | self.central_grid_layout.setSpacing(0) 32 | 33 | self.branches_panel = GroupsPanel(title="Branches") 34 | self.branches_panel.SIG_USER_UPDATE.connect(self.SIG_USER_UPDATE.emit) 35 | self.branches_panel.SIG_USER_GROUP_CHANGE.connect(self.core.SwitchBranches) 36 | self.dialogue_sequence = DialogueSequencePanel(self.core) 37 | self.dialogue_sequence.SIG_USER_UPDATE.connect(self.SIG_USER_UPDATE.emit) 38 | self.details = DetailsPanel() 39 | self.details.SIG_USER_UPDATE.connect(self.SIG_USER_UPDATE.emit) 40 | 41 | self.dialogue_settings = DetailsPanel(use_connections=False) 42 | self.dialogue_settings_src_obj = DialogueSettings() 43 | self.dialogue_settings.SIG_USER_UPDATE.connect(self.SIG_USER_UPDATE.emit) 44 | self.dialogue_settings.Populate(self.dialogue_settings_src_obj) 45 | 46 | # The dialogue editor makes use of the "Choice" input widget, which requires a reference 47 | # to the branches list 48 | self.details.branch_list = self.branches_panel.entry_list 49 | 50 | # Allow the user to resize each column 51 | self.main_resize_container = QtWidgets.QSplitter(self) 52 | 53 | # Add a sub tab widget for details, settings, etc 54 | self.sub_tab_widget = QtWidgets.QTabWidget(self) 55 | self.sub_tab_widget.setElideMode(QtCore.Qt.TextElideMode.ElideLeft) 56 | self.sub_tab_widget.addTab(self.details, "Details") 57 | self.sub_tab_widget.addTab(self.dialogue_settings, "Dialogue Settings") 58 | 59 | # Add everything to the editor interface 60 | self.central_grid_layout.addWidget(self.main_resize_container, 0, 0) 61 | self.main_resize_container.addWidget(self.branches_panel) 62 | self.main_resize_container.addWidget(self.dialogue_sequence) 63 | self.main_resize_container.addWidget(self.sub_tab_widget) 64 | 65 | def AdjustSize(self): 66 | # Adjust the main view so it's consuming as much space as possible 67 | self.main_resize_container.setSizes([round(self.width() / 5), round((self.width() / 2) + self.width() / 5), round(self.width() / 4)]) 68 | self.details.AdjustSize() 69 | 70 | 71 | class DialogueSettings(QtCore.QObject, SourceEntry): 72 | SIG_USER_UPDATE = QtCore.pyqtSignal() 73 | ACTION_DATA = { 74 | "interface": { 75 | "type": "Interface", 76 | "value": "", 77 | "flags": ["editable"] 78 | }, 79 | "description": { 80 | "type": "Paragraph", 81 | "value": "", 82 | "flags": ["editable"] 83 | } 84 | } 85 | 86 | def __init__(self): 87 | super().__init__() 88 | self.action_data = copy.deepcopy(self.ACTION_DATA) 89 | 90 | def Refresh(self, change_tree: list = None): 91 | self.SIG_USER_UPDATE.emit() 92 | -------------------------------------------------------------------------------- /HBEditor/Content/content.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fonts/Comfortaa/Comfortaa-Regular.ttf 5 | Fonts/Comfortaa/Comfortaa-Light.ttf 6 | Fonts/Comfortaa/Comfortaa-Bold.ttf 7 | Fonts/Cabin/Cabin-Regular.ttf 8 | Fonts/Cabin/Cabin-Medium.ttf 9 | Fonts/Cabin/Cabin-MediumItalic.ttf 10 | Fonts/Cabin/Cabin-Bold.ttf 11 | Fonts/Cabin/Cabin-BoldItalic.ttf 12 | Fonts/Cabin/Cabin-SemiBold.ttf 13 | Fonts/Cabin/Cabin-SemiBoldItalic.ttf 14 | Fonts/Dustismo/Dustismo.ttf 15 | Fonts/Dustismo/dustismo_italic.ttf 16 | Fonts/Dustismo/dustismo_bold.ttf 17 | Fonts/Dustismo/dustismo_bold_italic.ttf 18 | 19 | 20 | Icons/Checkmark.png 21 | Icons/Checkmark_Disabled.png 22 | Icons/Color_Wheel.png 23 | Icons/Color_Wheel_Disabled.png 24 | Icons/Copy.png 25 | Icons/Down.png 26 | Icons/Up.png 27 | Icons/Close.png 28 | Icons/Close_Hover.png 29 | Icons/File.png 30 | Icons/File_Image.png 31 | Icons/Folder.png 32 | Icons/Folder_Disabled.png 33 | Icons/Minus.png 34 | Icons/Plus.png 35 | Icons/Small_Minus.png 36 | Icons/Small_Minus_Hover.png 37 | Icons/Small_Plus.png 38 | Icons/Small_Plus_Hover.png 39 | Icons/Dialogue.png 40 | Icons/Renderable.png 41 | Icons/Scene.png 42 | Icons/Engine_Logo.png 43 | Icons/Trash.png 44 | Icons/Arrow_Down.png 45 | Icons/Arrow_Down_Hover.png 46 | Icons/Arrow_Right.png 47 | Icons/Arrow_Right_Hover.png 48 | Icons/Lock.png 49 | Icons/Import.png 50 | Icons/Information.png 51 | Icons/Rename.png 52 | Icons/SceneItem_Sprite.png 53 | Icons/Interface.png 54 | Icons/ChangesPending.png 55 | 56 | 57 | Themes/Dark/Dark.css 58 | Themes/Light/Light.css 59 | 60 | 61 | Images/Thumbnails/Template_Blank.jpg 62 | Images/Thumbnails/Template_Interface_Dialogue.jpg 63 | Images/Thumbnails/Template_Interface_Main_Menu.jpg 64 | Images/Thumbnails/Template_Interface_Pause_Menu.jpg 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /HBEngine/Content/Fonts/Comfortaa/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2011 The Comfortaa Project Authors (aajohan@gmail.com), with Reserved Font Name "Comfortaa". 2 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 3 | This license is copied below, and is also available with a FAQ at: 4 | http://scripts.sil.org/OFL 5 | 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION & CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /HBEditor/Content/Fonts/Cabin/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Pablo Impallari (www.impallari.com|impallari@gmail.com), 2 | Copyright (c) 2011, Igino Marini. (www.ikern.com|mail@iginomarini.com), 3 | with Reserved Font Name Cabin. 4 | 5 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 6 | This license is copied below, and is also available with a FAQ at: 7 | http://scripts.sil.org/OFL 8 | 9 | 10 | ----------------------------------------------------------- 11 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 12 | ----------------------------------------------------------- 13 | 14 | PREAMBLE 15 | The goals of the Open Font License (OFL) are to stimulate worldwide 16 | development of collaborative font projects, to support the font creation 17 | efforts of academic and linguistic communities, and to provide a free and 18 | open framework in which fonts may be shared and improved in partnership 19 | with others. 20 | 21 | The OFL allows the licensed fonts to be used, studied, modified and 22 | redistributed freely as long as they are not sold by themselves. The 23 | fonts, including any derivative works, can be bundled, embedded, 24 | redistributed and/or sold with any software provided that any reserved 25 | names are not used by derivative works. The fonts and derivatives, 26 | however, cannot be released under any other type of license. The 27 | requirement for fonts to remain under this license does not apply 28 | to any document created using the fonts or their derivatives. 29 | 30 | DEFINITIONS 31 | "Font Software" refers to the set of files released by the Copyright 32 | Holder(s) under this license and clearly marked as such. This may 33 | include source files, build scripts and documentation. 34 | 35 | "Reserved Font Name" refers to any names specified as such after the 36 | copyright statement(s). 37 | 38 | "Original Version" refers to the collection of Font Software components as 39 | distributed by the Copyright Holder(s). 40 | 41 | "Modified Version" refers to any derivative made by adding to, deleting, 42 | or substituting -- in part or in whole -- any of the components of the 43 | Original Version, by changing formats or by porting the Font Software to a 44 | new environment. 45 | 46 | "Author" refers to any designer, engineer, programmer, technical 47 | writer or other person who contributed to the Font Software. 48 | 49 | PERMISSION & CONDITIONS 50 | Permission is hereby granted, free of charge, to any person obtaining 51 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 52 | redistribute, and sell modified and unmodified copies of the Font 53 | Software, subject to the following conditions: 54 | 55 | 1) Neither the Font Software nor any of its individual components, 56 | in Original or Modified Versions, may be sold by itself. 57 | 58 | 2) Original or Modified Versions of the Font Software may be bundled, 59 | redistributed and/or sold with any software, provided that each copy 60 | contains the above copyright notice and this license. These can be 61 | included either as stand-alone text files, human-readable headers or 62 | in the appropriate machine-readable metadata fields within text or 63 | binary files as long as those fields can be easily viewed by the user. 64 | 65 | 3) No Modified Version of the Font Software may use the Reserved Font 66 | Name(s) unless explicit written permission is granted by the corresponding 67 | Copyright Holder. This restriction only applies to the primary font name as 68 | presented to the users. 69 | 70 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 71 | Software shall not be used to promote, endorse or advertise any 72 | Modified Version, except to acknowledge the contribution(s) of the 73 | Copyright Holder(s) and the Author(s) or with their explicit written 74 | permission. 75 | 76 | 5) The Font Software, modified or unmodified, in part or in whole, 77 | must be distributed entirely under this license, and must not be 78 | distributed under any other license. The requirement for fonts to 79 | remain under this license does not apply to any document created 80 | using the Font Software. 81 | 82 | TERMINATION 83 | This license becomes null and void if any of the above conditions are 84 | not met. 85 | 86 | DISCLAIMER 87 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 88 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 89 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 90 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 91 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 92 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 93 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 94 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 95 | OTHER DEALINGS IN THE FONT SOFTWARE. 96 | -------------------------------------------------------------------------------- /HBEditor/Core/Dialogs/dialog_new_file_from_template.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from PyQt6 import QtWidgets, QtGui, QtCore 16 | from HBEditor.Core import settings 17 | 18 | 19 | class DialogNewFileFromTemplate(QtWidgets.QDialog): 20 | def __init__(self, icon: str, window_title: str = "Choose a Template"): 21 | super().__init__() 22 | 23 | self.setWindowIcon(QtGui.QIcon(QtGui.QPixmap(icon))) 24 | self.setWindowTitle(window_title) 25 | 26 | self.resize(640, 360) 27 | self.main_layout = QtWidgets.QVBoxLayout(self) 28 | self.sub_layout = QtWidgets.QHBoxLayout(self) 29 | self.details_layout = QtWidgets.QVBoxLayout(self) 30 | 31 | # List of options 32 | self.options_list = QtWidgets.QListWidget() 33 | 34 | # Description / thumbnail 35 | self.thumbnail = QtWidgets.QLabel() 36 | self.thumbnail.setScaledContents(True) 37 | self.details_layout.addWidget(self.thumbnail, 1, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter) 38 | self.description = QtWidgets.QLabel() 39 | self.description.setWordWrap(True) 40 | self.details_layout.addWidget(self.description, 1, QtCore.Qt.AlignmentFlag.AlignTop) 41 | 42 | # Name selection 43 | self.name_layout = QtWidgets.QHBoxLayout(self) 44 | self.name_input_header = QtWidgets.QLabel("Name:") 45 | self.name_input = QtWidgets.QLineEdit() 46 | self.name_layout.addWidget(self.name_input_header) 47 | self.name_layout.addWidget(self.name_input) 48 | 49 | # Buttons 50 | self.buttons_layout = QtWidgets.QHBoxLayout(self) 51 | self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel) 52 | self.buttons_layout.addWidget(self.button_box) 53 | 54 | # Compile the full layout 55 | self.sub_layout.addWidget(self.options_list, 2) 56 | self.sub_layout.addLayout(self.details_layout, 1) 57 | self.main_layout.addLayout(self.sub_layout) 58 | self.main_layout.addLayout(self.name_layout) 59 | self.main_layout.addLayout(self.buttons_layout) 60 | 61 | # Signals 62 | self.options_list.currentItemChanged.connect(self.UpdateDetails) 63 | self.button_box.accepted.connect(self.accept) 64 | self.button_box.rejected.connect(self.reject) 65 | 66 | def UpdateDetails(self): 67 | """ Updates the displayed details for the selected option """ 68 | cur_item: FileOption = self.options_list.currentItem() 69 | if cur_item: 70 | self.description.setText(cur_item.GetDescription()) 71 | self.thumbnail.setPixmap( 72 | QtGui.QPixmap(cur_item.GetPreviewImage()).scaled( 73 | round(self.width() / 2), self.thumbnail.height(), QtCore.Qt.AspectRatioMode.KeepAspectRatio 74 | ) 75 | ) 76 | 77 | def GetSelection(self): 78 | """ Returns the template file path for the selected template """ 79 | return self.options_list.currentItem().GetTemplatePath() 80 | 81 | def GetName(self): 82 | """ Returns the inputted name """ 83 | return self.name_input.text() 84 | 85 | 86 | class FileOption(QtWidgets.QListWidgetItem): 87 | def __init__(self, display_text: str, description_text: str, template_path: str, preview_image: str, parent): 88 | super().__init__(display_text, parent) 89 | 90 | self.description_text = description_text 91 | self.template_path = template_path 92 | self.preview_image = preview_image 93 | 94 | def GetDescription(self): 95 | """ Returns the description text for this entry """ 96 | return self.description_text 97 | 98 | def GetTemplatePath(self): 99 | """ Returns the path to the template file stored in this class """ 100 | return self.template_path 101 | 102 | def GetPreviewImage(self): 103 | """ Returns the preview image stored in this class """ 104 | return self.preview_image 105 | -------------------------------------------------------------------------------- /HBEditor/Core/Dialogs/dialog_file_system.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import os 16 | from HBEditor.Core.Logger.logger import Logger 17 | from HBEditor.Core import settings 18 | from PyQt6.QtWidgets import QFileDialog, QMessageBox 19 | 20 | 21 | class DialogFileSystem(QFileDialog): 22 | def __init__(self, parent): 23 | super().__init__(parent) 24 | 25 | def SaveFile(self, type_filter, starting_dir, prompt_title="Save File", project_only=True) -> str: 26 | """ 27 | Prompts the user with a filedialog which has them specify a file to create or write to. If nothing 28 | is selected, return an empty string 29 | """ 30 | 31 | file_path = self.getSaveFileName( 32 | self.parent(), 33 | prompt_title, 34 | starting_dir, 35 | type_filter 36 | ) 37 | 38 | # Did the user choose a value? 39 | if file_path[0]: 40 | selected_dir = file_path[0] 41 | 42 | if project_only: 43 | if settings.user_project_dir in selected_dir: 44 | Logger.getInstance().Log("Valid file chosen") 45 | return selected_dir 46 | else: 47 | self.ShowPathOutsideProjectMessage() 48 | return "" 49 | else: 50 | Logger.getInstance().Log("Valid file chosen") 51 | return selected_dir 52 | else: 53 | Logger.getInstance().Log("File name and path not provided", 3) 54 | return "" 55 | 56 | def GetDirectory(self, starting_dir, prompt_title="Choose a Directory", project_only=True) -> str: 57 | """ Opens up a prompt for choosing an existing directory. If nothing is selected, return an empty string""" 58 | Logger.getInstance().Log("Requesting directory path...") 59 | 60 | dir_path = self.getExistingDirectory( 61 | self.parent(), 62 | prompt_title, 63 | starting_dir 64 | ) 65 | 66 | if dir_path: 67 | if project_only: 68 | if settings.user_project_dir in dir_path: 69 | Logger.getInstance().Log("Valid directory chosen") 70 | return dir_path 71 | else: 72 | self.ShowPathOutsideProjectMessage() 73 | return "" 74 | else: 75 | Logger.getInstance().Log("Valid directory chosen") 76 | return dir_path 77 | else: 78 | Logger.getInstance().Log("No directory chosen", 3) 79 | return "" 80 | 81 | def GetFile(self, starting_dir, type_filter, prompt_title="Choose a File", project_only=True) -> str: 82 | """ Opens up a prompt for choosing an existing file. If nothing is selected, return an empty string""" 83 | Logger.getInstance().Log("Requesting file path...") 84 | 85 | file_path = self.getOpenFileName( 86 | self.parent(), 87 | prompt_title, 88 | starting_dir, 89 | type_filter 90 | ) 91 | 92 | # Did the user choose a value? 93 | if file_path[0]: 94 | selected_dir = file_path[0] 95 | 96 | if project_only: 97 | if settings.user_project_dir in selected_dir: 98 | Logger.getInstance().Log("Valid file chosen") 99 | return selected_dir 100 | else: 101 | self.ShowPathOutsideProjectMessage() 102 | return "" 103 | else: 104 | Logger.getInstance().Log("Valid file chosen") 105 | return selected_dir 106 | else: 107 | Logger.getInstance().Log("File name and path not provided", 3) 108 | return "" 109 | 110 | def ShowPathOutsideProjectMessage(self): 111 | """ Show a notice to the user that the path they have chosen does not reside in the active project directory """ 112 | QMessageBox.about( 113 | self.parent(), 114 | "Invalid Value Provided!", 115 | "The chosen file path exists outside the active project directory.\n" 116 | "Please select a path that resides in the active project" 117 | ) 118 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorCommon/connection_button.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import QtWidgets, QtCore 2 | from HBEditor.Core import settings 3 | from HBEditor.Core.DataTypes.parameter_types import ParameterType 4 | from HBEditor.Core.Logger.logger import Logger 5 | 6 | 7 | class ConnectionButton(QtWidgets.QComboBox): 8 | SIG_USER_UPDATE = QtCore.pyqtSignal(object, str) 9 | 10 | def __init__(self, supported_type: ParameterType, owning_model_item: QtWidgets.QWidgetItem = None): 11 | super().__init__() 12 | self.owning_model_item = owning_model_item # Track the owning widget so we can emit signals properly 13 | self.supported_type = supported_type 14 | self.setToolTip("Connect this parameter to a project variable") 15 | 16 | def showPopup(self) -> None: 17 | result = self.ShowConnectionDialog() 18 | if result == self.currentText() or result == '': 19 | Logger.getInstance().Log("Connection unchanged") 20 | else: 21 | self.Set(result) 22 | self.SIG_USER_UPDATE.emit(self.owning_model_item, result) 23 | 24 | def Get(self) -> str: 25 | return self.currentText() 26 | 27 | def Set(self, new_value: str): 28 | self.removeItem(0) 29 | self.addItem(new_value) 30 | 31 | def ShowConnectionDialog(self) -> str: 32 | connect_dialog = DialogConnection(self.supported_type) 33 | return connect_dialog.GetVariable() 34 | 35 | 36 | class DialogConnection(QtWidgets.QDialog): 37 | def __init__(self, supported_type: ParameterType): 38 | super().__init__() 39 | 40 | # A list of 'FileType' that controls the available files in this browser 41 | self.supported_type = supported_type 42 | 43 | # Hide the OS header to lock its position 44 | self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint) 45 | self.resize(640, 400) 46 | self.main_layout = QtWidgets.QVBoxLayout(self) 47 | 48 | # Options - Search and Active Content Directory 49 | self.options_layout = QtWidgets.QHBoxLayout(self) 50 | self.search_input = QtWidgets.QLineEdit(self) 51 | self.search_input.setPlaceholderText("Search...") 52 | self.search_input.textEdited.connect(self.GetSearchedVariables) # Re-search on every char input 53 | self.options_layout.addWidget(self.search_input, 2) 54 | self.type_explanation = QtWidgets.QLabel(f"Expected Type: {self.supported_type.name}") 55 | self.options_layout.addWidget(self.type_explanation, 1) 56 | self.main_layout.addLayout(self.options_layout) 57 | 58 | # Asset List 59 | self.variable_list = QtWidgets.QListWidget(self) 60 | self.main_layout.addWidget(self.variable_list) 61 | 62 | # Confirmation Buttons 63 | self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel) 64 | self.button_box.accepted.connect(self.accept) 65 | self.button_box.rejected.connect(self.reject) 66 | self.main_layout.addWidget(self.button_box) 67 | 68 | self.Populate() 69 | 70 | def GetVariable(self) -> str: 71 | """ 72 | Activate the dialog, and return the name of the variable selected. If none were chosen, return 'None' 73 | """ 74 | if self.exec(): 75 | selection = self.variable_list.selectedItems() 76 | if selection: 77 | return selection[0].text() 78 | 79 | return "" # Since 'None' is a legitimate answer, use '' to represent a cancelled dialog 80 | 81 | def GetVariablesOfType(self, target_type: ParameterType): 82 | """ Returns a list of names for all variables that match the provided type """ 83 | applicable_variables = ['None'] 84 | for val_name, val_data in settings.user_project_variables.items(): 85 | if ParameterType[val_data['type']] == target_type: 86 | applicable_variables.append(val_name) 87 | 88 | return applicable_variables 89 | 90 | def GetSearchedVariables(self): 91 | """ Use the search_input text to hide all items that don't have it as a substring. Reveal those that do """ 92 | self.variable_list.clearSelection() 93 | search_criteria = self.search_input.text().lower() 94 | for item_index in range(0, self.variable_list.count()): 95 | var_item = self.variable_list.item(item_index) 96 | if search_criteria not in var_item.text().lower(): 97 | var_item.setHidden(True) 98 | else: 99 | var_item.setHidden(False) 100 | 101 | def Populate(self): 102 | """ Clear the variable list, then create an entry for each variable that matches the supported type. Lastly, 103 | add them to 'variable_list' """ 104 | self.variable_list.clear() 105 | 106 | applicable_vars = self.GetVariablesOfType(self.supported_type) 107 | for item in applicable_vars: 108 | new_item = QtWidgets.QListWidgetItem() 109 | new_item.setText(item) 110 | self.variable_list.addItem(new_item) 111 | -------------------------------------------------------------------------------- /HBEngine/Config/Game.yaml: -------------------------------------------------------------------------------- 1 | # Type: Project_Settings 2 | # Created with HBEditor v0.1 3 | 4 | Game: 5 | title: 6 | type: "String" 7 | value: "My Game" 8 | starting_scene: 9 | type: "Scene" 10 | value: "" 11 | Graphics: 12 | resolution: 13 | type: "Dropdown" 14 | value: "1280x720" 15 | options: 16 | - "1280x720" 17 | Audio: 18 | mute: 19 | type: "Bool" 20 | value: false 21 | volume_music: 22 | type: "Int" 23 | value: 100 24 | volume_sfx: 25 | type: "Int" 26 | value: 100 27 | Pause Menu: 28 | interface: 29 | type: "Interface" 30 | value: '' 31 | Default Variables - Text: 32 | text_size: 33 | type: "Int" 34 | value: 24 35 | text_color: 36 | type: "Color" 37 | value: 38 | - 255 39 | - 255 40 | - 255 41 | text_color_hover: 42 | type: "Color" 43 | value: 44 | - 200 45 | - 200 46 | - 200 47 | text_color_clicked: 48 | type: "Color" 49 | value: 50 | - 150 51 | - 150 52 | - 150 53 | text_font: 54 | type: "Asset_Font" 55 | value: HBEngine/Content/Fonts/Comfortaa/Comfortaa-Regular.ttf 56 | text_z_order: 57 | type: "Int" 58 | value: 100 59 | text_center_align: 60 | type: "Bool" 61 | value: true 62 | text_wrap_bounds: 63 | type: "Vector2" 64 | value: 65 | - 0.25 66 | - 0.05 67 | 68 | Default Variables - Sprites: 69 | background_sprite: 70 | type: "Asset_Image" 71 | value: HBEngine/Content/Sprites/Backgrounds/Basic_Dark_1280x720.png 72 | background_z_order: 73 | type: "Int" 74 | value: -9999 75 | 76 | Default Variables - Button: 77 | button_sprite: 78 | type: "Asset_Image" 79 | value: HBEngine/Content/Sprites/Interface/Buttons/Menu_Button_Normal.png 80 | button_sprite_hover: 81 | type: "Asset_Image" 82 | value: HBEngine/Content/Sprites/Interface/Buttons/Menu_Button_Hover.png 83 | button_sprite_clicked: 84 | type: "Asset_Image" 85 | value: HBEngine/Content/Sprites/Interface/Buttons/Menu_Button_Clicked.png 86 | button_z_order: 87 | type: "Int" 88 | value: 10001 89 | checkbox_sprite: 90 | type: "Asset_Image" 91 | value: HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Normal.png 92 | checkbox_sprite_hover: 93 | type: "Asset_Image" 94 | value: HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Hover.png 95 | checkbox_sprite_clicked: 96 | type: "Asset_Image" 97 | value: HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Clicked.png 98 | checkbox_sprite_icon: 99 | type: "Asset_Image" 100 | value: HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Icon.png 101 | 102 | Default Variables - Dialogue: 103 | transition_type: 104 | type: "Dropdown" 105 | value: "None" 106 | options: 107 | - "None" 108 | - "fade_in" 109 | transition_speed: 110 | type: "Int" 111 | value: 500 112 | speaker_text_position: 113 | type: "Vector2" 114 | value: 115 | - 0.262 116 | - 0.703 117 | speaker_text_size: 118 | type: "Int" 119 | value: 32 120 | speaker_text_color: 121 | type: "Color" 122 | value: 123 | - 253 124 | - 253 125 | - 253 126 | speaker_text_font: 127 | type: "Asset_Font" 128 | value: HBEngine/Content/Fonts/Comfortaa/Comfortaa-Regular.ttf 129 | speaker_text_z_order: 130 | type: "Int" 131 | value: 10001 132 | speaker_text_center_align: 133 | type: "Bool" 134 | value: True 135 | speaker_text_wrap_bounds: 136 | type: "Vector2" 137 | value: 138 | - 0.25 139 | - 0.05 140 | dialogue_text_position: 141 | type: "Vector2" 142 | value: 143 | - 0.1 144 | - 0.75 145 | dialogue_text_size: 146 | type: "Int" 147 | value: 24 148 | dialogue_text_color: 149 | type: "Color" 150 | value: 151 | - 253 152 | - 253 153 | - 253 154 | dialogue_text_font: 155 | type: "Asset_Font" 156 | value: HBEngine/Content/Fonts/Comfortaa/Comfortaa-Regular.ttf 157 | dialogue_text_z_order: 158 | type: "Int" 159 | value: 10001 160 | dialogue_text_center_align: 161 | type: "Bool" 162 | value: false 163 | dialogue_text_wrap_bounds: 164 | type: "Vector2" 165 | value: 166 | - 0.8 167 | - 0.16 168 | choice_button_sprite: 169 | type: "Asset_Image" 170 | value: HBEngine/Content/Sprites/Interface/Buttons/Choice_Button_Normal.png 171 | choice_button_sprite_hover: 172 | type: "Asset_Image" 173 | value: HBEngine/Content/Sprites/Interface/Buttons/Choice_Button_Hover.png 174 | choice_button_sprite_clicked: 175 | type: "Asset_Image" 176 | value: HBEngine/Content/Sprites/Interface/Buttons/Choice_Button_Clicked.png 177 | choice_button_z_order: 178 | type: "Int" 179 | value: 10001 180 | choice_button_text_size: 181 | type: "Int" 182 | value: 24 183 | choice_button_text_color: 184 | type: "Vector2" 185 | value: 186 | - 204 187 | - 255 188 | - 255 189 | choice_button_text_color_hover: 190 | type: "Color" 191 | value: 192 | - 204 193 | - 255 194 | - 255 195 | choice_button_text_color_clicked: 196 | type: "Color" 197 | value: 198 | - 204 199 | - 255 200 | - 255 201 | choice_button_font: 202 | type: "Asset_Font" 203 | value: HBEngine/Content/Fonts/Comfortaa/Comfortaa-Regular.ttf -------------------------------------------------------------------------------- /HBEditor/Core/EditorInterface/editor_interface_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import copy 16 | from PyQt6 import QtWidgets, QtCore 17 | from HBEditor.Core.base_editor_ui import EditorBaseUI 18 | from HBEditor.Core.EditorCommon.DetailsPanel.details_panel import DetailsPanel 19 | from HBEditor.Core.EditorCommon.SceneViewer.scene_viewer import SceneViewer 20 | from HBEditor.Core.EditorCommon.GroupsPanel.groups_panel import GroupsPanel 21 | from HBEditor.Core.DataTypes.file_types import FileType 22 | from HBEditor.Core.EditorCommon.DetailsPanel.base_source_entry import SourceEntry 23 | 24 | 25 | class EditorInterfaceUI(EditorBaseUI): 26 | def __init__(self, core_ref): 27 | super().__init__(core_ref) 28 | 29 | self.main_layout = QtWidgets.QHBoxLayout(self) 30 | self.main_layout.setContentsMargins(0, 0, 0, 0) 31 | self.main_layout.setSpacing(0) 32 | 33 | self.central_grid_layout = QtWidgets.QGridLayout(self) 34 | self.central_grid_layout.setContentsMargins(0, 0, 0, 0) 35 | self.central_grid_layout.setSpacing(0) 36 | 37 | self.pages_panel = GroupsPanel(title="Pages", enable_togglable_entries=True) 38 | self.pages_panel.SIG_USER_UPDATE.connect(self.SIG_USER_UPDATE.emit) 39 | self.pages_panel.SIG_USER_GROUP_CHANGE.connect(self.core.EnablePageItems) 40 | self.pages_panel.SIG_USER_GROUP_TOGGLE.connect(self.core.ShowPageItems) 41 | 42 | self.scene_viewer = SceneViewer(FileType.Interface) 43 | self.scene_viewer.SIG_USER_ADDED_ITEM.connect(self.core.RegisterItemToPage) 44 | self.scene_viewer.SIG_USER_DELETED_ITEMS.connect(self.SIG_USER_UPDATE.emit) 45 | self.scene_viewer.SIG_USER_MOVED_ITEMS.connect(self.OnItemMove) 46 | self.scene_viewer.SIG_SELECTION_CHANGED.connect(self.core.UpdateActiveSceneItem) 47 | 48 | self.details = DetailsPanel() 49 | self.details.SIG_USER_UPDATE.connect(self.SIG_USER_UPDATE.emit) 50 | 51 | self.interface_settings = DetailsPanel(use_connections=False) 52 | self.interface_settings_src_obj = InterfaceSettings() 53 | self.interface_settings.SIG_USER_UPDATE.connect(self.SIG_USER_UPDATE.emit) 54 | self.interface_settings.Populate(self.interface_settings_src_obj) 55 | 56 | # Allow the user to resize each column 57 | self.main_resize_container = QtWidgets.QSplitter(self) 58 | 59 | # Add a sub tab widget for details, settings, etc 60 | self.sub_tab_widget = QtWidgets.QTabWidget(self) 61 | self.sub_tab_widget.setElideMode(QtCore.Qt.TextElideMode.ElideLeft) 62 | self.sub_tab_widget.addTab(self.details, "Details") 63 | self.sub_tab_widget.addTab(self.interface_settings, "Settings") 64 | 65 | # Assign everything to the main widget 66 | self.main_layout.addWidget(self.main_resize_container) 67 | self.main_resize_container.addWidget(self.pages_panel) 68 | self.main_resize_container.addWidget(self.scene_viewer) 69 | self.main_resize_container.addWidget(self.sub_tab_widget) 70 | 71 | # Adjust the main view so it's consuming as much space as possible 72 | self.main_resize_container.setStretchFactor(0, 6) 73 | self.main_resize_container.setStretchFactor(1, 8) 74 | self.main_resize_container.setStretchFactor(2, 8) # Increase details panel size to accomodate connection column 75 | 76 | def AdjustSize(self): 77 | # Adjust the main view so it's consuming as much space as possible 78 | self.main_resize_container.setSizes([round(self.width() / 5), round((self.width() / 2) + self.width() / 5), round(self.width() / 4)]) 79 | self.details.AdjustSize() 80 | 81 | def OnItemMove(self, selected_items: list = None): 82 | self.SIG_USER_UPDATE.emit() 83 | self.core.UpdateActiveSceneItem(selected_items) 84 | 85 | def FreezeSignals(self): 86 | """ Block signals for all connected child widgets """ 87 | self.pages_panel.blockSignals(True) 88 | self.scene_viewer.blockSignals(True) 89 | self.details.blockSignals(True) 90 | 91 | def UnfreezeSignals(self): 92 | """ Unblock signals for all connected child widgets """ 93 | self.pages_panel.blockSignals(False) 94 | self.scene_viewer.blockSignals(False) 95 | self.details.blockSignals(False) 96 | 97 | class InterfaceSettings(QtCore.QObject, SourceEntry): 98 | SIG_USER_UPDATE = QtCore.pyqtSignal() 99 | ACTION_DATA = { 100 | "key": { 101 | "type": "String", 102 | "value": "!&INTERFACE&!", 103 | "flags": ["editable"] 104 | }, 105 | "description": { 106 | "type": "Paragraph", 107 | "value": "", 108 | "flags": ["editable"] 109 | } 110 | } 111 | 112 | def __init__(self): 113 | super().__init__() 114 | self.action_data = copy.deepcopy(self.ACTION_DATA) 115 | 116 | def Refresh(self, change_tree: list = None): 117 | self.SIG_USER_UPDATE.emit() 118 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorScene/editor_scene_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import copy 16 | from PyQt6 import QtWidgets, QtCore 17 | from HBEditor.Core.base_editor_ui import EditorBaseUI 18 | from HBEditor.Core.EditorCommon.DetailsPanel.details_panel import DetailsPanel 19 | from HBEditor.Core.EditorCommon.SceneViewer.scene_viewer import SceneViewer 20 | from HBEditor.Core.DataTypes.file_types import FileType 21 | from HBEditor.Core.EditorCommon.DetailsPanel.base_source_entry import SourceEntry 22 | 23 | 24 | class EditorSceneUI(EditorBaseUI): 25 | def __init__(self, core_ref): 26 | super().__init__(core_ref) 27 | 28 | self.main_layout = QtWidgets.QHBoxLayout(self) 29 | self.main_layout.setContentsMargins(0, 0, 0, 0) 30 | self.main_layout.setSpacing(0) 31 | 32 | self.central_grid_layout = QtWidgets.QGridLayout(self) 33 | self.central_grid_layout.setContentsMargins(0, 0, 0, 0) 34 | self.central_grid_layout.setSpacing(0) 35 | 36 | self.scene_viewer = SceneViewer(FileType.Scene) 37 | self.scene_viewer.SIG_USER_ADDED_ITEM.connect(self.SIG_USER_UPDATE.emit) 38 | self.scene_viewer.SIG_USER_DELETED_ITEMS.connect(self.SIG_USER_UPDATE.emit) 39 | self.scene_viewer.SIG_USER_MOVED_ITEMS.connect(self.OnItemMove) 40 | self.scene_viewer.SIG_SELECTION_CHANGED.connect(self.core.UpdateActiveSceneItem) 41 | 42 | self.details = DetailsPanel(self.core.excluded_properties) 43 | self.details.SIG_USER_UPDATE.connect(self.SIG_USER_UPDATE.emit) 44 | 45 | self.scene_settings = DetailsPanel(use_connections=False) 46 | self.scene_settings_src_obj = SceneSettings() 47 | self.scene_settings.SIG_USER_UPDATE.connect(self.SIG_USER_UPDATE.emit) 48 | self.scene_settings.Populate(self.scene_settings_src_obj) 49 | 50 | # Allow the user to resize each column 51 | self.main_resize_container = QtWidgets.QSplitter(self) 52 | 53 | # Add a sub tab widget for details, settings, etc 54 | self.sub_tab_widget = QtWidgets.QTabWidget(self) 55 | self.sub_tab_widget.setElideMode(QtCore.Qt.TextElideMode.ElideLeft) 56 | self.sub_tab_widget.addTab(self.details, "Details") 57 | self.sub_tab_widget.addTab(self.scene_settings, "Scene Settings") 58 | 59 | # Assign everything to the main widget 60 | self.main_layout.addWidget(self.main_resize_container) 61 | self.main_resize_container.addWidget(self.scene_viewer) 62 | self.main_resize_container.addWidget(self.sub_tab_widget) 63 | 64 | # Adjust the space allocation to favor the settings section 65 | self.main_resize_container.setStretchFactor(0, 10) 66 | self.main_resize_container.setStretchFactor(1, 8) # Increase details panel size to accomodate connection column 67 | 68 | def AdjustSize(self): 69 | # Adjust the main view so it's consuming as much space as possible 70 | self.main_resize_container.setSizes([round((self.width() / 4) * 3), round(self.width() / 4)]) 71 | self.details.AdjustSize() 72 | 73 | def OnItemMove(self, selected_items: list = None): 74 | self.SIG_USER_UPDATE.emit() 75 | self.core.UpdateActiveSceneItem(selected_items) 76 | 77 | 78 | class SceneSettings(QtCore.QObject, SourceEntry): 79 | SIG_USER_UPDATE = QtCore.pyqtSignal() 80 | ACTION_DATA = { 81 | "interface": { 82 | "type": "Interface", 83 | "value": "", 84 | "flags": ["editable"] 85 | }, 86 | "description": { 87 | "type": "Paragraph", 88 | "value": "", 89 | "flags": ["editable"] 90 | }, 91 | "allow_pausing": { 92 | "type": "Bool", 93 | "value": "True", 94 | "flags": ["editable"] 95 | }, 96 | "start_actions": { 97 | "type": "Array", 98 | "flags": ["editable", "no_exclusion"], 99 | "template": { 100 | "action": { 101 | "type": "Array_Element", 102 | "flags": ["editable"], 103 | "children": { 104 | "action": { 105 | "type": "Event", 106 | "value": "None", 107 | "options": [ 108 | "None", 109 | "create_background", 110 | "create_sprite", 111 | "create_interactable", 112 | "create_text", 113 | "create_sprite", 114 | "create_button", 115 | "create_text_button", 116 | "play_sfx", 117 | "play_music", 118 | "start_dialogue", 119 | "scene_fade_in", 120 | "set_mute" 121 | ], 122 | "flags": ["editable"] 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | def __init__(self): 131 | super().__init__() 132 | self.action_data = copy.deepcopy(self.ACTION_DATA) 133 | 134 | def Refresh(self, change_tree: list = None): 135 | self.SIG_USER_UPDATE.emit() 136 | 137 | 138 | -------------------------------------------------------------------------------- /HBEngine/Core/Objects/audio.py: -------------------------------------------------------------------------------- 1 | import pygame.mixer 2 | from HBEngine.Core import settings 3 | 4 | 5 | class Sound(pygame.mixer.Sound): 6 | """ A subclass for the pygame Sound object with extra functionality for muting and identification """ 7 | def __init__(self, sound_data: dict): 8 | super().__init__(settings.ConvertPartialToAbsolutePath(sound_data["sound"])) 9 | 10 | self.sound_data = sound_data 11 | self.key = "" 12 | self.channel = None 13 | self.paused = False 14 | self.loop_count = 0 15 | if "loop" in self.sound_data: 16 | if self.sound_data["loop"]: 17 | self.loop_count = -1 18 | 19 | # For indentification in the audio stack, all sounds require a unique identifier 20 | if 'key' not in self.sound_data: 21 | raise ValueError(f"No key assigned to {self}. The 'key' property is mandatory for all renderables") 22 | else: 23 | self.key = self.sound_data["key"] 24 | 25 | # Preset the volume (Use the base implementation to avoid the mute lock in 'SetVolume') 26 | super().set_volume(self.sound_data["volume"]) 27 | 28 | # Prepare muted state 29 | self.pre_mute_volume = 0 30 | if settings.GetProjectSetting("Audio", "mute"): 31 | self.Mute() 32 | 33 | def Mute(self): 34 | self.pre_mute_volume = self.get_volume() 35 | super().set_volume(0) # (Use the base implementation to avoid the mute lock) 36 | 37 | def Unmute(self): 38 | super().set_volume(self.pre_mute_volume) 39 | self.pre_mute_volume = 0 # (Use the base implementation to avoid the mute lock) 40 | 41 | def Pause(self): 42 | self.channel.pause() 43 | self.paused = True 44 | 45 | def Unpause(self): 46 | self.channel.unpause() 47 | self.paused = False 48 | 49 | def GetBusy(self) -> bool: 50 | # We need to ensure that a paused sound is considered still busy in order to avoid it being discarded during 51 | # any cleanup checks 52 | if self.paused: 53 | return True 54 | 55 | try: 56 | return self.channel.get_busy() 57 | except AttributeError: # Channel likely isn't set yet, and thus this sound hasn't started playing 58 | return False 59 | 60 | def set_volume(self, value: float) -> None: 61 | """ 62 | Set the volume for this object. If the 'mute' project setting is True, the volume will remain muted but will 63 | set properly 64 | """ 65 | # Always modify relative SFX volume by the global SFX volume 66 | value = value * settings.GetProjectSetting("Audio", "volume_sfx") 67 | if settings.GetProjectSetting("Audio", "mute"): 68 | self.pre_mute_volume = value 69 | else: 70 | super().set_volume(value) 71 | 72 | def Play(self): 73 | """ Play the loaded SFX file associated with this object, looping according to the loop policy """ 74 | self.channel = self.play(self.loop_count) 75 | 76 | def Stop(self): 77 | """ Stop SFX playback and clear the assigned channel """ 78 | self.paused = False 79 | self.channel = None 80 | self.stop() 81 | 82 | 83 | class Music: 84 | """ 85 | A wrapper for the pygame Music Mixer with extra functionality for muting 86 | 87 | Unlike SFX, pygame does not use an object for music, instead opting for streaming a single musical track using the 88 | pygame.mixer. Since this prevents subclassing, we need an entirely custom object that has hooks into the mixer 89 | """ 90 | def __init__(self, sound_data: dict): 91 | self.sound_data = sound_data 92 | self.paused = False 93 | if "loop" in self.sound_data: 94 | if self.sound_data["loop"]: 95 | self.loop_count = -1 96 | 97 | # Preset the volume (Use the base implementation to avoid the mute lock in 'SetVolume') 98 | pygame.mixer.music.set_volume(self.sound_data["volume"]) 99 | 100 | # Prepare muted state 101 | self.pre_mute_volume = 0 102 | if settings.GetProjectSetting("Audio", "mute"): 103 | self.Mute() 104 | 105 | # Load the track 106 | self.Load() 107 | 108 | def Mute(self): 109 | self.pre_mute_volume = pygame.mixer.music.get_volume() 110 | pygame.mixer.music.set_volume(0) 111 | 112 | def Unmute(self): 113 | pygame.mixer.music.set_volume(self.pre_mute_volume) 114 | self.pre_mute_volume = 0 115 | 116 | def SetVolume(self, value: float) -> None: 117 | """ 118 | Set the volume for this object. If the 'mute' project setting is True, the volume will remain muted but will 119 | set properly 120 | """ 121 | # Always modify music volume by the global music volume 122 | value = value * settings.GetProjectSetting("Audio", "volume_music") 123 | if settings.GetProjectSetting("Audio", "mute"): 124 | self.pre_mute_volume = value 125 | else: 126 | pygame.mixer.music.set_volume(value) 127 | 128 | def Pause(self): 129 | pygame.mixer.music.pause() 130 | self.paused = True 131 | 132 | def Unpause(self): 133 | pygame.mixer.music.unpause() 134 | self.paused = False 135 | 136 | def GetBusy(self) -> bool: 137 | # We need to ensure that a paused track is considered still busy in order to avoid it being discarded during 138 | # any cleanup checks 139 | if self.paused: 140 | return True 141 | 142 | return pygame.mixer.music.get_busy() 143 | 144 | def Load(self): 145 | """ Load the music file associated with this object into the mixer """ 146 | pygame.mixer.music.load(settings.ConvertPartialToAbsolutePath(self.sound_data["music"])) 147 | 148 | def Play(self): 149 | """ Play the loaded music file associated with this object, looping according to the loop policy """ 150 | pygame.mixer.music.play(self.loop_count) 151 | 152 | def Stop(self): 153 | pygame.mixer.music.stop() 154 | 155 | def set_endevent(self): 156 | pass 157 | 158 | def get_endevent(self): 159 | pass 160 | -------------------------------------------------------------------------------- /HBEditor/Core/Outliner/outliner.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import os 16 | from HBEditor.Core.Logger.logger import Logger 17 | from HBEditor.Core import settings 18 | from HBEditor.Core.Outliner.outliner_ui import OutlinerUI, OutlinerAsset 19 | from HBEditor.Core.DataTypes.file_types import FileType 20 | from HBEditor.Core.EditorUtilities import image 21 | 22 | 23 | class Outliner: 24 | def __init__(self, hb_core): 25 | 26 | self.cur_directory = "Content" 27 | 28 | self.hb_core = hb_core # We need an explicit reference to the Heartbeat core in order to access file commands 29 | self.ui = OutlinerUI(self) # Build the Logger UI 30 | self.Populate() # Populate the view with assets from the registry 31 | 32 | def GetUI(self): 33 | return self.ui 34 | 35 | def Populate(self): 36 | """ Create an asset widget for each registered asset in the current directory """ 37 | self.ui.ClearAssets() 38 | self.ui.ClearQAB() 39 | self.ui.CreateQuickAccessButtons() 40 | 41 | cur_dir = settings.GetAssetRegistryFolder(self.cur_directory) 42 | for asset in cur_dir: 43 | # File assets are registered with their respective type enum. Folders hold nested dicts 44 | if isinstance(cur_dir[asset], dict): 45 | self.ui.AddAsset(asset, FileType.Folder) 46 | 47 | elif FileType[cur_dir[asset]] == FileType.Asset_Image: 48 | thumbnail_path = image.GetThumbnail(f"{self.cur_directory}/{asset}") 49 | if not thumbnail_path: 50 | thumbnail_path = image.GenerateThumbnail(f"{self.cur_directory}/{asset}") 51 | 52 | self.ui.AddAsset(asset, FileType[cur_dir[asset]], thumbnail_path) 53 | 54 | else: 55 | self.ui.AddAsset(asset, FileType[cur_dir[asset]]) 56 | 57 | def CreateFolder(self): 58 | asset_name = self.hb_core.NewFolder(self.cur_directory) 59 | if asset_name: 60 | self.Populate() 61 | 62 | def CreateScene(self): 63 | asset_name = self.hb_core.NewFile(self.cur_directory, FileType.Scene) 64 | if asset_name: 65 | self.Populate() 66 | 67 | 68 | def CreateDialogue(self): 69 | asset_name = self.hb_core.NewFile(self.cur_directory, FileType.Dialogue) 70 | if asset_name: 71 | self.Populate() 72 | 73 | def CreateInterface(self): 74 | asset_name = self.hb_core.NewInterface(self.cur_directory) 75 | if asset_name: 76 | self.Populate() 77 | 78 | def OpenFile(self, asset_name: str): 79 | self.hb_core.OpenFile(f"{self.cur_directory}/{asset_name}") 80 | 81 | def DeleteAsset(self): 82 | selected_items = self.ui.asset_list.selectedItems() # Multi-select deletions are not supported currently 83 | if selected_items: 84 | self.hb_core.DeleteFileOrFolder( 85 | f"{self.cur_directory}/{selected_items[0].text()}", 86 | selected_items[0].GetType() == FileType.Folder 87 | ) 88 | self.Populate() 89 | 90 | def DuplicateAsset(self): 91 | selected_items = self.ui.asset_list.selectedItems() # Multi-select deletions are not supported currently 92 | if selected_items: 93 | is_folder = False 94 | if selected_items[0].GetType() == FileType.Folder: 95 | is_folder = True 96 | 97 | self.hb_core.DuplicateFileOrFolder(f"{self.cur_directory}/{selected_items[0].text()}", is_folder) 98 | self.Populate() 99 | 100 | def RenameAsset(self): 101 | selected_items = self.ui.asset_list.selectedItems() # Multi-select deletions are not supported currently 102 | if selected_items: 103 | is_folder = False 104 | if selected_items[0].GetType() == FileType.Folder: 105 | is_folder = True 106 | 107 | self.hb_core.RenameFileOrFolder(f"{self.cur_directory}/{selected_items[0].text()}", is_folder) 108 | self.Populate() 109 | 110 | def MoveAsset(self, source: OutlinerAsset, target: OutlinerAsset): 111 | """ Moves the 'source' asset to the 'target' asset directory """ 112 | source_path = f"{self.cur_directory}/{source.text()}" 113 | target_path = f"{self.cur_directory}/{target.text()}" 114 | 115 | if target.asset_type == FileType.Folder: 116 | if self.hb_core.MoveFileOrFolder(source_path, target_path): 117 | self.ui.RemoveAsset(source.text()) 118 | 119 | self.Populate() 120 | 121 | def MoveAssetUsingQAB(self, source: str, target: str): 122 | """ 123 | Moves an asset to the corresponding directory based on the Quick Access Button that was used. Both paths must 124 | be full paths with the content directory as their roots 125 | """ 126 | 127 | if self.hb_core.MoveFileOrFolder(source, target): 128 | self.ui.RemoveAsset(os.path.basename(source)) 129 | 130 | self.Populate() 131 | 132 | def ImportAsset(self, import_targets: list = None): 133 | if not import_targets: 134 | # No specified target, use full import process 135 | self.hb_core.Import(self.cur_directory) 136 | 137 | elif len(import_targets) == 1 and not os.path.isdir(import_targets[0]): 138 | # Singular target, use partial import process (Warning and error prompts, but no file dialog) 139 | self.hb_core.Import(self.cur_directory, import_targets[0]) 140 | 141 | else: 142 | # N targets, use batch import process (No prompts except for a collective results window) 143 | self.hb_core.BatchImport(self.cur_directory, import_targets) 144 | 145 | self.Populate() 146 | -------------------------------------------------------------------------------- /HBEngine/Content/Interfaces/pause_menu_01.interface: -------------------------------------------------------------------------------- 1 | # Created by Cronza 2 | type: Interface 3 | settings: 4 | description: '' 5 | key: '' 6 | pages: 7 | Persistent: 8 | description: '' 9 | items: 10 | # Background 11 | - create_background: 12 | key: "!&INTERFACE_PAUSE_Background&!" 13 | sprite: HBEngine/Content/Sprites/Interface/Backgrounds/Dark_Transparent_1280x720.png 14 | z_order: 100000 15 | conditions: { } 16 | 17 | # Title 18 | - create_text: 19 | key: "!&INTERFACE_PAUSE_Title&!" 20 | position: 21 | - 0.15 22 | - 0.2 23 | text: Paused 24 | text_size: 60 25 | z_order: 100001 26 | wrap_bounds: 27 | - 0.8 28 | - 0.4 29 | conditions: { } 30 | 31 | # Resume Button 32 | - create_text_button: 33 | key: "!&INTERFACE_PAUSE_Resume_Button&!" 34 | position: 35 | - 0.15 36 | - 0.3 37 | center_align: true 38 | z_order: 100002 39 | text: Resume 40 | text_size: 16 41 | text_color_hover: 42 | - 0 43 | - 127 44 | - 255 45 | wrap_bounds: 46 | - 0.2 47 | - 0.2 48 | events: 49 | event_0: 50 | action: 51 | action: unpause 52 | conditions: { } 53 | 54 | # Options Button 55 | - create_text_button: 56 | key: "!&INTERFACE_PAUSE_Options_Button&!" 57 | position: 58 | - 0.15 59 | - 0.35 60 | center_align: true 61 | z_order: 100002 62 | text: Option 63 | text_size: 16 64 | text_color_hover: 65 | - 0 66 | - 127 67 | - 255 68 | wrap_bounds: 69 | - 0.2 70 | - 0.2 71 | events: 72 | event_0: 73 | action: 74 | action: switch_page 75 | owner: "!&HBENGINE_INTERNAL_PAUSE_INTERFACE!&" 76 | page: page_Options 77 | conditions: { } 78 | conditions: { } 79 | 80 | # Save Button 81 | - create_text_button: 82 | key: "!&INTERFACE_PAUSE_Save_Button&!" 83 | position: 84 | - 0.15 85 | - 0.4 86 | center_align: true 87 | z_order: 100002 88 | text: Save 89 | text_size: 16 90 | text_color_hover: 91 | - 0 92 | - 127 93 | - 255 94 | wrap_bounds: 95 | - 0.2 96 | - 0.2 97 | events: 98 | event_0: 99 | action: 100 | action: switch_page 101 | owner: "!&HBENGINE_INTERNAL_PAUSE_INTERFACE!&" 102 | page: page_Save 103 | conditions: { } 104 | conditions: { } 105 | 106 | # Main Menu Button 107 | - create_text_button: 108 | key: "!&INTERFACE_PAUSE_Main_Menu_Button&!" 109 | position: 110 | - 0.15 111 | - 0.45 112 | center_align: true 113 | z_order: 100002 114 | text: Main Menu 115 | text_size: 16 116 | text_color_hover: 117 | - 0 118 | - 127 119 | - 255 120 | wrap_bounds: 121 | - 0.2 122 | - 0.2 123 | events: 124 | event_0: 125 | action: 126 | action: load_scene 127 | scene_file: '' 128 | conditions: { } 129 | conditions: { } 130 | 131 | # Quit Button 132 | - create_text_button: 133 | key: "!&INTERFACE_PAUSE_Quit_Button&!" 134 | position: 135 | - 0.15 136 | - 0.5 137 | center_align: true 138 | z_order: 100002 139 | text: Quit 140 | text_size: 16 141 | text_color_hover: 142 | - 0 143 | - 127 144 | - 255 145 | wrap_bounds: 146 | - 0.2 147 | - 0.2 148 | events: 149 | event_0: 150 | action: 151 | action: quit_game 152 | conditions: { } 153 | 154 | page_Options: 155 | description: '' 156 | items: 157 | # Page Title 158 | - create_text: 159 | key: "!&INTERFACE_PAUSE_pageOptions_Title&!" 160 | position: 161 | - 0.4 162 | - 0.2 163 | text: Options 164 | text_size: 60 165 | z_order: 100001 166 | wrap_bounds: 167 | - 0.8 168 | - 0.4 169 | conditions: { } 170 | 171 | # Mute Button Text 172 | - create_text: 173 | key: "!&INTERFACE_PAUSE_pageOptions_Mute_All_Button_Text&!" 174 | position: 175 | - 0.34 176 | - 0.3 177 | text: Mute 178 | text_size: 18 179 | z_order: 100001 180 | conditions: { } 181 | 182 | # Mute Button 183 | - create_checkbox: 184 | key: "!&INTERFACE_PAUSE_pageOptions_Mute_All_Button&!" 185 | position: 186 | - 0.4 187 | - 0.3 188 | center_align: true 189 | z_order: 100002 190 | sprite: HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Normal.png 191 | sprite_hover: HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Hover.png 192 | sprite_clicked: HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Clicked.png 193 | sprite_icon: HBEngine/Content/Sprites/Interface/Buttons/Checkbox_Icon.png 194 | connect_project_setting: 195 | category: Audio 196 | setting: mute 197 | events: 198 | event_0: 199 | action: 200 | action: set_mute 201 | toggle: True 202 | conditions: { } 203 | conditions: { } 204 | 205 | page_Save: 206 | description: '' 207 | items: 208 | # Page Title 209 | - create_text: 210 | key: "!&INTERFACE_PAUSE_pageSave_Title&!" 211 | position: 212 | - 0.4 213 | - 0.2 214 | text: Save 215 | text_size: 60 216 | z_order: 100001 217 | wrap_bounds: 218 | - 0.8 219 | - 0.4 220 | conditions: { } 221 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorCommon/SceneViewer/scene_view.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import QtWidgets, QtCore, QtGui 2 | from HBEditor.Core.EditorUtilities import utils 3 | 4 | 5 | class SceneView(QtWidgets.QGraphicsView): 6 | """ A custom subclass of QGraphicsView with extended features """ 7 | SIG_USER_MOVED_ITEMS = QtCore.pyqtSignal(list) 8 | SIG_USER_PASTE = QtCore.pyqtSignal(object) 9 | 10 | def __init__(self, parent, context: str): 11 | super().__init__(parent) 12 | 13 | self.setObjectName("scene-viewer") 14 | 15 | # Track the owning context of this view. This is used to ensure data being copied from this view can only be 16 | # pasted in places with matching context 17 | self.context = context 18 | 19 | self.zoom_min = 0.4 # Zoom Out 20 | self.zoom_max = 4 # Zoom In 21 | self.zoom = 1 # Tracks the amount we've zoomed in 22 | self.zoom_speed = 0.1 23 | 24 | self.setDragMode(self.DragMode.RubberBandDrag) 25 | self.setRubberBandSelectionMode(QtCore.Qt.ItemSelectionMode.ContainsItemShape) 26 | self.setTransformationAnchor(self.ViewportAnchor.AnchorUnderMouse) 27 | self.fitInView(parent.sceneRect(), QtCore.Qt.AspectRatioMode.KeepAspectRatio) 28 | 29 | # Track the mouse position on LMB press to know whether selected items have been moved 30 | self.mouse_pos_on_press = None 31 | 32 | def wheelEvent(self, event): 33 | """ Override the wheel event so we can add zoom functionality """ 34 | if event.angleDelta().y() > 0: # User scrolled in 35 | modifier = 1 36 | else: # User scrolled out 37 | modifier = -1 38 | 39 | factor = 1 + self.zoom_speed * modifier 40 | new_zoom = self.zoom * factor 41 | 42 | # If we've exceeded the zoom bounds, scale directly to the limit 43 | if new_zoom < self.zoom_min: 44 | factor = self.zoom_min / self.zoom 45 | elif new_zoom > self.zoom_max: 46 | factor = self.zoom_max / self.zoom 47 | 48 | self.scale(factor, factor) 49 | self.zoom *= factor 50 | 51 | def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: 52 | # Enable viewport panning while spacebar is down 53 | if event.key() == QtCore.Qt.Key.Key_Space and not event.isAutoRepeat(): 54 | self.setDragMode(self.DragMode.ScrollHandDrag) 55 | self.setInteractive(False) 56 | 57 | elif event == QtGui.QKeySequence.StandardKey.Copy: 58 | selected_items = self.scene().selectedItems() 59 | if selected_items: 60 | # Grab the data stored in all selected entries 61 | selected_items = self.scene().selectedItems() 62 | 63 | # Coalesce data from all selected items 64 | entries = [] 65 | for item in selected_items: 66 | entries.append(item.Get()) 67 | 68 | # Modify the data to include an identifier to the source. This will help ensure copy + pasting only 69 | # works for the right context 70 | data = {'source': self.context, 'data': entries} 71 | QtGui.QGuiApplication.clipboard().setText(str(data)) 72 | 73 | elif event == QtGui.QKeySequence.StandardKey.Paste: 74 | # Validate the data 75 | data = utils.ValidateClipboard(self.context, list) 76 | if data: 77 | self.SIG_USER_PASTE.emit(data) 78 | else: 79 | super().keyPressEvent(event) 80 | 81 | def keyReleaseEvent(self, event: QtGui.QKeyEvent) -> None: 82 | # Disable viewport panning once the spacebar is released 83 | if event.key() == QtCore.Qt.Key.Key_Space and not event.isAutoRepeat(): 84 | self.setDragMode(self.DragMode.RubberBandDrag) 85 | self.setInteractive(True) 86 | 87 | super().keyReleaseEvent(event) 88 | 89 | def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None: 90 | pass 91 | 92 | def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: 93 | super().mousePressEvent(event) 94 | self.mouse_pos_on_press = event.pos() 95 | 96 | def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None: 97 | super().mouseReleaseEvent(event) 98 | 99 | selected_items = self.scene().selectedItems() 100 | if selected_items: 101 | # Only consider a move having happened if the cursor pos moved from when the mouse was first pressed 102 | if self.mouse_pos_on_press != event.pos(): 103 | for item in selected_items: 104 | for child in item.childItems(): 105 | # Always store a normalized position value between 0-1 106 | # 107 | # Normally we'd use 'scenePos' to skip all these conversions, but that seems to return a value 108 | # that is altered by any translation applied to the item (Needs additional review to confirm). 109 | # This breaks features such as 'center_align' 110 | parent_pos = child.pos() 111 | item_pos = child.mapFromParent(parent_pos) 112 | scene_pos = child.mapToScene(item_pos) 113 | norm_range = [ 114 | scene_pos.x() / self.scene().width(), 115 | scene_pos.y() / self.scene().height() 116 | ] 117 | child.action_data["position"]["value"] = norm_range 118 | 119 | self.SIG_USER_MOVED_ITEMS.emit(selected_items) 120 | 121 | 122 | class Scene(QtWidgets.QGraphicsScene): 123 | """ A custom subclass of QGraphicsScene with extended features """ 124 | def __init__(self, rect): 125 | super().__init__(rect) 126 | 127 | self.scene_size = rect # type: QtCore.QRectF 128 | self.AddDefaultBackground() 129 | 130 | def AddDefaultBackground(self): 131 | """ Create a default background (since the scene is, by default, transparent) """ 132 | self.setBackgroundBrush(QtCore.Qt.GlobalColor.darkGray) 133 | background = self.addRect( 134 | QtCore.QRectF(0, 0, self.scene_size.width(), self.scene_size.height()), 135 | QtGui.QPen(), 136 | QtGui.QBrush(QtCore.Qt.GlobalColor.black) 137 | ) 138 | background.setZValue(-9999999999) 139 | 140 | def mouseDoubleClickEvent(self, event: 'QGraphicsSceneMouseEvent') -> None: 141 | pass 142 | 143 | -------------------------------------------------------------------------------- /HBEngine/Core/Modules/dialogue.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | import pygame 16 | from HBEngine.Core import settings, action_manager 17 | from Tools.HBYaml.hb_yaml import Reader 18 | from HBEngine.Core.Objects.renderable import Renderable 19 | from HBEngine.Core.Objects.interface import Interface 20 | 21 | 22 | class Dialogue: 23 | MODULE_NAME = "Dialogue" 24 | RESERVE_INPUT = True # Disable updates for everything but this module (IE. Scene, other modules) 25 | CLOSE_ON_SCENE_CHANGE = True # Prevent this module from persisting between scenes 26 | 27 | def __init__(self, file_path: str): 28 | self.active_renderables = {} 29 | 30 | self.dialogue_index = 0 31 | self.dialogue_data = {} 32 | self.active_branch = "Main" 33 | 34 | # Keep track of all spawned renderables by adding them as children to a root object. When removing this module, 35 | # instead of tracking each instance down, we can just delete the root and all children will go with it 36 | self.root_renderable = Renderable({'key': '!&MODULE_DIALOGUE_ROOT&!', 'z_order': 99999}) 37 | self.root_renderable.visible = False 38 | settings.scene.active_renderables.Add(self.root_renderable) 39 | self.interface = None 40 | 41 | # Read in the dialogue file data 42 | self.dialogue_data = Reader.ReadAll(settings.ConvertPartialToAbsolutePath(file_path)) 43 | 44 | def Start(self): 45 | self.LoadInterface() 46 | self.LoadAction() 47 | 48 | def Shutdown(self): 49 | """" Shut down the module, cleaning up spawned renderables and objects""" 50 | settings.scene.active_renderables.Remove(self.root_renderable.key) 51 | if self.interface: 52 | settings.scene.active_renderables.Remove(self.interface.key) 53 | del settings.scene.active_interfaces[self.interface.key] 54 | self.root_renderable = None 55 | self.interface = None 56 | 57 | def Update(self, events): 58 | for event in events: 59 | if event.type == pygame.KEYUP: 60 | if event.key == pygame.K_SPACE: 61 | # Skip the running action if it's able to be skipped 62 | if action_manager.active_actions: 63 | for action in action_manager.active_actions: 64 | if action.skippable: 65 | action.Skip() 66 | 67 | # No actions active. Go to next 68 | else: 69 | self.LoadAction() 70 | 71 | # Update the AM and all child renderables (if applicable) since we reserve input with this module 72 | action_manager.Update(events) 73 | if self.root_renderable: self.UpdateRenderables([self.root_renderable]) 74 | 75 | def UpdateRenderables(self, target: list = None): 76 | for renderable in target: 77 | renderable.update() 78 | if renderable.children: 79 | self.UpdateRenderables(renderable.children) 80 | 81 | def LoadAction(self): 82 | """ 83 | Runs the next action specified in the dialogue file. Will recurse if the action has 'wait_for_input' set 84 | to False 85 | """ 86 | if len(self.dialogue_data['dialogue'][self.active_branch]["entries"]) > self.dialogue_index: 87 | action_data = self.dialogue_data['dialogue'][self.active_branch]["entries"][self.dialogue_index] 88 | 89 | # The action_dict dict has one top level key which is the name of the action. We need to fetch it in order 90 | # to access the action data stored as the value 91 | name = next(iter(action_data)) 92 | data = action_data[name] 93 | if "post_wait" in data: 94 | if "wait_for_input" in data["post_wait"]: 95 | action_manager.PerformAction(action_data=data, action_name=name, parent=self.root_renderable) 96 | self.dialogue_index += 1 97 | elif "wait_until_complete" in data["post_wait"]: 98 | action_manager.PerformAction(action_data=data, action_name=name, parent=self.root_renderable, completion_callback=self.ActionComplete) 99 | elif "no_wait" in data["post_wait"]: 100 | action_manager.PerformAction(action_data=data, action_name=name, parent=self.root_renderable) 101 | self.dialogue_index += 1 102 | self.LoadAction() 103 | # Default to 'no_wait' when nothing is provided 104 | else: 105 | action_manager.PerformAction(action_data=data, action_name=name, parent=self.root_renderable) 106 | self.dialogue_index += 1 107 | self.LoadAction() 108 | else: 109 | from HBEngine import hb_engine 110 | hb_engine.UnloadModule(self.MODULE_NAME) 111 | 112 | def LoadInterface(self): 113 | """ Load the module interface, adding it as a child to the root renderable and registering it with the scene """ 114 | if self.dialogue_data['settings']['interface']: 115 | self.interface = Interface(Reader.ReadAll(settings.ConvertPartialToAbsolutePath(self.dialogue_data['settings']['interface']))) 116 | self.root_renderable.children.append(self.interface) 117 | 118 | # Add the interface to the scene so actions can still target it, but leave it out of the renderables list so 119 | # it's drawn as a group with other module-specific renderables 120 | settings.scene.active_interfaces[self.interface.key] = self.interface 121 | 122 | def SwitchDialogueBranch(self, branch): 123 | """ Given a branch name within the active dialogue file, switch to using it """ 124 | self.active_branch = branch 125 | self.dialogue_index = 0 126 | self.LoadAction() 127 | 128 | def ActionComplete(self): 129 | """ Moves the dialogue counter forward one, and loads the next action """ 130 | self.dialogue_index += 1 131 | self.LoadAction() 132 | 133 | 134 | -------------------------------------------------------------------------------- /HBEngine/Content/Interfaces/main_menu_01.interface: -------------------------------------------------------------------------------- 1 | # Created by Cronza 2 | type: Interface 3 | settings: 4 | description: '' 5 | key: "!&INTERFACE_MAIN_MENU&!" 6 | pages: 7 | Persistent: 8 | description: '' 9 | items: 10 | # Background 11 | - create_background: 12 | key: "!&INTERFACE_MAIN_MENU_Background&!" 13 | sprite: HBEngine/Content/Sprites/Backgrounds/Basic_Dark_1280x720.png 14 | z_order: 100000 15 | conditions: { } 16 | 17 | # Title 18 | - create_text: 19 | key: "!&INTERFACE_MAIN_MENU_Title&!" 20 | position: 21 | - 0.5 22 | - 0.35 23 | text: '' 24 | text_size: 60 25 | center_align: True 26 | z_order: 100001 27 | wrap_bounds: 28 | - 0.8 29 | - 0.8 30 | connect_project_setting: 31 | category: Game 32 | setting: title 33 | conditions: { } 34 | 35 | # Play Button 36 | - create_text_button: 37 | key: "!&INTERFACE_MAIN_MENU_Play_Button&!" 38 | position: 39 | - 0.5 40 | - 0.5 41 | center_align: true 42 | z_order: 100002 43 | text: Play 44 | text_size: 16 45 | text_color_hover: 46 | - 0 47 | - 127 48 | - 255 49 | wrap_bounds: 50 | - 0.2 51 | - 0.2 52 | events: 53 | event_0: 54 | action: 55 | action: play_sfx 56 | key: SFX 57 | sound: HBEngine/Content/SFX/whistle.wav 58 | volume: 1.0 59 | loop: false 60 | post_wait: no_wait 61 | conditions: { } 62 | event_1: 63 | action: 64 | action: scene_fade_out 65 | color: black 66 | speed: 200 67 | post_wait: wait_until_complete 68 | conditions: { } 69 | event_2: 70 | action: 71 | action: load_scene 72 | scene_file: '' 73 | conditions: { } 74 | conditions: { } 75 | 76 | # Options Button 77 | - create_text_button: 78 | key: "!&INTERFACE_MAIN_MENU_Options_Button&!" 79 | position: 80 | - 0.5 81 | - 0.6 82 | center_align: true 83 | z_order: 100002 84 | text: Option 85 | text_size: 16 86 | text_color_hover: 87 | - 0 88 | - 127 89 | - 255 90 | wrap_bounds: 91 | - 0.2 92 | - 0.2 93 | events: 94 | event_0: 95 | action: 96 | action: switch_page 97 | owner: "!&INTERFACE_MAIN_MENU&!" 98 | page: page_Options 99 | conditions: { } 100 | conditions: { } 101 | 102 | # Load Button 103 | - create_text_button: 104 | key: "!&INTERFACE_MAIN_MENU_Load_Button&!" 105 | position: 106 | - 0.5 107 | - 0.55 108 | center_align: true 109 | z_order: 100002 110 | text: Load 111 | text_size: 16 112 | text_color_hover: 113 | - 0 114 | - 127 115 | - 255 116 | wrap_bounds: 117 | - 0.2 118 | - 0.2 119 | events: 120 | event_0: 121 | action: 122 | action: switch_page 123 | owner: "!&INTERFACE_MAIN_MENU&!" 124 | page: page_Load 125 | conditions: { } 126 | conditions: { } 127 | 128 | # About Button 129 | - create_text_button: 130 | key: "!&INTERFACE_MAIN_MENU_About_Button&!" 131 | position: 132 | - 0.5 133 | - 0.65 134 | center_align: true 135 | z_order: 100002 136 | text: About 137 | text_size: 16 138 | text_color_hover: 139 | - 0 140 | - 127 141 | - 255 142 | wrap_bounds: 143 | - 0.2 144 | - 0.2 145 | events: 146 | event_0: 147 | action: 148 | action: switch_page 149 | owner: "!&INTERFACE_MAIN_MENU&!" 150 | page: page_About 151 | conditions: { } 152 | conditions: { } 153 | 154 | # Quit Button 155 | - create_text_button: 156 | key: "!&INTERFACE_MAIN_MENU_Quit_Button&!" 157 | position: 158 | - 0.5 159 | - 0.7 160 | center_align: true 161 | z_order: 100002 162 | text: Quit 163 | text_size: 16 164 | text_color_hover: 165 | - 0 166 | - 127 167 | - 255 168 | wrap_bounds: 169 | - 0.2 170 | - 0.2 171 | events: 172 | event_0: 173 | action: 174 | action: quit_game 175 | conditions: { } 176 | 177 | page_Load: 178 | description: '' 179 | items: 180 | # Page Title 181 | - create_text: 182 | key: "!&INTERFACE_MAIN_MENU_pageLoad_Title&!" 183 | position: 184 | - 0.5 185 | - 0.2 186 | text: Load 187 | text_size: 60 188 | z_order: 100001 189 | wrap_bounds: 190 | - 0.8 191 | - 0.4 192 | conditions: { } 193 | 194 | page_About: 195 | description: '' 196 | items: 197 | # Background 198 | - create_background: 199 | key: "!&INTERFACE_MAIN_MENU_pageAbout_Background&!" 200 | sprite: HBEngine/Content/Sprites/Backgrounds/Basic_Dark_1280x720.png 201 | z_order: 100000 202 | conditions: { } 203 | 204 | # Page Title 205 | - create_text: 206 | key: "!&INTERFACE_MAIN_MENU_pageAbout_Title&!" 207 | position: 208 | - 0.5 209 | - 0.2 210 | text: About 211 | text_size: 60 212 | center_align: True 213 | z_order: 100001 214 | wrap_bounds: 215 | - 0.8 216 | - 0.4 217 | conditions: { } 218 | 219 | # Back Button 220 | - create_text_button: 221 | key: "!&INTERFACE_MAIN_MENU_pageAbout_Back_Button&!" 222 | position: 223 | - 0.5 224 | - 0.85 225 | center_align: true 226 | z_order: 100002 227 | text: Back 228 | text_size: 16 229 | text_color_hover: 230 | - 0 231 | - 127 232 | - 255 233 | wrap_bounds: 234 | - 0.2 235 | - 0.2 236 | events: 237 | event_0: 238 | action: 239 | action: remove_page 240 | owner: "!&INTERFACE_MAIN_MENU&!" 241 | conditions: { } 242 | conditions: { } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heartbeat Engine 2 | ![ScreenShot](Images/HeartbeatEngine_Banner.jpg?raw=true "Heartbeat Engine Banner") 3 | [![License](https://img.shields.io/badge/license-GPLv3-blue?label=license&style=flat-square)](LICENSE.txt) 4 | [![GitHub issues](https://img.shields.io/github/issues-raw/Cronza/HeartbeatEngine?style=flat-square)](https://github.com/Cronza/HeartbeatEngine/issues) 5 | [![Python Version](https://img.shields.io/badge/python-3.12-4B8BBE?style=flat-square)](https://www.python.org/downloads/release/python-380/) 6 | [![Twitter Badge](https://img.shields.io/badge/Twitter-Profile-informational?style=flat-square&logo=twitter&logoColor=white&color=1CA2F1)](https://twitter.com/SomeCronzaGuy) 7 | 8 |

A Visual Novel / Point & Click Game Engine Designed for Creative Developers

9 | 10 | # What is the 'Heartbeat Engine'? 11 | ![ScreenShot](Images/v02_Dialogue_Scene.png?raw=true "Dialogue Scene") 12 |

Fig 1 - An example of branching dialogue in Visual Novel Format

13 | 14 | The Heartbeat Engine is a Visual Novel / Point & Click game engine written in Python using the PyGame framework, with a fully-fledged editor built in PyQt6. It is meant as a lightweight, user-friendly engine that allows developers to focus more on making games, and less on worrying about coding them. 15 | 16 | # What can I do with the 'Heartbeat Engine'? 17 | ![ScreenShot](Images/General_Editor_Example.png?raw=true "General Editor Example") 18 |

Fig 2 - Designing a Scene with the Drag 'n Drop Scene Editor

19 | 20 | Using the Heartbeat Editor, users are given a platform to design and build their projects. From importing external assets such as Art and Music to creating levels and scenes, the users have the tools to succeed. 21 | 22 | Within the Heartbeat Editor are a collection of "Sub-editors" which allow users to create custom formats representing levels, interfaces, dialogue sequences, etc. The editors for each of these formats are designed to be as user-friendly as possible, and require no prior experience with coding to use. 23 | 24 | ![ScreenShot](Images/Interface_Editor_Template_Options.jpg?raw=true "Interface Template Options") 25 |

Fig 3 - Available templates when creating Interfaces

26 | 27 | To further assist in the development of user projects, the Heartbeat Editor provides free templates and samples of custom formats, as well as free assets to fulfill a variety of needs such as: 28 | 29 | 1. Sprites (Buttons, backgrounds, etc) 30 | 2. Fonts 31 | 3. Audio (Music, SFX) 32 | 33 | These are all accessible directly through the Heartbeat Editor, and require no additional software or downloads to use. 34 | 35 | # Getting Started 36 | Currently, the Heartbeat Engine is in core development, so there are no compiled releases available. As such, you will need to setup your local environment to use the engine from source. Please follow these steps to do so: 37 | 38 | 1. Get the Engine Source 39 | 40 | ``` 41 | git clone 42 | ``` 43 | 44 | 2. Create a virtual environment (venv) in the repo folder 45 | 46 | *Note*: This is technically optional, although it is highly advised as to isolate this project and its dependencies. In addition, some utility terminal scripts which come with the engine explicitly refer to a local venv, and will fail if you do not do this. 47 | ``` 48 | python -m venv /venv 49 | ``` 50 | 51 | 3. Install from `requirements.txt` 52 | 53 | ``` 54 | /venv/Scripts/pip.exe install -r /requirements.txt 55 | ``` 56 | 57 | 4. Launch the Editor 58 | 59 | - Option 1: Run `launch_editor.bat`. This will launch the editor with a CMD terminal for debug output. However, the lack of debugger will mean fatal crashes are not caught. 60 | - Option 2: Use PyCharm / another IDE and invoke `python.exe main.py`. This offers more debugging functions and error handling, and is the advised workflow if you intend to modify the engine source (I personally use PyCharm) 61 | 62 | 63 | # License 64 | The Heartbeat Engine is licensed under the GPL version 3 (GPLv3) license. Details can be found in the `LICENSE.txt` file, but essentially any projects created with the engine are entirely, 100% yours. You may use your own license for them, keep them as private works, and release them commercially without worry. 65 | 66 | However, if you decide to fork the engine and alter or extend its behaviour (which I wholeheartedly welcome!), then that code must abide by the GPLv3 license and remain open source. 67 | 68 | # Features 69 | ## Simplified File Formats 70 | ![ScreenShot](Images/HBEditor_Dialogue_Editor.png?raw=true "Dialogue Scene") 71 |

Fig 4 - The Dialogue Editor with an example of numerous configurable properties for an action

72 | 73 | The Heartbeat Engine uses YAML for its proprietary file formats such as `.scene`, `.dialogue`, etc so that the underlying files are human-readable and easy to edit outside of the Heartbeat Editor. 74 | 75 | For example, if you were to create a Dialogue sequence within the Heartbeat Editor that contains a `create_sprite` action, and then open the underlying file in a text editor, you would see the following: 76 | 77 | ``` 78 | - create_sprite: 79 | key: ExampleKey 80 | sprite: HBEngine/Content/Sprites/Interface/Buttons/Menu_Button_Normal.png 81 | position: 82 | - 0.5 83 | - 0.5 84 | center_align: true 85 | z_order: 0 86 | flip: false 87 | transition: 88 | type: None 89 | speed: 500 90 | conditions: {} 91 | post_wait: wait_for_input 92 | ``` 93 | 94 | While the Heartbeat Editor acts as a wrapper for these files, anyone can edit these files directly if they need to perform a hotfix, or need to make changes through automation. 95 | 96 | ## Action Manager 97 | The Action Manager was created in order to allow developers (and the engine) to access any number of possible in-game actions in a flexible, YAML-accessible manner. 98 | 99 | 'Actions' are defined as classes in the `actions.py` file. When an action is requested, such as by a YAML file (Dialogue, clicking interactables, etc), it looks to this file using Python's reflection capabilities for the corresponding action class. For example: `class load_scene(Action)`. Once found, it calls it, passing it any additional pieces of information provided. An example of a dialogue YAML block: 100 | 101 | ``` 102 | - dialogue: 103 | speaker: 104 | text: Cronza 105 | transition: {} 106 | dialogue: 107 | text: Seg Faults Suck 108 | transition: {} 109 | conditions: {} 110 | post_wait: wait_for_input 111 | ``` 112 | The above is an example of an action used to display dialogue text to the screen. The action name is called `dialogue`. In the `actions.py` file, there is a corresponding `class dialogue(Action)` class. The engine is capable of correlating these strings to the classes found in the `actions.py` file. 113 | 114 | Additionally, each action is fed the entire data block that was provided when it was called. In the example above, we see a variety of parameters such as `post_wait` which are used to alter the function of the corresponding action. 115 | -------------------------------------------------------------------------------- /HBEditor/Core/EditorProjectSettings/editor_project_settings_ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Heartbeat Engine is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | The Heartbeat Engine is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with the Heartbeat Engine. If not, see . 14 | """ 15 | from HBEditor.Core.base_editor_ui import EditorBaseUI 16 | from HBEditor.Core.EditorCommon.input_entries import * 17 | from HBEditor.Core.EditorCommon import input_entry_handler as ieh 18 | 19 | 20 | class EditorProjectSettingsUI(EditorBaseUI): 21 | def __init__(self, core_ref): 22 | super().__init__(core_ref) 23 | # Track the active category, as we need a reference to it when we switch categories 24 | self.active_category = None 25 | 26 | # Build the core editor layout object 27 | self.main_layout = QtWidgets.QHBoxLayout(self) 28 | self.main_layout.setContentsMargins(0, 0, 0, 0) 29 | self.main_layout.setSpacing(0) 30 | 31 | # Allow the user to resize each column 32 | self.main_resize_container = QtWidgets.QSplitter(self) 33 | 34 | # Category Section 35 | self.categories = QtWidgets.QWidget() 36 | self.category_layout = QtWidgets.QVBoxLayout(self) 37 | self.category_layout.setContentsMargins(0, 0, 0, 0) 38 | self.category_layout.setSpacing(0) 39 | self.categories.setLayout(self.category_layout) 40 | 41 | self.category_title = QtWidgets.QLabel(self) 42 | self.category_title.setText("Categories") 43 | self.category_title.setObjectName("h1") 44 | self.category_list = QtWidgets.QListWidget() 45 | self.category_list.itemSelectionChanged.connect(self.SwitchCategory) 46 | self.category_layout.addWidget(self.category_title) 47 | self.category_layout.addWidget(self.category_list) 48 | 49 | # Settings Section 50 | self.settings = QtWidgets.QWidget() 51 | self.settings_layout = QtWidgets.QVBoxLayout(self) 52 | self.settings_layout.setContentsMargins(0, 0, 0, 0) 53 | self.settings_layout.setSpacing(0) 54 | self.settings.setLayout(self.settings_layout) 55 | self.settings_title = QtWidgets.QLabel(self) 56 | self.settings_title.setText("Settings") 57 | self.settings_title.setObjectName("h1") 58 | self.settings_tree = QtWidgets.QTreeWidget(self) 59 | self.settings_tree.setColumnCount(2) 60 | self.settings_tree.setHeaderLabels(['Name', 'Input']) 61 | self.settings_tree.setAutoScroll(False) 62 | self.settings_tree.header().setStretchLastSection(False) # Disable to allow custom sizing 63 | self.settings_tree.header().setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch) 64 | self.settings_tree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 65 | self.settings_tree.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.NoSelection) 66 | self.settings_layout.addWidget(self.settings_title) 67 | self.settings_layout.addWidget(self.settings_tree) 68 | 69 | # Assign everything to the main widget 70 | self.main_layout.addWidget(self.main_resize_container) 71 | self.main_resize_container.addWidget(self.categories) 72 | self.main_resize_container.addWidget(self.settings) 73 | 74 | # Adjust the space allocation to favor the settings section 75 | self.main_resize_container.setStretchFactor(0, 0) 76 | self.main_resize_container.setStretchFactor(1, 1) 77 | 78 | #TODO: Investigate how to improve the sizing for the first col. It should be a percentage of the available space 79 | self.settings_tree.header().setDefaultSectionSize(self.settings_tree.header().defaultSectionSize() * 2) 80 | 81 | # Generate entries for each project setting and category 82 | self.PopulateCategories() 83 | 84 | def PopulateCategories(self): 85 | """ Populates the list of category options using the available project settings data in the core """ 86 | self.category_list.clear() 87 | 88 | for category in self.core.project_settings: 89 | self.category_list.addItem(QtWidgets.QListWidgetItem(category)) 90 | 91 | self.category_list.setCurrentRow(0) 92 | self.active_category = self.category_list.item(0) 93 | 94 | def SwitchCategory(self): 95 | """ Switch the active category by saving the current settings data, then repopulates the settings list """ 96 | if self.active_category: 97 | self.UpdateProjectSettingsData() 98 | 99 | self.active_category = self.category_list.currentItem() 100 | self.PopulateSettings() 101 | 102 | def UpdateProjectSettingsData(self): 103 | """ 104 | This updates the project settings data currently loaded by the editor, but does not save the file. If the user 105 | wishes to avoid saving their changes, they just need to close this editor 106 | """ 107 | current_category_name = self.active_category.text() 108 | root = self.settings_tree.invisibleRootItem() 109 | for index in range(0, root.childCount()): 110 | name = self.settings_tree.itemWidget(root.child(index), 0).text() 111 | input_type = self.settings_tree.itemWidget(root.child(index), 1).GetType() 112 | value = self.settings_tree.itemWidget(root.child(index), 1).Get()["value"] 113 | 114 | self.core.project_settings[current_category_name][name]['type'] = input_type.name 115 | self.core.project_settings[current_category_name][name]['value'] = value 116 | 117 | def PopulateSettings(self): 118 | """ Populates the settings list based on the selected category """ 119 | 120 | self.settings_tree.clear() 121 | 122 | # Loop to add all settings for the selected category. Double up the search by parsing the 123 | # schema as well for the data types of each setting 124 | selected_category = self.category_list.currentItem().text() 125 | for setting_name, setting_data in self.core.project_settings[selected_category].items(): 126 | ieh.Add( 127 | owner=self, 128 | view=self.settings_tree, 129 | name=setting_name, 130 | data=setting_data, 131 | signal_func=self.ConnectSignals 132 | ) 133 | 134 | def ConnectSignals(self, tree_item): 135 | """ Connects the InputEntry signals to slots within this class """ 136 | input_widget = self.settings_tree.itemWidget(tree_item, 1) 137 | input_widget.SIG_USER_UPDATE.connect(self.UserUpdatedInputWidget) 138 | 139 | def UserUpdatedInputWidget(self, owning_tree_item: QtWidgets.QTreeWidgetItem): 140 | self.SIG_USER_UPDATE.emit() 141 | --------------------------------------------------------------------------------