From b076e6a2f5bb9b2642d72a9841a5eb01840f025b Mon Sep 17 00:00:00 2001 From: Sokomine Date: Thu, 29 Feb 2024 11:43:39 +0100 Subject: [PATCH] export_to_ink: works apart from preconditions and variables --- export_to_ink.lua | 341 +++++++++++++++++++++++++++++----------------- 1 file changed, 216 insertions(+), 125 deletions(-) diff --git a/export_to_ink.lua b/export_to_ink.lua index 424d856..140fc16 100644 --- a/export_to_ink.lua +++ b/export_to_ink.lua @@ -1,180 +1,271 @@ -- helper functions for export_to_ink_language: --- TODO: include alternate_text for effects --- TODO: include effect on_failure -- TODO: include effect setting variables to values (problem: diffrent characters allowed) -- TODO: include preconditions/conditions regarding setting variables --- helper function: --- do not quit the ink game if the player selected an end option - allow to continue talking -yl_speak_up.export_to_ink_rewrite_target = function(target, n_id) - if(not(target)) then - return "-> END" - elseif(target == "d_end" or target == tostring(n_id).."_d_end") then - return tostring(n_id).."_main" - end - return target + +-- this table will hold the functions for exporting to ink so that we don't fill that namespace too much +yl_speak_up.export_to_ink = {} + +-- an abbreviation +local ink_export = yl_speak_up.export_to_ink + + + +-- in order to be able to deal with multiple NPC in ink, we use the NPC id n_id +-- plus the dialog id d_id as a name prefix; o_id, a_id and r_id are appended +-- as needed +yl_speak_up.export_to_ink.print_knot_name = function(lines, knot_name) + table.insert(lines, "\n\n=== ") + table.insert(lines, tostring(knot_name or "ERROR")) + table.insert(lines, " ===") end +-- execution of effects ends if an on_failure effect is reached; for ink to be able to +-- display effects (as tags) correctly, we need to add them at the right place - some +-- tags come after the option/choice, some after the last action (if there is an action), +-- some between on_failure actions (if they exist) +yl_speak_up.export_to_ink.add_effect_tags = function(text, sorted_e_list, effects, start_at_effect) + if(not(text)) then + text = "" + end + if(not(start_at_effect) or start_at_effect > #sorted_e_list) then + return text + end + for i = start_at_effect, #sorted_e_list do + local r_id = sorted_e_list[i] + if(effects and effects[r_id]) then + local r = effects[r_id] + if(r and r.r_type and r.r_type == "on_failure") then + -- end as soon as we reach the next on_failure dialog + return text + end + if(r and r.r_type and r.r_type ~= "dialog") then + if(text ~= "") then + text = text.."\n " + end + -- the dialog effect is something diffrent + text = text.."# effect "..tostring(r_id).." "..tostring(yl_speak_up.show_effect(r)) + end + end + end + return text +end + + +-- choices are a bit complicated as they may contain alternate_text that is to be +-- displayed instead (in yl_speak_up) and before (in ink) shwoing the target dialog text; +-- also, the divert_to target dialog may need to be rewritten +yl_speak_up.export_to_ink.print_choice = function(lines, choice_text, n_id, start_dialog, + alternate_text, divert_to) + -- don't repeat the text of the choice in the output when running ink + table.insert(lines, "\n+ [") + table.insert(lines, choice_text) + table.insert(lines, "]") + -- dialogs, actions and effects can have an alternate_text with which they override the + -- text of the target_dialog/divert_to; + -- this isn't perfect as alternate_text supports $TEXT$ for inserting the text of the + -- target dialog anywhere in the alternate_text - while ink will print out this alternate_text + -- first and then that of the target dialog/divert_to + if(alternate_text and alternate_text ~= "") then + -- a new line and some indentation makes this more readable + table.insert(lines, "\n ") + table.insert(lines, alternate_text) + -- write the divert into a new line as well + table.insert(lines, "\n ") + end + table.insert(lines, " -> ") + if(not(start_dialog) or start_dialog == "") then + start_dialog = "d_1" + end + if(not(divert_to) or divert_to == "") then + -- go back to the start dialog (the start dialog may have been changed) + divert_to = tostring(n_id).."_"..tostring(start_dialog) + elseif(divert_to == "d_end" or divert_to == tostring(n_id).."_d_end") then + -- go back to choosing between talking to NPC and end + divert_to = tostring(n_id).."_main" + elseif(string.sub(divert_to, 1, 2) ~= "n_") then + -- make sure it is prefixed with the n_id + divert_to = tostring(n_id).."_"..tostring(divert_to) + end + table.insert(lines, divert_to) +end + + +-- this prints the dialog as a knot - but without choices (those are added to the lines table later) -- d: dialog -yl_speak_up.export_to_ink_dialog_knot = function(lines, d, n_d_id) - table.insert(lines, "\n\n=== ") - table.insert(lines, n_d_id) - table.insert(lines, " ===") +yl_speak_up.export_to_ink.print_dialog_knot = function(lines, n_id, d_id, d) + local knot_name = tostring(n_id).."_"..tostring(d_id) + ink_export.print_knot_name(lines, knot_name) + -- many characters at the start of a line have a special meaning; -- hopefully they will not be obstrusive later on; -- TODO: in order to be on the safe side: add a ":" in front of each line? local t = d.d_text or "" if(t == "") then -- entirely empty text for knots does not work - t = "[no text]" + t = "No text." end -- t = string.gsub(t, "\n([:>=])", "\n %1") table.insert(lines, "\n") table.insert(lines, t) - table.insert(lines, "\n") + return knot_name end --- a: action -yl_speak_up.export_to_ink_action_knot = function(lines, a, n_d_id, o_id, next_target, n_id) - table.insert(lines, "\n\n=== ") - table.insert(lines, n_d_id.."_"..tostring(o_id).."_"..tostring(a.a_id)) - table.insert(lines, " ===") - table.insert(lines, "\n:action:") +-- actions can fail *and* be aborted by the player; in order to model that in ink, we add +-- a knot for each action +-- Parameter: +-- a action +yl_speak_up.export_to_ink.print_action_knot = function(lines, n_id, d_id, o_id, start_dialog, + a, alternate_text_on_success, next_target) + local knot_name = tostring(n_id).."_"..tostring(d_id).."_"..tostring(o_id).."_"..tostring(a.a_id) + ink_export.print_knot_name(lines, knot_name) + + table.insert(lines, "\n:action: ") + table.insert(lines, a.a_id) + table.insert(lines, " ") table.insert(lines, yl_speak_up.show_action(a)) - table.insert(lines, "\n+ [Action was successful] -> ") - table.insert(lines, yl_speak_up.export_to_ink_rewrite_target(next_target, n_id)) - table.insert(lines, "\n+ [Action failed] ") - table.insert(lines, tostring(a.alternate_text or "")) - table.insert(lines, " -> ") - if(a.a_on_failure) then - next_target = tostring(n_id).."_"..tostring(a.a_on_failure) - else - next_target = next_target or "ERROR" - end - table.insert(lines, yl_speak_up.export_to_ink_rewrite_target(tostring(next_target), n_id)) - table.insert(lines, "\n+ [Back] -> ") - table.insert(lines, n_d_id) + + ink_export.print_choice(lines, "Action was successful", n_id, start_dialog, + alternate_text_on_success, next_target) + + ink_export.print_choice(lines, "Action failed", n_id, start_dialog, + a.alternate_text, a.a_on_failure) + + ink_export.print_choice(lines, "Back", n_id, start_dialog, + nil, tostring(n_id).."_"..tostring(d_id)) + return knot_name end --- r: effect/result --- r_prev: previous effect -yl_speak_up.export_to_ink_effect_knot = function(lines, r, n_d_id, o_id, next_target, r_prev, n_id) - table.insert(lines, "\n\n=== ") - table.insert(lines, n_d_id.."_"..tostring(o_id).."_"..tostring(r.r_id)) - table.insert(lines, " ===") - table.insert(lines, "\n:effect:") + +-- there is a special on_failure effect that can lead to a diffrent target dialog and print +-- out a diffrent alternate_text if the *previous* effect failed; in order to model that in +-- ink, we add a knot for such on_failure effects +-- Parameter: +-- r effect/result +-- r_prev previous effect +yl_speak_up.export_to_ink.print_effect_knot = function(lines, n_id, d_id, o_id, start_dialog, + r, r_prev, alternate_text_on_success, next_target) + local knot_name = tostring(n_id).."_"..tostring(d_id).."_"..tostring(o_id).."_"..tostring(r.r_id) + ink_export.print_knot_name(lines, knot_name) + + table.insert(lines, "\n:effect: ") + table.insert(lines, r.r_id) + table.insert(lines, " ") + -- show text of the *previous effect* - because that is the one which may have failed: + table.insert(lines, yl_speak_up.show_effect(r)) + + table.insert(lines, "\nThe previous effect was: ") + table.insert(lines, r_prev.r_id) + table.insert(lines, " ") -- show text of the *previous effect* - because that is the one which may have failed: table.insert(lines, yl_speak_up.show_effect(r_prev)) - table.insert(lines, "\n+ [Effect was successful] -> ") - table.insert(lines, yl_speak_up.export_to_ink_rewrite_target(next_target, n_id)) - table.insert(lines, "\n+ [Effect failed] -> ") - table.insert(lines, yl_speak_up.export_to_ink_rewrite_target(tostring(r.r_value or "ERROR"), n_id)) + + ink_export.print_choice(lines, "Effect was successful", n_id, start_dialog, + alternate_text_on_success, next_target) + + ink_export.print_choice(lines, "Effect failed", n_id, start_dialog, + r.alternate_text, r.r_value) + + return knot_name end yl_speak_up.export_to_ink_language = function(dialog, n_id) + local start_dialog = yl_speak_up.get_start_dialog_id(dialog) + if(not(start_dialog)) then + start_dialog = "d_1" + end + local main = tostring(n_id).."_main" local tmp = {"-> ", main, - "\n=== ", main, " ===\n", + "\n=== ", main, " ===", "\nWhat do you wish to do?", - "\n+ Talk to ", tostring(dialog.n_npc), " -> ", tostring(n_id).."_d_1", + "\n+ Talk to ", tostring(dialog.n_npc), " -> ", tostring(n_id).."_"..tostring(start_dialog), "\n+ End -> END"} local sorted_d_list = yl_speak_up.sort_keys(dialog.n_dialogs or {}, true) for i, d_id in ipairs(sorted_d_list) do -- store the knots for actions and effects here: local tmp2 = {} local d = dialog.n_dialogs[d_id] - local n_d_id = tostring(n_id).."_"..tostring(d_id) - yl_speak_up.export_to_ink_dialog_knot(tmp, d, n_d_id) + -- print the dialog knot, but without choices (those we add in the following loop) + local this_knot_name = ink_export.print_dialog_knot(tmp, n_id, d_id, d) + -- iterate over all options local sorted_o_list = yl_speak_up.get_sorted_options(dialog.n_dialogs[d_id].d_options or {}, "o_sort") for j, o_id in ipairs(sorted_o_list) do local o_data = d.d_options[o_id] - local target_action = nil - local target_effect = nil - local target_dialog = nil + local sorted_a_list = yl_speak_up.sort_keys(o_data.actions or {}) local sorted_e_list = yl_speak_up.sort_keys(o_data.o_results or {}) - -- we will get alternate_text from the dialog result later on - local alternate_text = "" - -- is there an action? That takes precedence. - if(#sorted_a_list > 0) then - -- this action dialog knot needs to be created: - target_action = n_d_id.."_"..tostring(o_id).."_"..tostring(sorted_a_list[1]) - alternate_text = "" - end - -- is there an effect/result of type on_failure? - local effect_list = {} - for k, r_id in ipairs(sorted_e_list) do - local r = o_data.o_results[r_id] - -- checking for previous effect failed? - if(not(target_effect) - and k > 1 and r and r.r_type and r.r_type == "on_failure") then - -- this effect dialog knot needs to be created: - target_effect = n_d_id.."_"..tostring(o_id).."_"..tostring(r_id) - elseif(target_effect - and k > 1 and r and r.r_type and r.r_type == "on_failure") then - -- collect all effects that need their own knots - table.insert(effect_list, target_effect) - -- normal target dialog? - elseif(not(target_dialog) - and r and r.r_type and r.r_type == "dialog") then - target_dialog = tostring(n_id).."_"..tostring(r.r_value) - alternate_text = r.alternate_text or "" - end - end - table.insert(effect_list, target_dialog) - local target = (target_action or target_effect or target_dialog or "d_1") - table.insert(tmp, "+ [") - table.insert(tmp, o_data.o_text_when_prerequisites_met) - table.insert(tmp, "] ") - table.insert(tmp, alternate_text) - -- add the actions - local next_target = "ERROR" - for k, a_id in ipairs(sorted_a_list) do - local a = o_data.actions[a_id] - if(k < #sorted_a_list) then - next_target = n_d_id.."_"..tostring(o_id).."_"..tostring(sorted_a_list[k+1]) - elseif(target_effect) then - next_target = target_effect - elseif(target_dialog) then - next_target = target_dialog - else - next_target = "d_1" - end - yl_speak_up.export_to_ink_action_knot(tmp2, a, n_d_id, o_id, next_target,n_id) - end - -- add the effects + -- we will get alternate_text from the dialog result later on + local alternate_text_on_success = "" + local target_dialog = nil + -- what is the normal target dialog/divert (in ink language) of this dialog? for k, r_id in ipairs(sorted_e_list) do local r = o_data.o_results[r_id] - -- checking for previous effect failed? - if(not(target_effect) - and k > 1 and r and r.r_type and r.r_type == "on_failure") then - -- this effect dialog knot needs to be created: - if(effect_list > 0) then - next_target = effect_list[1] - table.remove(effect_list, 1) - else - next_target = "ERROR" - end - yl_speak_up.export_to_ink_effect_knot(tmp2, r, n_d_id, o_id, next_target, o_data.o_results[sorted_e_list[k-1]], n_id) - elseif(not(r) or not(r.r_type) or r.r_type ~= "dialog") then - -- add the effect as a tag - -- TODO: sometimes this needs to be added at diffrent places (action, previous effect) - table.insert(tmp, "\n # effect ") - table.insert(tmp, yl_speak_up.show_effect(r)) + if(r and r.r_type and r.r_type == "dialog") then + target_dialog = tostring(n_id).."_"..tostring(r.r_value) + alternate_text_on_success = r.alternate_text or "" end end - table.insert(tmp, "\n -> ") - table.insert(tmp, yl_speak_up.export_to_ink_rewrite_target(target, n_id)) - table.insert(tmp, "\n") - end + + -- iterate backwards through the effects and serach for on_failure; + -- the first effect cannot be an on_failure effect because on_failure effects + -- decide on failure/success of the *previous* effect + for k = #sorted_e_list, 2, -1 do + local r_id = sorted_e_list[k] + local r = o_data.o_results[r_id] + if(r and r.r_type and r.r_type == "on_failure") then + local r_prev = o_data.o_results[sorted_e_list[k-1]] + -- *after* this effect we still need to execute all the other + -- remaining effects (read: add them as tag) + alternate_text_on_success = ink_export.add_effect_tags( + alternate_text_on_success, + sorted_e_list, o_data.o_results, k) + -- whatever dialog comes previously - the dialog, an action, or + -- another on_failure dialog - needs to lead to this dialog + target_dialog = ink_export.print_effect_knot(tmp2, + n_id, d_id, o_id, start_dialog, + r, r_prev, + alternate_text_on_success, target_dialog) + -- we have dealt with the alternate text (it will only be shown + -- in the last on_failure dialog before we go to the target) + alternate_text_on_success = "" + end + end + + -- add the remaining effects + alternate_text_on_success = ink_export.add_effect_tags( + alternate_text_on_success, + sorted_e_list, o_data.o_results, 1) + + -- iterate backwards through the actions (though usually only one is supported) + for k = #sorted_a_list, 1, -1 do + local a_id = sorted_a_list[k] + local a = o_data.actions[a_id] + + target_dialog = ink_export.print_action_knot(tmp2, + n_id, d_id, o_id, start_dialog, + a, + alternate_text_on_success, target_dialog) + -- has been dealt with + alternate_text_on_success = "" + end + + -- what remains is to print the option/choice itself + ink_export.print_choice(tmp, + -- TODO: deal with when_prerequisites_not_met + o_data.o_text_when_prerequisites_met, n_id, start_dialog, + alternate_text_on_success, target_dialog) + end -- dealt with the option table.insert(tmp, "\n") -- add way to end talking to the NPC - table.insert(tmp, "\n+ Farewell! -> "..tostring(n_id).."_main") + ink_export.print_choice(tmp, "Farewell!", n_id, start_dialog, + nil, tostring(n_id).."_main") + -- add the knots for actions and effects for this dialog and all its options: for _, line in ipairs(tmp2) do table.insert(tmp, line)