extends Node2D # Instructions data var vn_labels = {} var vn_lines = [] # Runtime var vn_can_save = false var vn_play_advance_noise = false var vn_hash = "" var vn_title = "" var vn_music = "" var vn_pos: int = 0 var vn_pos_savestate: int = 0 var vn_pos_offset: int = 0 var vn_sprite_map = {} var vn_bg_animation = "RESET" var vn_overlay_color = "00000000" var vn_vars = {} var vn_initial_state = {} # Instantiable scenes @onready var vn_sprite: PackedScene = preload("res://scenes/ui_elements/vn_sprite.tscn") enum VNInstruction { VN_NOOP = 100, VN_QUIT = 101, VN_ESCAPE = 102, VN_LOAD = 103, VN_COMMENT = 104, VN_SHOW_TEXTBOX = 105, VN_HIDE_TEXTBOX = 106, VN_EXPAND_TEXTBOX = 107, VN_COLLAPSE_TEXTBOX = 108, VN_TEXT = 109, VN_CONTINUE_AFTER_TEXT = 110, VN_STOP = 111, VN_GOTO = 112, VN_LABEL = 113, VN_SET = 114, VN_INCREMENT = 115, VN_DECREMENT = 116, VN_IF = 117, VN_PROMPT = 118, VN_WAIT = 119, VN_CLEAR_SPRITES = 120, VN_SET_SPRITE = 121, VN_REMOVE_SPRITE = 122, VN_START_MUSIC = 123, VN_STOP_MUSIC = 124, VN_TITLE = 125, VN_SET_BACKGROUND = 126, VN_OVERLAY_COLOR = 127, VN_PLAY_VIDEO = 128, VN_SOUND_EFFECT = 129, } func parse_vn_file(input_file: String): var file = FileAccess.open(input_file, FileAccess.READ) var parsed_labels = {} var parsed_lines = [] var parsed_vars = [] var is_multiline_text: bool = false var line_count = 0 while file.get_position() < file.get_length(): var line = file.get_line().lstrip(" ").rstrip(" ") line_count += 1 if not line: is_multiline_text = false elif line[0] == "#": is_multiline_text = false parsed_lines.append([VNInstruction.VN_COMMENT, line.right(-1).lstrip(" ")]) elif line[0] == "$": var data = line.split(" ", false) match data[0]: # No parameters "$quit": parsed_lines.append([VNInstruction.VN_QUIT]) "$end": # Alias of $quit parsed_lines.append([VNInstruction.VN_QUIT]) "$show_textbox": parsed_lines.append([VNInstruction.VN_SHOW_TEXTBOX]) "$hide_textbox": parsed_lines.append([VNInstruction.VN_HIDE_TEXTBOX]) "$expand_textbox": parsed_lines.append([VNInstruction.VN_EXPAND_TEXTBOX]) "$collapse_textbox": parsed_lines.append([VNInstruction.VN_COLLAPSE_TEXTBOX]) "$continue_after_text": parsed_lines.append([VNInstruction.VN_CONTINUE_AFTER_TEXT]) "$stop": parsed_lines.append([VNInstruction.VN_STOP]) "$prompt": parsed_lines.append([VNInstruction.VN_PROMPT]) "$clear_sprites": parsed_lines.append([VNInstruction.VN_CLEAR_SPRITES]) "$reset_background": # Alias of $set_background RESET parsed_lines.append([VNInstruction.VN_SET_BACKGROUND, "RESET", false]) # One parameter "$text_new_line": # remainder_of_the_text if is_multiline_text: parsed_lines[-1][2] += "\n%s" % line.split(" ", false, 1)[1].lstrip(" ").rstrip(" ") else: assert(false, "Cannot add new line to text without a preceding text!") continue "$title": # some text parsed_lines.append([VNInstruction.VN_TITLE, line.split(" ", false, 1)[1]]) "$load": # next_vn parsed_lines.append([VNInstruction.VN_LOAD, data[1]]) "$goto": # label_name parsed_lines.append([VNInstruction.VN_GOTO, data[1]]) "$label": # label_name parsed_labels[data[1]] = parsed_lines.size() parsed_lines.append([VNInstruction.VN_LABEL, data[1]]) "$increment": # var_name # Initialize var if not parsed_vars.has(data[1]): parsed_vars.append(data[1]) parsed_lines.append([VNInstruction.VN_INCREMENT, data[1]]) "$decrement": # var_name # Initialize var if not parsed_vars.has(data[1]): parsed_vars.append(data[1]) parsed_lines.append([VNInstruction.VN_DECREMENT, data[1]]) "$wait": # [time_secs] parsed_lines.append([VNInstruction.VN_WAIT, float(data[1])]) "$remove_sprite": # layer_name parsed_lines.append([VNInstruction.VN_REMOVE_SPRITE, data[1]]) "$stop_music": # [crossfade_secs] parsed_lines.append([VNInstruction.VN_STOP_MUSIC, 5.0]) if data.size() >= 2: parsed_lines[-1].insert(1, float(data[1])) "$play_video": # video_name parsed_lines.append([VNInstruction.VN_PLAY_VIDEO, data[1]]) "$sound_effect": # sfx_name parsed_lines.append([VNInstruction.VN_SOUND_EFFECT, data[1]]) # Two parameters "$set_background": # bg_animation_name [await_bg_animation] if $"../Background/AnimationPlayer".has_animation(data[1]): parsed_lines.append([VNInstruction.VN_SET_BACKGROUND, data[1], false]) if data.size() >= 3: if data[2] == "true": parsed_lines[-1][2] = true elif data[2] == "false": parsed_lines[-1][2] = false else: assert(false, "Invalid set_background boolean option '%s'" % data[2]) else: assert(false, "Unknown background animation '%s'" % data[1]) "$set": # var_name var_value # Initialize var if not parsed_vars.has(data[1]): parsed_vars.append(data[1]) parsed_lines.append([VNInstruction.VN_SET, data[1], int(data[2])]) "$option": # branch_label_name text var last_line = parsed_lines[-1] if last_line[0] == VNInstruction.VN_PROMPT: last_line.append([data[1], line.split(" ", false, 2)[2]]) else: assert(false, "Error on file '%s', line %d: $option command must only come after a $prompt command" % [input_file, line_count]) "$start_music": # track_name [crossfade_secs] parsed_lines.append([VNInstruction.VN_START_MUSIC, data[1], 0.0]) if data.size() >= 3: parsed_lines[-1].insert(2, float(data[2])) "$overlay_color": # color [fade_secs] parsed_lines.append([VNInstruction.VN_OVERLAY_COLOR, Color(data[1]).to_html(), 1.0]) if data.size() >= 3: parsed_lines[-1].insert(2, float(data[2])) # Four parameters "$if": # var_name comparison_operator var_value branch_label_name # Initialize var if not parsed_vars.has(data[1]): parsed_vars.append(data[1]) if data[2] not in [">", "<", ">=", "<=", "==", "!="]: assert(false, "Error on file '%s', line %d: Invalid operator for $if statement: '%s'" % [input_file, line_count, data[2]]) parsed_lines.append([VNInstruction.VN_IF, data[1], data[2], int(data[3]), data[4]]) "$set_sprite": # layer_name sprite_path [animation_name [{"flip_h":true,...}]] parsed_lines.append([VNInstruction.VN_SET_SPRITE, data[1], data[2], "RESET", {}]) if data.size() >= 4: parsed_lines[-1][3] = data[3] if data.size() >= 5: parsed_lines[-1][4] = JSON.parse_string(line.split(" ", false, 4)[4]) # Variable parameters "$escape": # [arg [...]] parsed_lines.append([VNInstruction.VN_ESCAPE]) parsed_lines[-1].append_array(data.slice(1)) _: assert(false, "Error on file '%s', line %d: Unknown command '%s'" % [input_file, line_count, data]) is_multiline_text = false else: var data = line.split(":", true, 1) if data.size() < 1: # Empty dialogue (i.e. line with only ":" and spaces) is ignored continue elif data.size() == 1: if is_multiline_text: parsed_lines[-1][2] += "\n%s" % data[0].lstrip(" ").rstrip(" ") else: parsed_lines.append([VNInstruction.VN_TEXT, "", data[0].lstrip(" ").rstrip(" ")]) is_multiline_text = true else: parsed_lines.append([VNInstruction.VN_TEXT, data[0].rstrip(" "), data[1].lstrip(" ")]) is_multiline_text = true file.close() var vn_data = { "labels": parsed_labels, "vars": parsed_vars, "lines": parsed_lines, } # Hash data to snapshot for save states var hash_ctx = HashingContext.new() hash_ctx.start(HashingContext.HASH_SHA256) # hash_ctx.update(var_to_bytes_with_objects(vn_data)) hash_ctx.update(var_to_bytes(vn_data)) vn_data["hash"] = hash_ctx.finish().hex_encode() return vn_data func sprite_map_to_array(): var sprite_values = vn_sprite_map.values() sprite_values.sort_custom(func (a, b): return a["idx"] < b["idx"]) return sprite_values.map(func (sprite): return { "layer": vn_sprite_map.find_key(sprite), "texture": sprite["texture"], "properties": sprite["properties"], }) func save_vn(): return { "hash": vn_hash, "title": vn_title, "pos": vn_pos_savestate - vn_pos_offset, "sprite_list": sprite_map_to_array(), "bg_animation": vn_bg_animation, "overlay_color": vn_overlay_color, "music": vn_music, "vars": vn_vars, "initial_state": vn_initial_state, } func load_vn_data(vn_data, save_state = {}): # Next line might not be necessary...? vn_initial_state = save_state.get("initial_state", vn_initial_state) vn_hash = vn_data["hash"] # Restore original VN state if hash changed (i.e. VN was modified) if save_state.get("hash", vn_hash) != vn_hash: save_state = save_state.get("initial_state", {}) vn_pos = save_state.get("pos", 0) vn_vars.merge(save_state.get("vars", {}), true) vn_title = save_state.get("title", vn_title) vn_music = save_state.get("music", vn_music) var sprite_list = save_state.get("sprite_list", []) vn_bg_animation = save_state.get("bg_animation", vn_bg_animation) vn_overlay_color = save_state.get("overlay_color", vn_overlay_color) vn_pos_savestate = vn_pos # Grab VN data vn_labels = vn_data.labels for preloaded_var in vn_data.vars: if preloaded_var not in vn_vars: vn_vars[preloaded_var] = 0 # vn_initial_state["vars"].merge(vn_vars, false) vn_lines = vn_data.lines if vn_lines.size() > 0 and vn_lines[0][0] == VNInstruction.VN_TITLE: vn_title = vn_lines[0][1] $"../OverlayColor/ColorRect".color = vn_overlay_color if sprite_list: vn_sprite_map = {} for i in sprite_list.size(): var sprite_data = sprite_list[i] var curr_sprite = vn_sprite.instantiate() curr_sprite.name = sprite_data["layer"] $"../Sprites".add_child(curr_sprite) populate_sprite_data(curr_sprite.get_node("Sprite"), sprite_data["texture"], sprite_data["properties"]) curr_sprite.get_node("AnimationPlayer").play("RESET") vn_sprite_map[sprite_data["layer"]] = { "idx": i, "texture": sprite_data["texture"], "properties": sprite_data["properties"], } $"../Background/AnimationPlayer".play(vn_bg_animation) update_window_title() if vn_music: if not MusicManager.is_playing("vn_music", vn_music): MusicManager.play("vn_music", vn_music, 0.0) # Special case: Continue main menu music only with new game elif vn_vars.get("autostop_music", 1) == 1 and MusicManager.is_playing(): MusicManager.stop(0.0) # Save initial state of VN (this state is restored when VN hash changes) if save_state.is_empty() and vn_pos == 0: vn_initial_state = { "title": vn_title, "pos": 0, "sprite_list": sprite_map_to_array(), "bg_animation": vn_bg_animation, "overlay_color": vn_overlay_color, "music": vn_music, "vars": vn_vars, } vn_can_save = true func populate_sprite_data(sprite_node: Sprite2D, texture: String, properties: Dictionary): sprite_node.texture = load("res://images/sprites/%s.png" % texture) for prop in properties: sprite_node[prop] = properties[prop] func update_window_title(): if vn_title: get_window().title = "Crossing Over - %s" % vn_title else: get_window().title = "Crossing Over" func can_save() -> bool: return vn_can_save func can_play_sfx() -> bool: return not FileGlobals.get_global_data("muted") and not vn_vars.get("disable_sfx", 0) func can_play_music() -> bool: return not FileGlobals.get_global_data("muted") func advance_sfx(): if can_play_sfx(): #SoundManager.play("vn_sfx", "advance") SoundManager.play_varied("vn_sfx", "advance", 1.0, FileGlobals.get_global_data("volume", 0.0)) func advance_vn(label = null) -> Array: var continue_after_text = false if vn_play_advance_noise: advance_sfx() vn_play_advance_noise = false if label: vn_pos = vn_labels[label] while true: vn_can_save = false if vn_pos >= vn_lines.size(): return ["@eof"] var curr_line = vn_lines[vn_pos] vn_pos_savestate = vn_pos vn_pos_offset = 0 vn_pos += 1 match curr_line[0]: # Textbox handling commands VNInstruction.VN_SHOW_TEXTBOX: await $"../Textbox".show_textbox() VNInstruction.VN_HIDE_TEXTBOX: await $"../Textbox".hide_textbox() VNInstruction.VN_EXPAND_TEXTBOX: await $"../Textbox".expand_textbox() VNInstruction.VN_COLLAPSE_TEXTBOX: await $"../Textbox".collapse_textbox() VNInstruction.VN_TEXT: vn_can_save = true await $"../Textbox".write_text(curr_line[2], curr_line[1]) $"../ChatLog".add_line(curr_line[2], curr_line[1]) if continue_after_text: continue_after_text = false else: vn_play_advance_noise = true break VNInstruction.VN_CONTINUE_AFTER_TEXT: continue_after_text = true # Control flow commands VNInstruction.VN_NOOP: continue VNInstruction.VN_TITLE: vn_title = curr_line[1] VNInstruction.VN_QUIT: return ["@quit"] VNInstruction.VN_LOAD: # return ["@load", curr_line[1], save_vn()] return ["@load", curr_line[1]] VNInstruction.VN_ESCAPE: var escape_args = ["@escape"] for arg in curr_line.slice(1): if arg[0] == "$": escape_args.append(vn_vars.get(arg.right(-1), arg)) else: escape_args.append(arg) return escape_args VNInstruction.VN_STOP: vn_pos_offset = 1 vn_can_save = true vn_play_advance_noise = true break VNInstruction.VN_GOTO: vn_pos = vn_labels[curr_line[1]] VNInstruction.VN_WAIT: $"../Textbox".disable_textbox() $"../Timer".start(curr_line[1]) await $"../Timer".timeout VNInstruction.VN_LABEL: continue VNInstruction.VN_COMMENT: continue VNInstruction.VN_PROMPT: var options_array = curr_line.slice(1) if options_array.size() <= 0: push_warning("Skipping prompt with no complete options.") continue $"../Textbox".disable_textbox() $"../Prompt".clear_options() # Pass new options to prompt while options_array.size() > 0: match options_array.pop_front(): [var option_label, var option_text]: $"../Prompt".create_option(option_label, option_text) var invalid_option: push_warning("Ignoring invalid option '%s'." % invalid_option) $"../Prompt".show_prompt() # Save any text present in the textbox if vn_pos_savestate > 0: if vn_lines[vn_pos_savestate - 1][0] == VNInstruction.VN_TEXT: vn_pos_offset = 1 vn_can_save = true #var prompt_signal = await $"../Prompt".prompt_option_selected #var option_label = prompt_signal[0] #var option_text = prompt_signal[1] match await $"../Prompt".prompt_option_selected: [var option_label, var option_text]: advance_sfx() await $"../Prompt".hide_prompt() vn_pos = vn_labels[option_label] $"../Textbox".enable_textbox() $"../ChatLog".add_selected_option_text(option_text) continue VNInstruction.VN_IF: var condition = false match curr_line[2]: ">": condition = vn_vars[curr_line[1]] > curr_line[3] "<": condition = vn_vars[curr_line[1]] < curr_line[3] ">=": condition = vn_vars[curr_line[1]] >= curr_line[3] "<=": condition = vn_vars[curr_line[1]] <= curr_line[3] "==": condition = vn_vars[curr_line[1]] == curr_line[3] "!=": condition = vn_vars[curr_line[1]] != curr_line[3] _: push_warning("Unknown conditional operator '%s'. Not branching." % curr_line[2]) if condition: vn_pos = vn_labels[curr_line[4]] # Variable mangling commands VNInstruction.VN_SET: vn_vars[curr_line[1]] = curr_line[2] VNInstruction.VN_INCREMENT: vn_vars[curr_line[1]] += 1 VNInstruction.VN_DECREMENT: vn_vars[curr_line[1]] -= 1 # Extra media VNInstruction.VN_CLEAR_SPRITES: vn_sprite_map = {} for node in $"../Sprites".get_children(): node.visible = false $"../Sprites".remove_child(node) node.queue_free() VNInstruction.VN_REMOVE_SPRITE: var curr_sprite = $"../Sprites".get_node_or_null(curr_line[1]) if curr_sprite == null: push_warning("Can't remove unknown sprite '%s'. Ignoring." % curr_line[1]) continue #var curr_idx = vn_sprite_map.get(curr_line[1], {"idx": -1})["idx"] var curr_idx = curr_sprite.get_index() #if curr_idx == -1: # push_warning("Can't remove unknown sprite '%s'. Ignoring." % curr_line[1]) # continue vn_sprite_map.erase(curr_line[1]) for k in vn_sprite_map.keys(): if vn_sprite_map[k]["idx"] > curr_idx: vn_sprite_map[k]["idx"] -= 1 curr_sprite.visible = false $"../Sprites".remove_child(curr_sprite) curr_sprite.queue_free() VNInstruction.VN_SET_SPRITE: var curr_sprite = $"../Sprites".get_node_or_null(curr_line[1]) # var already_exists = curr_sprite != null if not curr_sprite: # Create new sprite vn_sprite_map[curr_line[1]] = { "idx": $"../Sprites".get_children().size(), "texture": curr_line[2], "properties": curr_line[4], } curr_sprite = vn_sprite.instantiate() curr_sprite.name = curr_line[1] # Make sure reveal_fishing_item doesn't flash if curr_line[3] == "reveal_fishing_item": curr_sprite.get_node("Sprite").self_modulate = "#000" curr_line[4].erase("self_modulate") else: curr_sprite.get_node("Sprite").self_modulate = "#fff" $"../Sprites".add_child(curr_sprite) var sprite_node = curr_sprite.get_node("Sprite") as Sprite2D var animation_player = curr_sprite.get_node("AnimationPlayer") as AnimationPlayer # TODO: Figure out weird bugs with sprite props #if not already_exists: # animation_player.queue("RESET") populate_sprite_data(sprite_node, curr_line[2], curr_line[4]) if curr_line[3] == "RESET": sprite_node.visible = true sprite_node.self_modulate = "#fff" else: if animation_player.get_animation_library("").has_animation(curr_line[3]): animation_player.queue(curr_line[3]) else: push_warning("Unknown sprite animation '%s'. Skipping animation." % curr_line[3]) VNInstruction.VN_START_MUSIC: vn_music = curr_line[1] if MusicManager.is_playing("vn_music", vn_music): print_debug("Already playing track '%s'" % vn_music) else: MusicManager.play("vn_music", vn_music, curr_line[2]) VNInstruction.VN_STOP_MUSIC: vn_music = "" if MusicManager.is_playing(): MusicManager.stop(curr_line[1]) VNInstruction.VN_SOUND_EFFECT: if can_play_sfx(): #SoundManager.play("vn_sfx", curr_line[1]) SoundManager.play_varied("vn_sfx", curr_line[1], 1.0, FileGlobals.get_global_data("volume", 0.0)) VNInstruction.VN_SET_BACKGROUND: if $"../Background/AnimationPlayer".has_animation(curr_line[1]): vn_bg_animation = curr_line[1] $"../Background/AnimationPlayer".play(vn_bg_animation) if curr_line[2]: $"../Textbox".disable_textbox() await $"../Background/AnimationPlayer".animation_finished else: push_warning("Unknown background animation '%s'. Ignoring." % curr_line[1]) VNInstruction.VN_OVERLAY_COLOR: $"../Textbox".disable_textbox() vn_overlay_color = curr_line[1] var color = Color.html(vn_overlay_color) if curr_line[2] >= 0.09: var tween = get_tree().create_tween() tween.tween_property($"../OverlayColor/ColorRect", "color", color, curr_line[2]) await tween.finished else: $"../OverlayColor/ColorRect".color = color VNInstruction.VN_PLAY_VIDEO: var video_file = "res://media/%s.ogv" % curr_line[1] if not FileAccess.file_exists(video_file): assert(false, "Video '%s.ogv' not found in res://media" % curr_line[1]) continue $"../Textbox".disable_textbox() var video_stream = VideoStreamTheora.new() video_stream.file = video_file var player: VideoStreamPlayer = $"../Video/VideoStreamPlayer" player.stream = video_stream player.volume = 0 player.visible = true player.play() await player.finished player.visible = false player.stream = null _: push_warning("Skipping unknown command. (%s)" % curr_line) $"../Textbox".ready_textbox() return []