Initial commit

This commit is contained in:
Bad Manners 2024-03-14 00:53:46 -03:00
commit 7becdd23b6
989 changed files with 28526 additions and 0 deletions

View file

@ -0,0 +1,564 @@
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
#if can_play_music():
# player.volume_db = -12.0 + FileGlobals.get_global_data("volume", 0.0)
#else:
# player.volume = 0
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 []