GithubHelp home page GithubHelp logo

atomic-junky / monologue Goto Github PK

View Code? Open in Web Editor NEW
10.0 2.0 2.0 38.64 MB

Create your dialogues for your game

Home Page: https://atomic-junky.itch.io/monologue

License: MIT License

GDScript 98.13% Batchfile 0.07% Shell 0.05% C# 0.17% CSS 0.66% HTML 0.92%
dialog dialogue dialogue-systems dialogues godot lightnovel node-editor visual-novel visualnovel

monologue's Introduction

Monologue is a powerful non-linear dialogue editor. ๐Ÿฆ–

You can find the wiki here.

Monologue is a graph node based system dialogue editor that allows you to create modular and non-linear dialogues in any game engine.

The principle of Monologue is to assign a unique ID to each node, so that any node can refer to any other node. This powerful mechanism allows you to create amazing dialogues and stories.

Monologue example

Super Simple export

Monologue is working with just a simple JSON file and nothing else. You just need to save your chnages and it will update your JSON file.

Features

  • Easy to use: A simple and intuitive user interface.
  • JSON: Everything is stored in a JSON file.
  • Godot: Monologue is built using a free and open source game engine.
  • Manage everything: Control audio, backgrounds and characters.
  • Variables: You can define variables, compare them and update their values.
  • Events: You create events to execute storylines when something happens.
  • Test: You can test your story directly in Monologue.

The JSON file format

{
	"EditorVersion": "",
	"RootNodeId": "", # The id of the root node (where all start)
	"ListNodes": [ # Where all the nodes are stored
		...
	],
	"Characters": [ # All the characters
		...
	],
	"Variables": [ # All the variables
		...
	]
}

Interpretation

You can write your own script or use the MonologueProcess script here if you're using Godot.

To use the MonologueProcess script, create a script that extends MonologueProcess. This will allow you to use the built-in signals. You can read more about this in this script.

Credits ans Support

This project is originally from Amberlim (although these two projects no longer share any lines of code), so if you want, here is her Discord server. However, if you need help, don't hesitate to create an issue on Github.

monologue's People

Contributors

amberlim avatar atomic-junky avatar jeremi360 avatar railkill avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

monologue's Issues

Interface redesign

Monologue is becoming more and more complex, and tends to become even more so. It is therefore necessary to revise the interface to make the software more user-friendly. Also, with the addition of new features and elements, the interface has become messy and inconsistent.

The aim of this issue is to discuss possible changes. It is linked to the feature/ui-redesign branch.

Prevent the program from closing if one of the files has not been saved

Use Case

Quality of life.

Current Implementation

When you close the program, the program exit even if changes have been made.

Proposed Implementation

Detect if any changes have been made since the program was last saved. If the program tries to close while changes have not been saved, a pop-up window opens asking whether or not the user wants to save the changes made.

There are two ways of detecting changes. The first, and least clean, would be to generate a json save for each project at every close request and compare it with the original file to see if there's any difference. The other, and most preferable, would be to keep a trace of every change made, to see if there have been any changes since the last save. This last solution (a little abstract) could be implemented using part of an undo/redo system #7, but would require a lot of code rewriting/cleaning.

Monologue crashes during Test if ActionOption has invalid option ID

Version: Tested in v2.1.2, still present in v2.2.3
Severity: Low

Steps to Reproduce

  1. Create an ActionNode.
  2. Select ActionOption as the action type.
  3. Enter some option ID that doesn't exist, the panel will tell you that it doesn't exist.
  4. Start the Test anyway.

Current Behavior

Monologue will crash when it reaches this ActionNode. Changes are saved so it's not a big deal, but it's an inconvenience.

Expected Behavior

The node should be skipped with a toast notification saying option ID was not found. The user should be informed of this error.

Photoshopped Preview

missing_notification

Revert property crash

When you delete the selected node with the delete shortcut and you do Ctrl+Z after the program will crash.
The program stop at the line 32 in PropertyHistory.gd where the variable node can be null.

image

Separate Header from MonologueControl.gd

This issue breaks down refactoring efforts to separate concerns from MonologueControl.gd, so contributors can branch out and work on it without much conflict/overlap. Collaborators can assign themselves to the issue they're working on, or feel free to leave a comment if you're working on it from a fork.

The Header control containing File, Add, Help menu buttons, together with Save and Test buttons, should be separated into its own scene with callbacks/signals to MonologueControl.gd if needed. File > Open has ties to #31, there is already a FileDialogv2 and GlobalFileDialog.gd implementation in main branch, so that's where it will connect to for the file opening operation.

#################
# Header menu #
#################
func _on_file_id_pressed(id):
match id:
0: # Open file
is_header_file_operation = true
open_file_select()
1: # New file
is_header_file_operation = true
new_file_select()
3: # Config
side_panel_node.show_config()
4: # Test
GlobalSignal.emit("test_trigger")
func _on_new_file_btn_pressed():
is_header_file_operation = false
new_file_select()
func _on_open_file_btn_pressed():
is_header_file_operation = false
open_file_select()
func _on_help_id_pressed(id):
match id:
0:
OS.shell_open("https://github.com/atomic-junky/Monologue/wiki")

Separate file handling from MonologueControl.gd into FileDialogv2

This issue breaks down refactoring efforts to separate concerns from MonologueControl.gd, so contributors can branch out and work on it without much conflict/overlap. Collaborators can assign themselves to the issue they're working on, or feel free to leave a comment if you're working on it from a fork.

FileDialogv2 is already present in the main branch as preliminary work for this separation of concerns. This was introduced as part of FilePickerButton, so MonologueControl should use it too. Continue the refactoring along this idea and move the file selection code section from MonologueControl.gd out into its own scene/script with GlobalFileDialog.gd in mind:

####################
# File selection #
####################
func new_file_select():
file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
file_dialog.title = "Create New File"
file_dialog.ok_button_text = "Create"
file_dialog.popup_centered()
func open_file_select():
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
file_dialog.title = "Open File"
file_dialog.ok_button_text = "Open"
file_dialog.popup_centered()
func _on_file_dialog_selected(path: String):
if is_header_file_operation:
welcome_window.hide()
file_dialog.hide()
new_graph_edit()
match file_dialog.file_mode:
FileDialog.FILE_MODE_SAVE_FILE:
FileAccess.open(path, FileAccess.WRITE)
file_selected(path, 0)
FileDialog.FILE_MODE_OPEN_FILE:
file_selected(path, 1)
func file_selected(path, open_mode):
if not FileAccess.open(path, FileAccess.READ):
return
for ge in graph_edits.get_children():
if not ge is MonologueGraphEdit:
continue
if ge.file_path == path:
return
no_interactions_dimmer.hide()
tab_bar.add_tab(Util.truncate_filename(path.get_file()))
tab_bar.move_tab(tab_bar.tab_count - 2, tab_bar.tab_count - 1)
tab_bar.current_tab = tab_bar.tab_count - 2
var graph_edit = get_current_graph_edit()
graph_edit.control_node = self
graph_edit.file_path = path
graph_edit.undo_redo.connect("version_changed", update_tab_savestate.bind(graph_edit))
welcome_window.hide()
if open_mode == 0: #NEW
for node in graph_edit.get_nodes():
node.queue_free()
var new_root_node = root_scene.instantiate()
graph_edit.add_child(new_root_node)
await save(true)
if not FileAccess.file_exists(HISTORY_FILE_PATH):
FileAccess.open(HISTORY_FILE_PATH, FileAccess.WRITE)
else:
var file: FileAccess = FileAccess.open(HISTORY_FILE_PATH, FileAccess.READ_WRITE)
var raw_data = file.get_as_text()
var data: Array
if raw_data:
data = JSON.parse_string(raw_data)
data.erase(path)
data.insert(0, path)
else:
data = [path]
for p in data:
if FileAccess.file_exists(p):
continue
data.erase(p)
file = FileAccess.open(HISTORY_FILE_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify(data.slice(0, 10)))
load_project(path)

so that all file open/close operations be handled by FileDialogv2. Remember to add unit tests.

Additional Thoughts

Seems clear that FileDialog is for adding new file and opening existing file, but what about saving files? Saving files doesn't use a FileDialog in our current design, so should it remain in MonologueControl.gd? Personally, I think it should just be moved into here. Just pretend we have a Save As... function for now, it's more normal and intuitive for all file operations to be in one place. MonologueControl.gd can still handle the building of JSON data for the save, then pass it to GlobalFileDialog to write it as a file.

Separate tab handling from MonologueControl.gd

This issue breaks down refactoring efforts to separate concerns from MonologueControl.gd, so contributors can branch out and work on it without much conflict/overlap. Collaborators can assign themselves to the issue they're working on, or feel free to leave a comment if you're working on it from a fork.

Separate tab bar into its own scene/script. Some of the code has already been sectioned out in MonologueControl.gd, look for the comment:

###############################
# Tab-switching and closing #
###############################
func _close_tab(graph_edit, tab_index, save_first = false):
if save_first:
save(true)
graph_edit.queue_free()
await graph_edit.tree_exited # buggy if we switch tabs without waiting
tab_bar.remove_tab(tab_index)
if tab_bar.tab_count == 0:
get_tree().quit()
elif is_closing_all_tabs:
tab_close_pressed(0)
func close_welcome_tab():
if tab_bar.tab_count > 1:
tab_bar.select_previous_available()
welcome_window.hide()
no_interactions_dimmer.hide()
func new_graph_edit():
var new_graph: MonologueGraphEdit = graph_edit_inst.instantiate()
var new_root_node = root_scene.instantiate()
new_graph.name = "new"
connect_side_panel(new_graph)
graph_edits.add_child(new_graph)
new_graph.add_child(new_root_node)
for ge in graph_edits.get_children():
ge.visible = ge == new_graph
func tab_changed(_idx):
if tab_bar.get_tab_title(tab_bar.current_tab) != "+":
for ge in graph_edits.get_children():
if graph_edits.get_child(tab_bar.current_tab) == ge:
ge.visible = true
if ge.active_graphnode:
side_panel_node.on_graph_node_selected(ge.active_graphnode, true)
else:
side_panel_node.hide()
else:
ge.visible = false
return
new_graph_edit()
var welcome_close_button = $WelcomeWindow/PanelContainer/CloseButton
if tab_bar.tab_count > 1:
welcome_close_button.show()
else:
welcome_close_button.hide()
welcome_window.show()
no_interactions_dimmer.show()
side_panel_node.hide()
func tab_close_pressed(tab):
var ge = graph_edits.get_child(tab)
if ge.is_unsaved(): # prompt user if there are unsaved changes
disable_picker_mode()
tab_bar.current_tab = tab
var save_prompt = prompt_scene.instantiate()
save_prompt.connect("ready", no_interactions_dimmer.show)
save_prompt.connect("tree_exited", no_interactions_dimmer.hide)
save_prompt.connect("confirmed", _close_tab.bind(ge, tab, true))
save_prompt.connect("cancelled", set.bind("is_closing_all_tabs", false))
save_prompt.connect("denied", _close_tab.bind(ge, tab))
add_child(save_prompt)
save_prompt.prompt_save(ge.file_path)
else:
_close_tab(ge, tab)
func update_tab_savestate(graph_edit):
var index = graph_edit.get_index()
var trim = tab_bar.get_tab_title(index).trim_suffix(UNSAVED_FILE_SUFFIX)
var title = trim + UNSAVED_FILE_SUFFIX if graph_edit.is_unsaved() else trim
tab_bar.set_tab_title(index, title)

Move the stuff from that section out to its own tab control. This control should manage everything related to tab creation, selection and deletion, where each tab represents showing/hiding of its corresponding graph edit.

Add action history for undo/redo functionality

Use Case

Quality of life.

Current Implementation

No action history. If the user accidentally deletes a node, they have to reload the file before save. I believe this functionality is important for serious users who want to use Monologue as a key part of their workflow.

Proposed Implementation

MonologueNodePanel and GraphEdit should keep track of its own history (let's say 20 actions each). If the user is in SentenceNodePanel (basically if that panel is visible), then undo/redo only affects that SentenceNodePanel. If the user is in the GraphEdit (when side panel is not visible), then undo/redo affects that GraphEdit. This way, different GraphEdits (different tabs) will also keep track of its own action history.

This means UI controls within the individual side node panel or graph will need to connect to the "changed" signal, so whenever a change is detected, it adds it to the list of actions. For LineEdits and TextEdits, it is better to refactor them to only trigger on focus_exited, otherwise it will populate the history incredibly quickly for every character typed. There are some additional actions such as adding and deleting of graph nodes that need to be handled slightly differently for undo/redo.

Proposed default hotkey to trigger undo/redo is Ctrl + Z and Ctrl + Shift + Z respectively, following the convention of the Save hotkey (Ctrl + S). Anticipated type of actions to keep track of are as follows:

  • Changes in any LineEdit, TextEdit, OptionButton (dropdowns), radio buttons, checkboxes
  • Adding and deleting options in ChoiceNode
  • Connecting and disconnecting graph nodes (be wary of stuff like connection_from_empty which handles both creation and connection at once)
  • Adding and deleting graph nodes

Separate GraphNodeSelecter from MonologueControl.gd

This issue breaks down refactoring efforts to separate concerns from MonologueControl.gd, so contributors can branch out and work on it without much conflict/overlap. Collaborators can assign themselves to the issue they're working on, or feel free to leave a comment if you're working on it from a fork.

GraphNodeSelecter has an embedded script, move it out into its own .gd file and move the MonologueControl.gd section for graph node picker out:

##################################
# Graph node selecter (picker) #
##################################
## Start the picker mode from a given node and port. Picker mode is where
## a new node is created from another node through a connection to empty.
func enable_picker_mode(from_node, from_port, _release_position):
graph_node_selecter.position = get_viewport().get_mouse_position()
graph_node_selecter.show()
picker_from_node = from_node
picker_from_port = from_port
var graph = get_current_graph_edit()
picker_position = (graph.get_local_mouse_position() + graph.scroll_offset) / graph.zoom
picker_mode = true
no_interactions_dimmer.show()
## Exit picker mode. Picker mode is where a new node is created from another
## node through a connection to empty.
func disable_picker_mode():
graph_node_selecter.hide()
picker_mode = false
no_interactions_dimmer.hide()
func _on_graph_node_selecter_focus_exited():
disable_picker_mode()
func _on_graph_node_selecter_close_requested():
disable_picker_mode()

GraphNodeSelector should have its own .tscn and handles everything about graph node picking.

Add voiceline field and AudioPlayer designated for SentenceNodes

I'm proposing some enhancement requests here that are involve the direction/architecture of Monologue; these aren't just bug fixes so I think it's good to discuss first.

Use Case

Handling of voice lines for sentences. Developers mainly tie a voice line to each line of dialogue, whether in .csv or .json, they go hand-in-hand.

Current Implementation

The user has to create an ActionNode set to PlayAudio before every SentenceNode to play voice lines. This also has a clear separation between audio and dialogue, which doesn't reflect how a real developer organizes voice lines to dialogue.

Proposed Implementation

voice_sentence

Add an additional Voiceline field in SentenceNodePanel for the user to specify a sound file there. Then, add AudioPlayer to the Test story to play it because the user can skip dialogue lines, so audio playback is somewhat controlled by user skips. Whereas ActionNode PlayAudio shall be used for sound effects and other ambient stuff that would not stop abruptly just because a dialogue skip occurred.

Separate recent file container from MonologueControl.gd

This issue breaks down refactoring efforts to separate concerns from MonologueControl.gd, so contributors can branch out and work on it without much conflict/overlap. Collaborators can assign themselves to the issue they're working on, or feel free to leave a comment if you're working on it from a fork.

Recent file history container is just a small part but it should still be separated so that it is more reusable/testable. We want do be able to do stuff to it without affecting other things, so separate this out from MonologueContol.gd:

# Load recent files
if not FileAccess.file_exists(HISTORY_FILE_PATH):
FileAccess.open(HISTORY_FILE_PATH, FileAccess.WRITE)
%RecentFilesContainer.hide()
else:
var file = FileAccess.open(HISTORY_FILE_PATH, FileAccess.READ)
var raw_data = file.get_as_text()
if raw_data:
var data: Array = JSON.parse_string(raw_data)
for path in data:
if FileAccess.file_exists(path):
continue
data.erase(path)
for path in data.slice(0, 3):
var btn: Button = recent_file_button.instantiate()
var btn_text = path.replace("\\", "/")
btn_text = btn_text.replace("//", "/")
btn_text = btn_text.split("/")
if btn_text.size() >= 2:
btn_text = btn_text.slice(-2, btn_text.size())
btn_text = btn_text[0].path_join(btn_text[1])
else:
btn_text = btn_text.back()
btn.text = Util.truncate_filename(btn_text)
btn.pressed.connect(file_selected.bind(path, 1))
%RecentFilesButtonContainer.add_child(btn)
%RecentFilesContainer.show()
else:
%RecentFilesContainer.hide()

EndPathNode does not go to next story in Test

Version: Tested in v2.1.2, still present in v2.2.3
Severity: Medium

Steps to Reproduce

  1. Create an EndPathNode.
  2. Edit next story to a file name (with or without ".json" suffix) in the same directory as the current file.

nextstory4

nextstory1

nextstory2

  1. Start test and proceed to EndPathNode.

Current Behavior

Test does not go to next story and just freezes.

nextstory3

Expected Behavior

Should load next json file and play it, keeping any variable values from the previous json play. If I understand correctly, if the next json has the same variable names, their default values will simply be replaced by any previous values stored in the Test run.

Add float variable type

Use Case

A story may have a simple money or math conversation that it would be good to store values with decimal points, so users can have some haggling where the price changes in their dialogue for example. Monologue's strength is with its cross-compatibility via JSON, the data may be read by other engines or applications, so it would be great to have float values be managed by Monologue too, at least it's quite a common primitive type.

Current Implementation

No float variable type.

Proposed Implementation

When adding this variable type, consider refactoring the way Variable is handled in Monologue. Notice how there are many match and checking of 0, 1, 2 indices that are common across multiple nodes such as RootNode, ActionNode and ConditionNode. Each of these node panel controls are slightly different from one another, but we can create a VariableHandler node with @export variables that can take in user-defined UI controls from the Inspector, so that this VariableHandler can update the GUI controls as needed while still retaining the core logic of dealing with variables in its own script. With this refactoring, adding new variable types across all related nodes will be trivial.

Integer variable min-max value is too limited

Use Case

Negative numbers are a thing, and plenty of conversation flow may use values above 100.

Current Implementation

The user can type in any value in the Integer variable spinner box:

beforeint

But after exiting focus or pressing enter, the spinner will confine the value for Integer variables in RootNode, ActionNode or ConditionNode between 0 to 100.

afterint

Proposed Implementation

Use Godot engine's actual min-max value for 64-bit int.

GraphNode rework

All GraphNodes are hardcoded and all their fields are implemented by hand, even when it's for very simple things.

The problem is that the slightest change can very easily break everything, and is a pain in the ass every time.
The aim would therefore be to make the GraphNode code as simple as possible and put everything in the MonologueGraphNode class.

Currently, the way these changes have been made is that control nodes with the prefix "Field_" automatically become fields, which will be registered and monitored automatically.
The value of these nodes is automatically associated with a key in the name following the "Field_" prefix.

For example :
A LineEdit node with the name "Field_Sentence" will be linked to the "Sentence" field, and when loaded will take the value of the "Sentence" key. Each time the LineEdit changes, the value of "Sentence" will be updated. There's no need to specify that the "Sentence" key exists, as it will be created automatically.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.